Skip to content

Commit

Permalink
Merge pull request #3843 from vodorok/store_limit
Browse files Browse the repository at this point in the history
[server] Rate limit based on report count
  • Loading branch information
dkrupp authored Oct 19, 2023
2 parents fcd5f23 + 76adc4a commit fa41e4e
Show file tree
Hide file tree
Showing 23 changed files with 278 additions and 47 deletions.
4 changes: 4 additions & 0 deletions docs/web/products.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ making a new product available on the server.
```
usage: CodeChecker cmd products add [-h] [-n DISPLAY_NAME]
[--description DESCRIPTION]
[--report-limit REPORT_LIMIT]
[--sqlite SQLITE_FILE | --postgresql]
[--dbaddress DBADDRESS] [--dbport DBPORT]
[--dbusername DBUSERNAME]
Expand All @@ -142,6 +143,9 @@ optional arguments:
--description DESCRIPTION
A custom textual description to be shown alongside the
product.
--report-limit REPORT_LIMIT
The maximum number of reports allowed to store in one
run, if exceeded, the storeaction will be rejected.
database arguments:
NOTE: These database arguments are relative to the server machine, as it
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/js/codechecker-api-node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codechecker-api",
"version": "6.53.0",
"version": "6.54.0",
"description": "Generated node.js compatible API stubs for CodeChecker server.",
"main": "lib",
"homepage": "https://github.com/Ericsson/codechecker",
Expand Down
6 changes: 4 additions & 2 deletions web/api/products.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ struct ProductConfiguration {
5: optional DatabaseConnection connection,
6: i64 runLimit,
7: optional bool isReviewStatusChangeDisabled,
8: optional Confidentiality confidentiality
8: optional Confidentiality confidentiality,
9: optional i64 reportLimit
}
typedef list<ProductConfiguration> ProductConfigurations

Expand All @@ -53,7 +54,8 @@ struct Product {
11: i64 runLimit, // Number of allowed runs for this product.
12: list<string> admins, // Administrators of this product.
13: list<string> runStoreInProgress, // List of run names which are in progress.
14: optional Confidentiality confidentiality // Confidentiality classification of the product
14: optional Confidentiality confidentiality, // Confidentiality classification of the product.
15: optional i64 reportLimit // Maximum number of reports allowed in a run.
}
typedef list<Product> Products

Expand Down
Binary file modified web/api/py/codechecker_api/dist/codechecker_api.tar.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/py/codechecker_api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
with open('README.md', encoding='utf-8', errors="ignore") as f:
long_description = f.read()

api_version = '6.53.0'
api_version = '6.54.0'

setup(
name='codechecker_api',
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion web/api/py/codechecker_api_shared/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
with open('README.md', encoding='utf-8', errors="ignore") as f:
long_description = f.read()

api_version = '6.53.0'
api_version = '6.54.0'

setup(
name='codechecker_api_shared',
Expand Down
9 changes: 9 additions & 0 deletions web/client/codechecker_client/cmd/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,15 @@ def __register_add(parser):
help="A custom textual description to be shown "
"alongside the product.")

parser.add_argument('--report-limit',
type=int,
dest="report_limit",
default=argparse.SUPPRESS,
required=False,
help="The maximum number of reports allowed to "
"store in one run, if exceeded, the store "
"action will be rejected.")

dbmodes = parser.add_argument_group(
"database arguments",
"NOTE: These database arguments are relative to the server "
Expand Down
48 changes: 44 additions & 4 deletions web/client/codechecker_client/cmd/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@
from codechecker_report_converter import twodim
from codechecker_report_converter.report import Report, report_file, \
reports as reports_helper, statistics as report_statistics
from codechecker_report_converter.report.hash import HashType
from codechecker_report_converter.report.hash import HashType, \
get_report_path_hash
from codechecker_report_converter.source_code_comment_handler import \
SourceCodeCommentHandler

from codechecker_client import client as libclient
from codechecker_client import product
from codechecker_common import arg, logger, cmd_config
from codechecker_common.checker_labels import CheckerLabels
from codechecker_common.util import load_json
Expand Down Expand Up @@ -419,7 +421,11 @@ def parse_analyzer_result_files(
return analyzer_result_file_reports


def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels):
def assemble_zip(inputs,
zip_file,
client,
prod_client,
checker_labels: CheckerLabels):
"""Collect and compress report and source files, together with files
contanining analysis related information into a zip file which
will be sent to the server.
Expand Down Expand Up @@ -463,6 +469,7 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels):
changed_files = set()
file_paths = set()
file_report_positions: FileReportPositions = defaultdict(set)
unique_reports = set()
for file_path, reports in analyzer_result_file_reports.items():
files_to_compress.add(file_path)
stats.num_of_analyzer_result_files += 1
Expand All @@ -471,7 +478,11 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels):
if report.changed_files:
changed_files.update(report.changed_files)
continue
stats.add_report(report)
# Need to calculate unique reoirt count to determine report limit
report_path_hash = get_report_path_hash(report)
if report_path_hash not in unique_reports:
unique_reports.add(report_path_hash)
stats.add_report(report)

file_paths.update(report.original_files)
file_report_positions[report.file.original_path].add(report.line)
Expand Down Expand Up @@ -580,6 +591,25 @@ def assemble_zip(inputs, zip_file, client, checker_labels: CheckerLabels):
# Print statistics what will be stored to the server.
stats.write()

# Fail store early if too many reports.
p = prod_client.getCurrentProduct()
if len(unique_reports) > p.reportLimit:
LOG.error(f"""Report Limit Exceeded
This report folder cannot be stored because the number of reports in the
result folder is too high. Usually noisy checkers, generating a lot of
reports are not useful and it is better to disable them.
Run `CodeChecker parse <report_folder>` to gain a better understanding on
report counts.
Disable checkers that have generated an excessive number of reports and then
rerun the analysis to be able to store the results on the server.
Configured report limit for this product: {p.reportLimit}
""")
sys.exit(1)

zip_size = os.stat(zip_file).st_size

LOG.info("Compressing report zip file...")
Expand Down Expand Up @@ -829,6 +859,12 @@ def main(args):

# Setup connection to the remote server.
client = libclient.setup_client(args.product_url)
protocol, host, port, product_name = \
product.split_product_url(args.product_url)
prod_client = libclient.setup_product_client(protocol,
host,
port,
product_name=product_name)

zip_file_handle, zip_file = tempfile.mkstemp('.zip')
LOG.debug("Will write mass store ZIP to '%s'...", zip_file)
Expand All @@ -838,7 +874,11 @@ def main(args):

LOG.debug("Assembling zip file.")
try:
assemble_zip(args.input, zip_file, client, context.checker_labels)
assemble_zip(args.input,
zip_file,
client,
prod_client,
context.checker_labels)
except Exception as ex:
print(ex)
import traceback
Expand Down
5 changes: 5 additions & 0 deletions web/client/codechecker_client/product_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,15 @@ def handle_add_product(args):
desc = convert.to_b64(args.description) \
if 'description' in args else None

report_limit = None
if hasattr(args, "report_limit") and args.report_limit:
report_limit = int(args.report_limit)

prod = ProductConfiguration(
endpoint=args.endpoint,
displayedName_b64=name,
description_b64=desc,
reportLimit=report_limit,
connection=dbc)

LOG.debug("Sending request to add product...")
Expand Down
48 changes: 29 additions & 19 deletions web/client/codechecker_client/thrift_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,26 +56,36 @@ def wrapper(self, *args, **kwargs):
except codechecker_api_shared.ttypes.RequestFailed as reqfailure:
LOG.error('Calling API endpoint: %s', funcName)
if reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.DATABASE:
LOG.error('Database error on server\n%s',
str(reqfailure.message))
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.AUTH_DENIED:
LOG.error('Authentication denied\n %s',
str(reqfailure.message))
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED:
LOG.error('Unauthorized to access\n %s',
str(reqfailure.message))
LOG.error('Ask the product admin for additional access '
'rights.')
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.API_MISMATCH:
LOG.error('Client/server API mismatch\n %s',
str(reqfailure.message))
codechecker_api_shared.ttypes.ErrorCode.GENERAL and \
reqfailure.extraInfo and \
reqfailure.extraInfo[0] == "report_limit":
# We handle this error in near the business logic.
raise reqfailure
else:
LOG.error('API call error: %s\n%s', funcName, str(reqfailure))
sys.exit(1)
if reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.DATABASE:
LOG.error('Database error on server\n%s',
str(reqfailure.message))
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.AUTH_DENIED:
LOG.error('Authentication denied\n %s',
str(reqfailure.message))
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED:
LOG.error('Unauthorized to access\n %s',
str(reqfailure.message))
LOG.error('Ask the product admin for additional access '
'rights.')
elif reqfailure.errorCode ==\
codechecker_api_shared.ttypes.ErrorCode.API_MISMATCH:
LOG.error('Client/server API mismatch\n %s',
str(reqfailure.message))
else:
LOG.error('API call error: %s\n%s',
funcName,
str(reqfailure)
)
sys.exit(1)
except TApplicationException as ex:
LOG.error("Internal server error: %s", str(ex.message))
sys.exit(1)
Expand Down
2 changes: 1 addition & 1 deletion web/codechecker_web/shared/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# The newest supported minor version (value) for each supported major version
# (key) in this particular build.
SUPPORTED_VERSIONS = {
6: 53
6: 54
}

# Used by the client to automatically identify the latest major and minor
Expand Down
39 changes: 39 additions & 0 deletions web/server/codechecker_server/api/mass_store_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,16 @@ def __init__(
self.__analysis_info: Dict[str, AnalysisInfo] = {}
self.__duration: int = 0
self.__report_count: int = 0
self.__report_limit: int = 0
self.__wrong_src_code_comments: List[str] = []
self.__already_added_report_hashes: Set[str] = set()
self.__severity_map: Dict[str, int] = {}
self.__new_report_hashes: Dict[str, Tuple] = {}
self.__all_report_checkers: Set[str] = set()
self.__added_reports: List[Tuple[DBReport, Report]] = list()

self.__get_report_limit_for_product()

@property
def __manager(self):
return self.__report_server._manager
Expand Down Expand Up @@ -1011,6 +1014,7 @@ def get_missing_file_ids(report: Report) -> List[str]:
else:
fixed_at = run_history_time

self.__check_report_count()
report_id = self.__add_report(
session, run_id, report, file_path_to_id,
rs_from_source, detection_status, detected_at,
Expand Down Expand Up @@ -1069,6 +1073,41 @@ def __validate_and_add_report_annotations(
f"'{ALLOWED_ANNOTATIONS[key]['display']}'."
)

def __get_report_limit_for_product(self):
with DBSession(self.__config_database) as session:
product = session.query(Product).get(self.__product.id)
if product.report_limit:
self.__report_limit = product.report_limit

def __check_report_count(self):
"""
This method comparest the already added report count to the report
limit, Raises exception if the number of reports is more than the
that is configured for the product.
"""
if len(self.__added_reports) >= self.__report_limit:
LOG.error("The number of reports in the given report folder is " +
"larger than the allowed." +
f"The limit: {self.__report_limit}!")
extra_info = [
"report_limit",
f"limit:{self.__report_limit}"
]
raise codechecker_api_shared.ttypes.RequestFailed(
codechecker_api_shared.ttypes.
ErrorCode.GENERAL,
"**Report Limit Exceeded** " +
"This report folder cannot be stored because the number of " +
"reports in the result folder is too high. Usually noisy " +
"checkers, generating a lot of reports are not useful and " +
"it is better to disable them. Run `CodeChecker parse " +
"<report_folder>` to gain a better understanding on report " +
"counts. Disable checkers that have generated an excessive " +
"number of reports and then rerun the analysis to be able " +
"to store the results on the server. " +
f"Limit: {self.__report_limit}",
extra_info)

def __store_reports(
self,
session: DBSession,
Expand Down
8 changes: 7 additions & 1 deletion web/server/codechecker_server/api/product_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def __get_product(self, session, product):
confidentiality = \
confidentiality_enum(product.confidentiality)

report_limit = product.report_limit

return server_product, ttypes.Product(
id=product.id,
endpoint=product.endpoint,
Expand All @@ -141,7 +143,8 @@ def __get_product(self, session, product):
administrating=self.__administrating(args),
databaseStatus=server_product.db_status,
admins=[admin.name for admin in admins],
confidentiality=confidentiality)
confidentiality=confidentiality,
reportLimit=report_limit)

@timeit
def getPackageVersion(self):
Expand Down Expand Up @@ -295,6 +298,7 @@ def getProductConfiguration(self, product_id):
description_b64=descr,
connection=dbc,
runLimit=product.run_limit,
reportLimit=product.report_limit,
isReviewStatusChangeDisabled=is_review_status_change_disabled,
confidentiality=confidentiality)

Expand Down Expand Up @@ -387,6 +391,7 @@ def addProduct(self, product):
name=displayed_name,
description=description,
run_limit=product.runLimit,
report_limit=product.reportLimit,
is_review_status_change_disabled=is_rws_change_disabled,
confidentiality=confidentiality)

Expand Down Expand Up @@ -583,6 +588,7 @@ def editProduct(self, product_id, new_config):
# Update the settings in the database.
product.endpoint = new_config.endpoint
product.run_limit = new_config.runLimit
product.report_limit = new_config.reportLimit
product.is_review_status_change_disabled = \
new_config.isReviewStatusChangeDisabled
product.connection = conn_str
Expand Down
5 changes: 4 additions & 1 deletion web/server/codechecker_server/database/config_db_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Product(Base):
display_name = Column(String, nullable=False)
description = Column(Text)
run_limit = Column(Integer)
report_limit = Column(Integer, nullable=False, server_default="500000")
num_of_runs = Column(Integer, server_default="0")
latest_storage_date = Column(DateTime, nullable=True)

Expand All @@ -49,13 +50,15 @@ class Product(Base):
confidentiality = Column(String, nullable=True)

def __init__(self, endpoint, conn_str, name=None, description=None,
run_limit=None, is_review_status_change_disabled=False,
run_limit=None, report_limit=500000,
is_review_status_change_disabled=False,
confidentiality=None):
self.endpoint = endpoint
self.connection = conn_str
self.display_name = name if name else endpoint
self.description = description
self.run_limit = run_limit
self.report_limit = report_limit
self.is_review_status_change_disabled = \
True if is_review_status_change_disabled else False
self.confidentiality = confidentiality
Expand Down
Loading

0 comments on commit fa41e4e

Please sign in to comment.