From a75244f4b6819c92b8b85bde0c04c6215b922914 Mon Sep 17 00:00:00 2001 From: Whisperity Date: Mon, 19 Aug 2024 19:11:13 +0200 Subject: [PATCH 1/4] feat(server): Asynchronous server-side background task execution This patch implements the whole support ecosystem for server-side background tasks, in order to help lessen the load (and blocking) of API handlers in the web-server for long-running operations. A **Task** is represented by two things in strict co-existence: a lightweight, `pickle`-able implementation in the server's code (a subclass of `AbstractTask`) and a corresponding `BackgroundTask` database entity, which resides in the "configuration" database (shared across all products). A Task is created by API request handlers and then the user is instructed to retain the `TaskToken`: the task's unique identifier. Following, the server will dispatch execution of the object into a background worker process, and keep status synchronisation via the database. Even in a service cluster deployment, load balancing will not interfere with users' ability to query a task's status. While normal users can only query the status of a single task (which is usually automatically done by client code, and not the user manually executing something); product administrators, and especially server administrators have the ability to query an arbitrary set of tasks using the potential filters, with a dedicated API function (`getTasks()`) for this purpose. Tasks can be cancelled only by `SUPERUSER`s, at which point a special binary flag is set in the status record. However, to prevent complicating inter-process communication, cancellation is supposed to be implemented by `AbstractTask` subclasses in a co-operative way. The execution of tasks in a process and a `Task`'s ability to "communicate" with its execution environment is achieved through the new `TaskManager` instance, which is created for every process of a server's deployment. Unfortunately, tasks can die gracelessly if the server is terminated (either internally, or even externally). For this reason, the `DROPPED` status will indicate that the server has terminated prior to, or during a task's execution, and it was unable to produce results. The server was refactored significantly around the handling of subprocesses in order to support various server shutdown scenarios. Servers will start `background_worker_processes` number of task handling subprocesses, which are distinct from the already existing "API handling" subprocesses. By default, if unconfigured, `background_worker_processes` is equal to `worker_processes` (the number of API processes to spawn), which is equal to `$(nproc)` (CPU count in the system). This patch includes a `TestingDummyTask` demonstrative subclass of `AbstractTask` which counts up to an input number of seconds, and each second it gracefully checks whether it is being killed. The corresponding testing API endpoint, `createDummyTask()` can specify whether the task should simulate a failing status. This endpoint can only be used from, but is used extensively, the unit testing of the project. This patch does not include "nice" or "ergonomic" facilities for admins to manage the tasks, and so far, only the server-side of the corresponding API calls are supported. --- .github/workflows/test.yml | 4 +- .../codechecker_analyzer/analysis_manager.py | 39 +- bin/CodeChecker | 49 +- .../compatibility/multiprocessing.py | 15 +- codechecker_common/logger.py | 54 +- codechecker_common/process.py | 49 ++ codechecker_common/util.py | 19 + docs/web/server_config.md | 10 +- docs/web/user_guide.md | 19 +- web/api/Makefile | 9 +- web/api/codechecker_api_shared.thrift | 31 +- .../dist/codechecker-api-6.58.0.tgz | Bin 64458 -> 0 bytes .../dist/codechecker-api-6.59.0.tgz | Bin 0 -> 69705 bytes web/api/js/codechecker-api-node/package.json | 2 +- .../dist/codechecker_api.tar.gz | Bin 60379 -> 62389 bytes web/api/py/codechecker_api/setup.py | 2 +- .../dist/codechecker_api_shared.tar.gz | Bin 7977 -> 3723 bytes web/api/py/codechecker_api_shared/setup.py | 2 +- web/api/report_server.thrift | 82 ++- web/api/tasks.thrift | 162 +++++ web/client/codechecker_client/client.py | 4 +- .../codechecker_client/helpers/results.py | 8 +- web/codechecker_web/shared/version.py | 2 +- .../codechecker_server/api/authentication.py | 4 +- web/server/codechecker_server/api/common.py | 49 ++ .../codechecker_server/api/report_server.py | 49 +- web/server/codechecker_server/api/tasks.py | 391 ++++++++++++ web/server/codechecker_server/cmd/server.py | 88 ++- .../database/config_db_model.py | 197 +++++- ...ented_keeping_track_of_background_tasks.py | 79 +++ web/server/codechecker_server/routing.py | 33 +- web/server/codechecker_server/server.py | 563 +++++++++++++++--- .../codechecker_server/session_manager.py | 37 +- .../task_executors/__init__.py | 7 + .../task_executors/abstract_task.py | 183 ++++++ .../codechecker_server/task_executors/main.py | 142 +++++ .../task_executors/task_manager.py | 229 +++++++ web/server/codechecker_server/tmp.py | 37 -- web/server/config/server_config.json | 2 + web/server/vue-cli/package-lock.json | 12 +- web/server/vue-cli/package.json | 2 +- .../instance_manager/test_instances.py | 3 +- web/tests/functional/tasks/__init__.py | 7 + .../functional/tasks/test_task_management.py | 494 +++++++++++++++ web/tests/libtest/env.py | 38 +- web/tests/libtest/thrift_client_to_db.py | 27 + 46 files changed, 2879 insertions(+), 356 deletions(-) create mode 100644 codechecker_common/process.py delete mode 100644 web/api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz create mode 100644 web/api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz create mode 100644 web/api/tasks.thrift create mode 100644 web/server/codechecker_server/api/common.py create mode 100644 web/server/codechecker_server/api/tasks.py create mode 100644 web/server/codechecker_server/migrations/config/versions/73b04c41885b_implemented_keeping_track_of_background_tasks.py create mode 100644 web/server/codechecker_server/task_executors/__init__.py create mode 100644 web/server/codechecker_server/task_executors/abstract_task.py create mode 100644 web/server/codechecker_server/task_executors/main.py create mode 100644 web/server/codechecker_server/task_executors/task_manager.py delete mode 100644 web/server/codechecker_server/tmp.py create mode 100644 web/tests/functional/tasks/__init__.py create mode 100644 web/tests/functional/tasks/test_task_management.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00e8ffca07..290d56bb5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,8 @@ jobs: - name: Install dependencies run: | pip install $(grep -iE "pylint|pycodestyle" analyzer/requirements_py/dev/requirements.txt) - - name: Run tests - run: make pylint pycodestyle + - name: Run pycodestyle & pylint + run: make -k pycodestyle pylint tools: name: Tools (report-converter, etc.) diff --git a/analyzer/codechecker_analyzer/analysis_manager.py b/analyzer/codechecker_analyzer/analysis_manager.py index 6b22ca4231..6f7e8a22dd 100644 --- a/analyzer/codechecker_analyzer/analysis_manager.py +++ b/analyzer/codechecker_analyzer/analysis_manager.py @@ -13,16 +13,15 @@ import shutil import signal import sys -import time import traceback import zipfile from threading import Timer import multiprocess -import psutil from codechecker_common.logger import get_logger +from codechecker_common.process import kill_process_tree from codechecker_common.review_status_handler import ReviewStatusHandler from codechecker_statistics_collector.collectors.special_return_value import \ @@ -341,42 +340,6 @@ def handle_failure( os.remove(plist_file) -def kill_process_tree(parent_pid, recursive=False): - """Stop the process tree try it gracefully first. - - Try to stop the parent and child processes gracefuly - first if they do not stop in time send a kill signal - to every member of the process tree. - - There is a similar function in the web part please - consider to update that in case of changing this. - """ - proc = psutil.Process(parent_pid) - children = proc.children(recursive) - - # Send a SIGTERM (Ctrl-C) to the main process - proc.terminate() - - # If children processes don't stop gracefully in time, - # slaughter them by force. - _, still_alive = psutil.wait_procs(children, timeout=5) - for p in still_alive: - p.kill() - - # Wait until this process is running. - n = 0 - timeout = 10 - while proc.is_running(): - if n > timeout: - LOG.warning("Waiting for process %s to stop has been timed out" - "(timeout = %s)! Process is still running!", - parent_pid, timeout) - break - - time.sleep(1) - n += 1 - - def setup_process_timeout(proc, timeout, failure_callback=None): """ diff --git a/bin/CodeChecker b/bin/CodeChecker index ad820b8a05..261e2312b2 100755 --- a/bin/CodeChecker +++ b/bin/CodeChecker @@ -6,10 +6,10 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # # ------------------------------------------------------------------------- - """ Used to kickstart CodeChecker. -Save original environment without modifications. + +Saves original environment without modifications. Used to run the logging in the same env. """ # This is for enabling CodeChecker as a filename (i.e. module name). @@ -25,9 +25,10 @@ import sys import tempfile PROC_PID = None +EXIT_CODE = None -def run_codechecker(checker_env, subcommand=None): +def run_codechecker(checker_env, subcommand=None) -> int: """ Run the CodeChecker. * checker_env - CodeChecker will be run in the checker env. @@ -63,11 +64,13 @@ def run_codechecker(checker_env, subcommand=None): global PROC_PID PROC_PID = proc.pid - proc.wait() - sys.exit(proc.returncode) + global EXIT_CODE + EXIT_CODE = proc.wait() + + return EXIT_CODE -def main(subcommand=None): +def main(subcommand=None) -> int: original_env = os.environ.copy() checker_env = original_env @@ -94,30 +97,32 @@ def main(subcommand=None): print('Saving original build environment failed.') print(ex) - def signal_term_handler(signum, _frame): + def signal_handler(signum, _frame): + """ + Forwards the received signal to the CodeChecker subprocess started by + this `main` script. + """ global PROC_PID if PROC_PID and sys.platform != "win32": - os.kill(PROC_PID, signal.SIGINT) - - _remove_tmp() - sys.exit(128 + signum) - - signal.signal(signal.SIGTERM, signal_term_handler) - signal.signal(signal.SIGINT, signal_term_handler) - - def signal_reload_handler(_sig, _frame): - global PROC_PID - if PROC_PID: - os.kill(PROC_PID, signal.SIGHUP) + try: + os.kill(PROC_PID, signum) + except ProcessLookupError: + pass + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) if sys.platform != "win32": - signal.signal(signal.SIGHUP, signal_reload_handler) + signal.signal(signal.SIGHUP, signal_handler) + signal.signal(signal.SIGCHLD, signal_handler) try: - run_codechecker(checker_env, subcommand) + global EXIT_CODE + EXIT_CODE = run_codechecker(checker_env, subcommand) finally: _remove_tmp() + return EXIT_CODE + if __name__ == "__main__": - main(None) + sys.exit(main(None) or 0) diff --git a/codechecker_common/compatibility/multiprocessing.py b/codechecker_common/compatibility/multiprocessing.py index 14ef7ebebe..eaee9a78e7 100644 --- a/codechecker_common/compatibility/multiprocessing.py +++ b/codechecker_common/compatibility/multiprocessing.py @@ -13,8 +13,15 @@ # pylint: disable=no-name-in-module # pylint: disable=unused-import if sys.platform in ["darwin", "win32"]: - from multiprocess import Pool # type: ignore - from multiprocess import cpu_count + from multiprocess import \ + Pool, Process, \ + Queue, \ + Value, \ + cpu_count else: - from concurrent.futures import ProcessPoolExecutor as Pool # type: ignore - from multiprocessing import cpu_count + from concurrent.futures import ProcessPoolExecutor as Pool + from multiprocessing import \ + Process, \ + Queue, \ + Value, \ + cpu_count diff --git a/codechecker_common/logger.py b/codechecker_common/logger.py index 8c860dee6e..35702fb0b8 100644 --- a/codechecker_common/logger.py +++ b/codechecker_common/logger.py @@ -6,16 +6,18 @@ # # ------------------------------------------------------------------------- - import argparse +import datetime import json import logging from logging import config from pathlib import Path import os +import sys +from typing import Optional -# The logging leaves can be accesses without -# importing the logging module in other modules. +# The logging leaves can be accesses without importing the logging module in +# other modules. DEBUG = logging.DEBUG INFO = logging.INFO WARNING = logging.WARNING @@ -25,14 +27,24 @@ CMDLINE_LOG_LEVELS = ['info', 'debug_analyzer', 'debug'] -DEBUG_ANALYZER = logging.DEBUG_ANALYZER = 15 # type: ignore +DEBUG_ANALYZER = 15 logging.addLevelName(DEBUG_ANALYZER, 'DEBUG_ANALYZER') +_Levels = {"DEBUG": DEBUG, + "DEBUG_ANALYZER": DEBUG_ANALYZER, + "INFO": INFO, + "WARNING": WARNING, + "ERROR": ERROR, + "CRITICAL": CRITICAL, + "NOTSET": NOTSET, + } + + class CCLogger(logging.Logger): def debug_analyzer(self, msg, *args, **kwargs): - if self.isEnabledFor(logging.DEBUG_ANALYZER): - self._log(logging.DEBUG_ANALYZER, msg, args, **kwargs) + if self.isEnabledFor(DEBUG_ANALYZER): + self._log(DEBUG_ANALYZER, msg, args, **kwargs) logging.setLoggerClass(CCLogger) @@ -113,6 +125,36 @@ def validate_loglvl(log_level): return log_level +def raw_sprint_log(logger: logging.Logger, level: str, message: str) \ + -> Optional[str]: + """ + Formats a raw log `message` using the date format of the specified + `logger`, without actually invoking the logging infrastructure. + """ + if not logger.isEnabledFor(_Levels[level]): + return None + + formatter = logger.handlers[0].formatter if len(logger.handlers) > 0 \ + else None + datefmt = formatter.datefmt if formatter else None + time = datetime.datetime.now().strftime(datefmt) if datefmt \ + else str(datetime.datetime.now()) + + return f"[{validate_loglvl(level)} {time}] - {message}" + + +def signal_log(logger: logging.Logger, level: str, message: str): + """ + Simulates a log output and logs a message within a signal handler, without + triggering a `RuntimeError` due to reentrancy in `print`-like method calls. + """ + formatted = raw_sprint_log(logger, level, message) + if not formatted: + return + + os.write(sys.stderr.fileno(), f"{formatted}\n".encode()) + + class LogCfgServer: """ Initialize a log configuration server for dynamic log configuration. diff --git a/codechecker_common/process.py b/codechecker_common/process.py new file mode 100644 index 0000000000..86da476d2a --- /dev/null +++ b/codechecker_common/process.py @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +import time + +import psutil + +from .logger import get_logger + + +LOG = get_logger("system") + + +def kill_process_tree(parent_pid, recursive=False): + """ + Stop the process tree, gracefully at first. + + Try to stop the parent and child processes gracefuly first. + If they do not stop in time, send a kill signal to every member of the + process tree. + """ + proc = psutil.Process(parent_pid) + children = proc.children(recursive) + + # Send a SIGTERM to the main process. + proc.terminate() + + # If children processes don't stop gracefully in time, slaughter them + # by force. + _, still_alive = psutil.wait_procs(children, timeout=5) + for p in still_alive: + p.kill() + + # Wait until this process is running. + n = 0 + timeout = 10 + while proc.is_running(): + if n > timeout: + LOG.warning("Waiting for process %s to stop has been timed out" + "(timeout = %s)! Process is still running!", + parent_pid, timeout) + break + + time.sleep(1) + n += 1 diff --git a/codechecker_common/util.py b/codechecker_common/util.py index e389b8d1a0..c9075d5599 100644 --- a/codechecker_common/util.py +++ b/codechecker_common/util.py @@ -8,9 +8,12 @@ """ Util module. """ +import datetime +import hashlib import itertools import json import os +import random from typing import TextIO import portalocker @@ -112,3 +115,19 @@ def path_for_fake_root(full_path: str, root_path: str = '/') -> str: def strtobool(value: str) -> bool: """Parse a string value to a boolean.""" return value.lower() in ('y', 'yes', 't', 'true', 'on', '1') + + +def generate_random_token(num_bytes: int = 32) -> str: + """ + Returns a random-generated string usable as a token with `num_bytes` + hexadecimal characters in the output. + """ + prefix = str(os.getpid()).encode() + suffix = str(datetime.datetime.now()).encode() + + hash_value = ''.join( + [hashlib.sha256(prefix + os.urandom(num_bytes * 2) + suffix) + .hexdigest() + for _ in range(0, -(num_bytes // -64))]) + idx = random.randrange(0, len(hash_value) - num_bytes + 1) + return hash_value[idx:(idx + num_bytes)] diff --git a/docs/web/server_config.md b/docs/web/server_config.md index add9bddcb7..7eb0e4f468 100644 --- a/docs/web/server_config.md +++ b/docs/web/server_config.md @@ -17,7 +17,7 @@ Table of Contents * [Size of the compilation database](#size-of-the-compilation-database) * [Authentication](#authentication) -## Number of worker processes +## Number of API worker processes The `worker_processes` section of the config file controls how many processes will be started on the server to process API requests. @@ -25,6 +25,14 @@ will be started on the server to process API requests. The server needs to be restarted if the value is changed in the config file. +### Number of task worker processes +The `background_worker_processes` section of the config file controls how many +processes will be started on the server to process background jobs. + +*Default value*: Fallback to same amount as `worker_processes`. + +The server needs to be restarted if the value is changed in the config file. + ## Run limitation The `max_run_count` section of the config file controls how many runs can be stored on the server for a product. diff --git a/docs/web/user_guide.md b/docs/web/user_guide.md index 846599b76a..bca75ee459 100644 --- a/docs/web/user_guide.md +++ b/docs/web/user_guide.md @@ -145,8 +145,9 @@ or via the `CodeChecker cmd` command-line client. ``` usage: CodeChecker server [-h] [-w WORKSPACE] [-f CONFIG_DIRECTORY] - [--host LISTEN_ADDRESS] [-v PORT] [--not-host-only] - [--skip-db-cleanup] [--config CONFIG_FILE] + [--machine-id MACHINE_ID] [--host LISTEN_ADDRESS] + [-v PORT] [--not-host-only] [--skip-db-cleanup] + [--config CONFIG_FILE] [--sqlite SQLITE_FILE | --postgresql] [--dbaddress DBADDRESS] [--dbport DBPORT] [--dbusername DBUSERNAME] [--dbname DBNAME] @@ -172,6 +173,20 @@ optional arguments: specific configuration (such as authentication settings, and TLS/SSL certificates) from. (default: /home//.codechecker) + --machine-id MACHINE_ID + A unique identifier to be used to identify the machine + running subsequent instances of the "same" server + process. This value is only used internally to + maintain normal function and bookkeeping of executed + tasks following an unclean server shutdown, e.g., + after a crash or system-level interference. If + unspecified, defaults to a reasonable default value + that is generated from the computer's hostname, as + reported by the operating system. In most scenarios, + there is no need to fine-tune this, except if + subsequent executions of the "same" server is achieved + in distinct environments, e.g., if the server + otherwise is running in a container. --host LISTEN_ADDRESS The IP address or hostname of the server on which it should listen for connections. For IPv6 listening, diff --git a/web/api/Makefile b/web/api/Makefile index e145755563..d322ab77b0 100644 --- a/web/api/Makefile +++ b/web/api/Makefile @@ -37,10 +37,11 @@ build: clean target_dirs thrift:$(THRIFT_VERSION) \ /bin/bash -c " \ thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/authentication.thrift && \ - thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/products.thrift && \ - thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/report_server.thrift && \ - thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/configuration.thrift && \ - thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/server_info.thrift" + thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/configuration.thrift && \ + thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/products.thrift && \ + thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/report_server.thrift && \ + thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/server_info.thrift && \ + thrift $(THRIFT_OPTS) $(TARGET_PY) $(TARGET_JS) /data/tasks.thrift" # Create tarball from the API JavaScript part which will be commited in the # repository and installed as a dependency. diff --git a/web/api/codechecker_api_shared.thrift b/web/api/codechecker_api_shared.thrift index 167f8ab40b..3e01ea5fbd 100644 --- a/web/api/codechecker_api_shared.thrift +++ b/web/api/codechecker_api_shared.thrift @@ -4,15 +4,24 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // ------------------------------------------------------------------------- +/** + * Helper enum for expressing a three-way boolean in a filter. + */ +enum Ternary { + BOTH = 0, // Indicates a query where both set and unset booleans are matched. + OFF = 1, // Indicates a query where the filter matches an UNSET boolean. + ON = 2, // Indicates a query where the filter matches a SET boolean. +} + enum ErrorCode { DATABASE, IOERROR, GENERAL, - AUTH_DENIED, // Authentication denied. We do not allow access to the service. - UNAUTHORIZED, // Authorization denied. User does not have right to perform an action. - API_MISMATCH, // The client attempted to query an API version that is not supported by the server. - SOURCE_FILE, // The client sent a source code which contains errors (e.g.: source code comment errors). - REPORT_FORMAT, // The client sent a report with wrong format (e.g. report annotation has bad type in a .plist) + AUTH_DENIED, // Authentication denied. We do not allow access to the service. + UNAUTHORIZED, // Authorization denied. User does not have right to perform an action. + API_MISMATCH, // The client attempted to query an API version that is not supported by the server. + SOURCE_FILE, // The client sent a source code which contains errors (e.g., source code comment errors). + REPORT_FORMAT, // The client sent a report with wrong format (e.g., report annotation has bad type in a .plist). } exception RequestFailed { @@ -30,7 +39,7 @@ exception RequestFailed { * PRODUCT: These permissions are configured per-product. * The extra data field looks like the following object: * { i64 productID } -*/ + */ enum Permission { SUPERUSER = 1, // scope: SYSTEM PERMISSION_VIEW = 2, // scope: SYSTEM @@ -42,8 +51,8 @@ enum Permission { } /** -* Status information about the database backend. -*/ + * Status information about the database backend. + */ enum DBStatus { OK, // Everything is ok with the database. MISSING, // The database is missing. @@ -54,3 +63,9 @@ enum DBStatus { SCHEMA_INIT_ERROR, // Failed to create initial database schema. SCHEMA_UPGRADE_FAILED // Failed to upgrade schema. } + +/** + * Common token type identifying a background task. + * (Main implementation for task management API is in tasks.thrift.) + */ +typedef string TaskToken; diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz deleted file mode 100644 index a8cf8ab10bd14f2623023e3167d40998ccc5f5d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64458 zcmV)%K#jj2iwFP!00002|LnbMbK6LgD7v5hE4YrhYbi#fCh>;d9XHgrJgu{~?6K^b zGuPp8&>|#rOp#iGvRgCT|NYkc0c7DtfC9zka~K&>lv|gBQuwbUt07>7~by*nA$dH9a1r zU&piA;3gT*2P?GJRkFNE2jlrC|2`O}g9#oonGC+%4=$2R+Q|s@zg*m1e;vew#TEL;6t!Di-rXegRd!yBWo}}Z zi`)C<^!n>+u=p`gmIE{;1UOyY55{+^uZ!jMUt~Pi4+OFLI$jOXpx4VW0-RryMrjQuI z{~VIx7HE6u#`BKuR)~C|`JyvG$FxjR-XrSrOEO2Ba5+Ussr9bRQy%$07I%*a2M8Ab zwS0W2o;vz}cu&4hCwI87vfs?OSWXHeZA#N6vDO%c?+$w&+)A{A>j%)?GgoF8FHJD9rrg(I8 zj_KlR^#h| z{P;S({HhQGbXY6I1JKP}Cf}#zC@~hJtzq|Mkf0r2Ecw4s7wnkT+r&Dl3)tn z7qX0-WSPwF(LI=dBOCVxVGG8S`S>O|`0)@VtCV!+{iWR2%3pDhM zlkN2G3!)v2cyPIcU>BJ=nI5|eWFSJvb7BQQWy0N$%A&NMiVc;1ODN%0<%MzkcdY`V6n^Yy?i09rVn1x&t z9?4qJ-!SQ0O6iD;TuC~;o)g-SP!XRI*;o)0F|m4lj`n*zQ19yT zNxTerpf=@A^e%)VJ{bG zzbU)M0a{Fq{4W4@UGnH+tJmW==TXC(G4ud(K|xF63}_kX@x zCg@*NB-wpmTw+R|W9Li-!~=453qhbuc8d`?N9rU!zrFi{V9_=rOue0r5vGe@(3~l+ zP024zvQQN_Rn8a0m`KaH5dnTqhzmO*S2>}#V@!5;;CK!YSaQ3h^8!-D|#}55-?Gj$%TZ_ z@Ri{$pohdq9pB#0u)?{Rqf;lljHwr!+U0CKMf*+LDvLwANIFq>S5VG5y1!{UUQWsV zyjmjq!*!Zu%DJC<^$*fRqzEtO3FC=~03w-0$n^v)3&0mdbzH`8(e$a9r%#Rz@dF

u9u56{WIAxyo}4m9hRWz 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%0D(vvt0cCTcadc@#JkI;iZYXdyr0D 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

W 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 diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..270aaf28fce6581ebdd7167ab89aa50f240f88ca GIT binary patch literal 69705 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 z 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
    G9cy$&Ck`jaukGFr4{tB0=L^i8#o;6V2hp>m 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
    hI_yS)Qt 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 literal 0 HcmV?d00001 diff --git a/web/api/js/codechecker-api-node/package.json b/web/api/js/codechecker-api-node/package.json index 0bfd792add..86e4a596e9 100644 --- a/web/api/js/codechecker-api-node/package.json +++ b/web/api/js/codechecker-api-node/package.json @@ -1,6 +1,6 @@ { "name": "codechecker-api", - "version": "6.58.0", + "version": "6.59.0", "description": "Generated node.js compatible API stubs for CodeChecker server.", "main": "lib", "homepage": "https://github.com/Ericsson/codechecker", diff --git a/web/api/py/codechecker_api/dist/codechecker_api.tar.gz b/web/api/py/codechecker_api/dist/codechecker_api.tar.gz index 3875d3ef7f4af5433227592e105f35a32e274690..601a9c2e5c4c80328f68f760cd44d8542282b458 100644 GIT binary patch literal 62389 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

    z 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%0DHYssHpRGYe>wt7{33=O{ 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 literal 7977 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 diff --git a/web/api/py/codechecker_api_shared/setup.py b/web/api/py/codechecker_api_shared/setup.py index a4c2e70d02..90f09bf34e 100644 --- a/web/api/py/codechecker_api_shared/setup.py +++ b/web/api/py/codechecker_api_shared/setup.py @@ -8,7 +8,7 @@ with open('README.md', encoding='utf-8', errors="ignore") as f: long_description = f.read() -api_version = '6.58.0' +api_version = '6.59.0' setup( name='codechecker_api_shared', diff --git a/web/api/report_server.thrift b/web/api/report_server.thrift index 359372e28a..962a1c49d8 100644 --- a/web/api/report_server.thrift +++ b/web/api/report_server.thrift @@ -201,6 +201,17 @@ struct RunData { } typedef list RunDataList +struct SubmittedRunOptions { + 1: string runName, + 2: string tag, + 3: string version, // The version of CodeChecker with + // which the analysis was done. + 4: bool force, // If set, existing results in + // the run are removed first. + 5: list trimPathPrefixes, + 6: optional string description, +} + struct RunHistoryData { 1: i64 runId, // Unique id of the run. 2: string runName, // Name of the run. @@ -208,8 +219,7 @@ struct RunHistoryData { 4: string user, // User name who analysed the run. 5: string time, // Date time when the run was analysed. 6: i64 id, // Id of the run history tag. - // !!!DEPRECATED!!! This field will be empty so use the getCheckCommand() API function to get the check command for a run. - 7: string checkCommand, + 7: string checkCommand, // Check command. !!!DEPRECATED!!! This field will be empty so use the getCheckCommand API function to get the check command for a run. 8: string codeCheckerVersion, // CodeChecker client version of the latest analysis. 9: AnalyzerStatisticsData analyzerStatistics, // Statistics for analyzers. Only number of failed and successfully analyzed // files field will be set. To get full analyzer statistics please use the @@ -943,32 +953,47 @@ service codeCheckerDBAccess { //============================================ // The client can ask the server whether a file is already stored in the - // database. If it is, then it is not necessary to send it in the ZIP file - // with massStoreRun() function. This function requires a list of file hashes - // (sha256) and returns the ones which are not stored yet. + // database. + // If it is present, then it is not necessary to send the file in the ZIP + // to the massStoreRunAsynchronous() function. + // This function requires a list of file hashes (sha256) and returns the + // ones which are not stored yet. + // // PERMISSION: PRODUCT_STORE list getMissingContentHashes(1: list fileHashes) throws (1: codechecker_api_shared.RequestFailed requestError), // The client can ask the server whether a blame info is already stored in the - // database. If it is, then it is not necessary to send it in the ZIP file - // with massStoreRun() function. This function requires a list of file hashes - // (sha256) and returns the ones to which no blame info is stored yet. + // database. + // If it is, then it is not necessary to send the info in the ZIP file + // to the massStoreRunAsynchronous() function. + // This function requires a list of file hashes (sha256) and returns the + // ones to which no blame info is stored yet. + // // PERMISSION: PRODUCT_STORE list getMissingContentHashesForBlameInfo(1: list fileHashes) throws (1: codechecker_api_shared.RequestFailed requestError), // This function stores an entire run encapsulated and sent in a ZIP file. - // The ZIP file has to be compressed and sent as a base64 encoded string. The - // ZIP file must contain a "reports" and an optional "root" sub-folder. - // The former one is the output of 'CodeChecker analyze' command and the - // latter one contains the source files on absolute paths starting as if - // "root" was the "/" directory. The source files are not necessary to be - // wrapped in the ZIP file (see getMissingContentHashes() function). + // The ZIP file has to be compressed by ZLib and the compressed buffer + // sent as a Base64-encoded string. The ZIP file must contain a "reports" and + // an optional "root" sub-directory. The former one is the output of the + // 'CodeChecker analyze' command and the latter one contains the source files + // on absolute paths starting as if "root" was the "/" directory. The source + // files are not necessary to be wrapped in the ZIP file + // (see getMissingContentHashes() function). // // The "version" parameter is the used CodeChecker version which checked this // run. // The "force" parameter removes existing analysis results for a run. + // + // !DEPRECATED!: Use of this function is deprecated as the storing client + // process is prone to infinite hangs while waiting for the return value of + // the Thrift call if the network communication terminates during the time + // the server is processing the sent data, which might take a very long time. + // Appropriately modern clients are expected to use the + // massStoreRunAsynchronous() function and the Task API instead! + // // PERMISSION: PRODUCT_STORE i64 massStoreRun(1: string runName, 2: string tag, @@ -979,6 +1004,35 @@ service codeCheckerDBAccess { 7: optional string description) throws (1: codechecker_api_shared.RequestFailed requestError), + // This function stores an entire analysis run encapsulated and sent as a + // ZIP file. The ZIP file must be compressed by ZLib and sent as a + // Base64-encoded string. It must contain a "reports" and an optional "root" + // sub-directory. "reports" contains the output of the `CodeChecker analyze` + // command, while "root", if present, contains the source code of the project + // with their full paths, with the logical "root" replacing the original + // "/" directory. + // + // The source files are not necessary to be present in the ZIP, see + // getMissingContentHashes() for details. + // + // After performing an initial validation of the well-formedness of the + // submitted structure (ill-formedness is reported as an exception), the + // potentially lengthy processing of the data and the database operations are + // done asynchronously. + // + // This function returns a TaskToken, which SHOULD be used as the argument to + // the tasks::getTaskInfo() function such that clients retrieve the + // processing's state. Clients MAY decide to "detach", i.e., not to wait + // for the processing of the submitted data, and ignore the returned handle. + // Even if the client detached, the processing of the stored reports will + // likely eventually conclude. + // + // PERMISSION: PRODUCT_STORE + codechecker_api_shared.TaskToken massStoreRunAsynchronous( + 1: string zipfileBlob, // Base64-encoded string. + 2: SubmittedRunOptions storeOpts) + throws (1: codechecker_api_shared.RequestFailed requestError), + // Returns true if analysis statistics information can be sent to the server, // otherwise it returns false. // PERMISSION: PRODUCT_STORE diff --git a/web/api/tasks.thrift b/web/api/tasks.thrift new file mode 100644 index 0000000000..e380d59ac3 --- /dev/null +++ b/web/api/tasks.thrift @@ -0,0 +1,162 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +include "codechecker_api_shared.thrift" + +namespace py codeCheckerServersideTasks_v6 +namespace js codeCheckerServersideTasks_v6 + +enum TaskStatus { + ALLOCATED, // Non-terminated state. Token registered but the job hasn't queued yet: the input is still processing. + ENQUEUED, // Non-terminated state. Job in the queue, and all inputs are meaningfully available. + RUNNING, // Non-terminated state. + COMPLETED, // Terminated state. Successfully ran to completion. + FAILED, // Terminated state. Job was running, but the execution failed. + CANCELLED, // Terminated state. Job was cancelled by an administrator, and the cancellation succeeded. + DROPPED, // Terminated state. Job was cancelled due to system reasons (server shutdown, crash, other interference). +} + +struct TaskInfo { + 1: codechecker_api_shared.TaskToken token, + 2: string taskKind, + 3: TaskStatus status, + // If the task is associated with a product, this ID can be used to query + // product information, see products.thirft service. + // The 'productID' is set to 0 if there is no product associated, meaning + // that the task is "global to the server". + 4: i64 productId, + 5: string actorUsername, + 6: string summary, + // Additional, human-readable comments, history, and log output from the + // tasks's processing. + 7: string comments, + 8: i64 enqueuedAtEpoch, + 9: i64 startedAtEpoch, + 10: i64 completedAtEpoch, + 11: i64 lastHeartbeatEpoch, + // Whether the administrator set this job for a co-operative cancellation. + 12: bool cancelFlagSet, +} + +/** + * TaskInfo with additional fields that is sent to administrators only. + */ +struct AdministratorTaskInfo { + 1: TaskInfo normalInfo, + 2: string machineId, // The hopefully unique identifier of the server + // that is/was processing the task. + 3: bool statusConsumed, // Whether the main actor of the task + // (see normalInfo.actorUsername) consumed the + // termination status of the job. +} + +/** + * Metastructure that holds the filters for getTasks(). + * The individual fields of the struct are in "AND" relation with each other. + * For list<> fields, elements of the list filter the same "column" of the + * task information table, and are considered in an "OR" relation. + */ +struct TaskFilter { + 1: list tokens, + 2: list machineIDs, + 3: list kinds, + 4: list statuses, + // If empty, it means "all", including those of no username. + 5: list usernames, + // If True, it means filter for **only** "no username". + // Can not be set together with a non-empty "usernames". + 6: bool filterForNoUsername, + // If empty, it means "all", including those of no product ID. + 7: list productIDs, + // If True, it means filter for **only** "no product ID". + // Can not be set together with a non-empty "productIDs". + 8: bool filterForNoProductID, + 9: i64 enqueuedBeforeEpoch, + 10: i64 enqueuedAfterEpoch, + 11: i64 startedBeforeEpoch, + 12: i64 startedAfterEpoch, + 13: i64 completedBeforeEpoch, + 14: i64 completedAfterEpoch, + 15: i64 heartbeatBeforeEpoch, + 16: i64 heartbeatAfterEpoch, + 17: codechecker_api_shared.Ternary cancelFlag, + 18: codechecker_api_shared.Ternary consumedFlag, +} + +service codeCheckerServersideTaskService { + // Retrieves the status of a task registered on the server, based on its + // identifying "token". + // + // Following this query, if the task is in any terminating states and the + // query was requested by the main actor, the status will be considered + // "consumed", and might be garbage collected by the server at a later + // point in time. + // + // If the server has authentication enabled, this query is only allowed to + // the following users: + // * The user who originally submitted the request that resulted in the + // creation of this job. + // * If the job is associated with a specific product, anyone with + // PRODUCT_ADMIN privileges for that product. + // * Users with SUPERUSER rights. + // + // PERMISSION: . + TaskInfo getTaskInfo( + 1: codechecker_api_shared.TaskToken token) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Returns privileged information about the tasks stored in the servers' + // databases, based on the given filter. + // + // This query does not set the "consumed" flag on the results, even if the + // querying user was a task's main actor. + // + // If the querying user only has PRODUCT_ADMIN rights, they are only allowed + // to query the tasks corresponding to a product they are PRODUCT_ADMIN of. + // + // PERMISSION: SUPERUSER, PRODUCT_ADMIN + list getTasks( + 1: TaskFilter filters) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Sets the specified task's "cancel" flag to TRUE, resulting in a request to + // the task's execution to co-operatively terminate itself. + // Returns whether the current RPC call was the one which set the flag. + // + // Tasks will generally terminate themselves at a safe point during their + // processing, but there are no guarantees that a specific task at any given + // point can reach such a safe point. + // There are no guarantees that a specific task is implemented in a way that + // it can ever be terminated via a "cancel" action. + // + // This method does not result in a communication via operating system + // primitives to the running server, and it is not capable of either + // completely shutting down a running server, or, conversely, to resurrect a + // hung server. + // + // Setting the "cancel" flag of an already cancelled task does nothing, and + // it is not possible to un-cancel a task. + // Setting the "cancel" flag of already terminated tasks does nothing. + // In both such cases, the RPC call will return "bool False". + // + // PERMISSION: SUPERUSER + bool cancelTask( + 1: codechecker_api_shared.TaskToken token) + throws (1: codechecker_api_shared.RequestFailed requestError), + + // Used for testing purposes only. + // This function will **ALWAYS** throw an exception when ran outside of a + // testing environment. + // + // The dummy task will increment a temporary counter in the background, with + // intermittent sleeping, up to approximately "timeout" number of seconds, + // after which point it will gracefully terminate. + // The result of the execution is unsuccessful if "shouldFail" is a true. + codechecker_api_shared.TaskToken createDummyTask( + 1: i32 timeout, + 2: bool shouldFail) + throws (1: codechecker_api_shared.RequestFailed requestError), +} diff --git a/web/client/codechecker_client/client.py b/web/client/codechecker_client/client.py index 730a83446b..0bbe2f69fe 100644 --- a/web/client/codechecker_client/client.py +++ b/web/client/codechecker_client/client.py @@ -205,7 +205,7 @@ def setup_product_client(protocol, host, port, auth_client=None, # Attach to the server-wide product service. product_client = ThriftProductHelper( protocol, host, port, - '/v' + CLIENT_API + '/Products', + f"/v{CLIENT_API}/Products", session_token, lambda: get_new_token(protocol, host, port, cred_manager)) else: @@ -260,6 +260,6 @@ def setup_client(product_url) -> ThriftResultsHelper: return ThriftResultsHelper( protocol, host, port, - '/' + product_name + '/v' + CLIENT_API + '/CodeCheckerService', + f"/{product_name}/v{CLIENT_API}/CodeCheckerService", session_token, lambda: get_new_token(protocol, host, port, cred_manager)) diff --git a/web/client/codechecker_client/helpers/results.py b/web/client/codechecker_client/helpers/results.py index c558cfe040..399623019e 100644 --- a/web/client/codechecker_client/helpers/results.py +++ b/web/client/codechecker_client/helpers/results.py @@ -9,7 +9,7 @@ Helper functions for Thrift api calls. """ -from codechecker_api.codeCheckerDBAccess_v6 import codeCheckerDBAccess +from codechecker_api.codeCheckerDBAccess_v6 import codeCheckerDBAccess, ttypes from codechecker_client.thrift_call import thrift_client_call from .base import BaseClientHelper @@ -181,6 +181,12 @@ def massStoreRun(self, name, tag, version, zipdir, force, trim_path_prefixes, description): pass + @thrift_client_call + def massStoreRunAsynchronous(self, zipfile_blob: str, + store_opts: ttypes.SubmittedRunOptions) \ + -> str: + pass + @thrift_client_call def allowsStoringAnalysisStatistics(self): pass diff --git a/web/codechecker_web/shared/version.py b/web/codechecker_web/shared/version.py index e5d544a750..2ac2d84ae7 100644 --- a/web/codechecker_web/shared/version.py +++ b/web/codechecker_web/shared/version.py @@ -18,7 +18,7 @@ # The newest supported minor version (value) for each supported major version # (key) in this particular build. SUPPORTED_VERSIONS = { - 6: 58 + 6: 59 } # Used by the client to automatically identify the latest major and minor diff --git a/web/server/codechecker_server/api/authentication.py b/web/server/codechecker_server/api/authentication.py index 1430ad9fd6..9e73923e45 100644 --- a/web/server/codechecker_server/api/authentication.py +++ b/web/server/codechecker_server/api/authentication.py @@ -19,6 +19,7 @@ AuthorisationList, HandshakeInformation, Permissions, SessionTokenData from codechecker_common.logger import get_logger +from codechecker_common.util import generate_random_token from codechecker_server.profiler import timeit @@ -28,7 +29,6 @@ from ..permissions import handler_from_scope_params as make_handler, \ require_manager, require_permission from ..server import permissions -from ..session_manager import generate_session_token LOG = get_logger('server') @@ -363,7 +363,7 @@ def newToken(self, description): """ self.__require_privilaged_access() with DBSession(self.__config_db) as session: - token = generate_session_token() + token = generate_random_token(32) user = self.getLoggedInUser() groups = ';'.join(self.__auth_session.groups) session_token = Session(token, user, groups, description, False) diff --git a/web/server/codechecker_server/api/common.py b/web/server/codechecker_server/api/common.py new file mode 100644 index 0000000000..2fc699a24f --- /dev/null +++ b/web/server/codechecker_server/api/common.py @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +import sqlalchemy + +from codechecker_api_shared.ttypes import RequestFailed, ErrorCode + +from codechecker_common.logger import get_logger + + +LOG = get_logger("server") + + +def exc_to_thrift_reqfail(function): + """ + Convert internal exceptions to a `RequestFailed` Thrift exception, which + can be sent back to the RPC client. + """ + func_name = function.__name__ + + def wrapper(*args, **kwargs): + try: + res = function(*args, **kwargs) + return res + except sqlalchemy.exc.SQLAlchemyError as alchemy_ex: + # Convert SQLAlchemy exceptions. + msg = str(alchemy_ex) + import traceback + traceback.print_exc() + + # pylint: disable=raise-missing-from + raise RequestFailed(ErrorCode.DATABASE, msg) + except RequestFailed as rf: + LOG.warning("%s:\n%s", func_name, rf.message) + raise + except Exception as ex: + import traceback + traceback.print_exc() + msg = str(ex) + LOG.warning("%s:\n%s", func_name, msg) + + # pylint: disable=raise-missing-from + raise RequestFailed(ErrorCode.GENERAL, msg) + + return wrapper diff --git a/web/server/codechecker_server/api/report_server.py b/web/server/codechecker_server/api/report_server.py index c98cbc71c0..2348708b81 100644 --- a/web/server/codechecker_server/api/report_server.py +++ b/web/server/codechecker_server/api/report_server.py @@ -44,7 +44,8 @@ ReviewStatusRuleSortType, RunData, RunFilter, RunHistoryData, \ RunReportCount, RunSortType, RunTagCount, \ ReviewStatus as API_ReviewStatus, \ - SourceComponentData, SourceFileData, SortMode, SortType + SourceComponentData, SourceFileData, SortMode, SortType, \ + SubmittedRunOptions from codechecker_common import util from codechecker_common.logger import get_logger @@ -69,6 +70,7 @@ Run, RunHistory, RunHistoryAnalysisInfo, RunLock, \ SourceComponent +from .common import exc_to_thrift_reqfail from .thrift_enum_helper import detection_status_enum, \ detection_status_str, report_status_enum, \ review_status_enum, review_status_str, report_extended_data_type_enum @@ -141,39 +143,6 @@ def slugify(text): return norm_text -def exc_to_thrift_reqfail(function): - """ - Convert internal exceptions to RequestFailed exception - which can be sent back on the thrift connections. - """ - func_name = function.__name__ - - def wrapper(*args, **kwargs): - try: - res = function(*args, **kwargs) - return res - - except sqlalchemy.exc.SQLAlchemyError as alchemy_ex: - # Convert SQLAlchemy exceptions. - msg = str(alchemy_ex) - import traceback - traceback.print_exc() - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg) - except codechecker_api_shared.ttypes.RequestFailed as rf: - LOG.warning("%s:\n%s", func_name, rf.message) - raise - except Exception as ex: - import traceback - traceback.print_exc() - msg = str(ex) - LOG.warning("%s:\n%s", func_name, msg) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, msg) - - return wrapper - - def get_component_values( session: DBSession, component_name: str @@ -3933,6 +3902,18 @@ def massStoreRun(self, name, tag, version, b64zip, force, trim_path_prefixes, description) return m.store() + @exc_to_thrift_reqfail + @timeit + def massStoreRunAsynchronous(self, zipfile_blob: str, + store_opts: SubmittedRunOptions) -> str: + import pprint + LOG.info("massStoreRunAsynchronous() called with:\n\t - %d bytes " + "input\n\t - Options:\n\n%s", len(zipfile_blob), + pprint.pformat(store_opts.__dict__, indent=2, depth=8)) + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes.ErrorCode.GENERAL, + "massStoreRunAsynchronous() not implemented in this server build!") + @exc_to_thrift_reqfail @timeit def allowsStoringAnalysisStatistics(self): diff --git a/web/server/codechecker_server/api/tasks.py b/web/server/codechecker_server/api/tasks.py new file mode 100644 index 0000000000..9abe4fb743 --- /dev/null +++ b/web/server/codechecker_server/api/tasks.py @@ -0,0 +1,391 @@ + +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Handle Thrift requests for background task management. +""" +import datetime +import os +import time +from typing import Dict, List, Optional + +from sqlalchemy.sql.expression import and_, or_ + +from codechecker_api_shared.ttypes import RequestFailed, ErrorCode, Ternary +from codechecker_api.codeCheckerServersideTasks_v6.ttypes import \ + AdministratorTaskInfo, TaskFilter, TaskInfo, TaskStatus + +from codechecker_common.logger import get_logger + +from codechecker_server.profiler import timeit + +from ..database.config_db_model import BackgroundTask as DBTask, Product +from ..database.database import DBSession, conv +from ..task_executors.abstract_task import AbstractTask, TaskCancelHonoured +from ..task_executors.task_manager import TaskManager +from .. import permissions +from .common import exc_to_thrift_reqfail + +LOG = get_logger("server") + + +class TestingDummyTask(AbstractTask): + """Implementation of task object created by ``createDummyTask()``.""" + def __init__(self, token: str, timeout: int, should_fail: bool): + super().__init__(token, None) + self.timeout = timeout + self.should_fail = should_fail + + def _implementation(self, tm: TaskManager) -> None: + counter: int = 0 + while counter < self.timeout: + tm.heartbeat(self) + + counter += 1 + LOG.debug("Dummy task ticking... [%d / %d]", + counter, self.timeout) + + if tm.should_cancel(self): + LOG.info("Dummy task '%s' was %s at tick [%d / %d]!", + self.token, + "KILLED BY SHUTDOWN" if tm.is_shutting_down + else "CANCELLED BY ADMIN", + counter, + self.timeout) + raise TaskCancelHonoured(self) + + time.sleep(1) + + if self.should_fail: + raise ValueError("Task self-failure as per the user's request.") + + +def _db_timestamp_to_posix_epoch(d: Optional[datetime.datetime]) \ + -> Optional[int]: + return int(d.replace(tzinfo=datetime.timezone.utc).timestamp()) if d \ + else None + + +def _posix_epoch_to_db_timestamp(s: Optional[int]) \ + -> Optional[datetime.datetime]: + return datetime.datetime.fromtimestamp(s, datetime.timezone.utc) if s \ + else None + + +def _make_task_info(t: DBTask) -> TaskInfo: + """Format API `TaskInfo` from `DBTask`.""" + return TaskInfo( + token=t.token, + taskKind=t.kind, + status=TaskStatus._NAMES_TO_VALUES[t.status.upper()], + productId=t.product_id or 0, + actorUsername=t.username, + summary=t.summary, + comments=t.comments, + enqueuedAtEpoch=_db_timestamp_to_posix_epoch(t.enqueued_at), + startedAtEpoch=_db_timestamp_to_posix_epoch(t.started_at), + completedAtEpoch=_db_timestamp_to_posix_epoch(t.finished_at), + lastHeartbeatEpoch=_db_timestamp_to_posix_epoch( + t.last_seen_at), + cancelFlagSet=t.cancel_flag, + ) + + +def _make_admin_task_info(t: DBTask) -> AdministratorTaskInfo: + """Format API `AdministratorTaskInfo` from `DBTask`.""" + return AdministratorTaskInfo( + normalInfo=_make_task_info(t), + machineId=t.machine_id, + statusConsumed=t.consumed, + ) + + +# These names are inherited from Thrift stubs. +# pylint: disable=invalid-name +class ThriftTaskHandler: + """ + Manages Thrift requests concerning the user-facing Background Tasks API. + """ + + def __init__(self, + configuration_database_sessionmaker, + task_manager: TaskManager, + auth_session): + self._config_db = configuration_database_sessionmaker + self._task_manager = task_manager + self._auth_session = auth_session + + def _get_username(self) -> Optional[str]: + """ + Returns the actually logged in user name. + """ + return self._auth_session.user if self._auth_session else None + + @exc_to_thrift_reqfail + @timeit + def getTaskInfo(self, token: str) -> TaskInfo: + """ + Returns the `TaskInfo` for the task identified by `token`. + """ + with DBSession(self._config_db) as session: + db_task: Optional[DBTask] = session.query(DBTask).get(token) + if not db_task: + raise RequestFailed(ErrorCode.GENERAL, + f"Task '{token}' does not exist!") + + has_right_to_query_status: bool = False + should_set_consumed_flag: bool = False + + if db_task.username == self._get_username(): + has_right_to_query_status = True + should_set_consumed_flag = db_task.is_in_terminated_state + elif db_task.product_id is not None: + associated_product: Optional[Product] = \ + session.query(Product).get(db_task.product_id) + if not associated_product: + LOG.error("No product with ID '%d', but a task is " + "associated with it.", + db_task.product_id) + else: + has_right_to_query_status = \ + permissions.require_permission( + permissions.PRODUCT_ADMIN, + {"config_db_session": session, + "productID": associated_product.id}, + self._auth_session) + + if not has_right_to_query_status: + has_right_to_query_status = permissions.require_permission( + permissions.SUPERUSER, + {"config_db_session": session}, + self._auth_session) + + if not has_right_to_query_status: + raise RequestFailed( + ErrorCode.UNAUTHORIZED, + "Only the task's submitter, a PRODUCT_ADMIN (of the " + "product the task is associated with), or a SUPERUSER " + "can getTaskInfo()!") + + info = _make_task_info(db_task) + + if should_set_consumed_flag: + db_task.consumed = True + session.commit() + + return info + + @exc_to_thrift_reqfail + @timeit + def getTasks(self, filters: TaskFilter) -> List[AdministratorTaskInfo]: + """Obtain tasks matching the `filters` for administrators.""" + if filters.filterForNoProductID and filters.productIDs: + raise RequestFailed(ErrorCode.GENERAL, + "Invalid request, do not set " + "\"no product ID\" and some product IDs in " + "the same filter!") + if filters.filterForNoUsername and filters.usernames: + raise RequestFailed(ErrorCode.GENERAL, + "Invalid request, do not set " + "\"no username\" and some usernames in the " + "same filter!") + + with DBSession(self._config_db) as session: + if filters.filterForNoProductID: + if not permissions.require_permission( + permissions.SUPERUSER, + {"config_db_session": session}, + self._auth_session): + raise RequestFailed( + ErrorCode.UNAUTHORIZED, + "Querying service tasks (not associated with a " + "product) requires SUPERUSER privileges!") + if filters.productIDs: + no_admin_products = [ + prod_id for prod_id in filters.productIDs + if not permissions.require_permission( + permissions.PRODUCT_ADMIN, + {"config_db_session": session, "productID": prod_id}, + self._auth_session)] + if no_admin_products: + no_admin_products = [session.query(Product) + .get(product_id).endpoint + for product_id in no_admin_products] + # pylint: disable=consider-using-f-string + raise RequestFailed(ErrorCode.UNAUTHORIZED, + "Querying product tasks requires " + "PRODUCT_ADMIN rights, but it is " + "missing from product(s): '%s'!" + % ("', '".join(no_admin_products))) + + AND = [] + if filters.tokens: + AND.append(or_(*(DBTask.token.ilike(conv(token)) + for token in filters.tokens))) + + if filters.machineIDs: + AND.append(or_(*(DBTask.machine_id.ilike(conv(machine_id)) + for machine_id in filters.machineIDs))) + + if filters.kinds: + AND.append(or_(*(DBTask.kind.ilike(conv(kind)) + for kind in filters.kinds))) + + if filters.statuses: + AND.append(or_(DBTask.status.in_([ + TaskStatus._VALUES_TO_NAMES[status].lower() + for status in filters.statuses]))) + + if filters.usernames: + AND.append(or_(*(DBTask.username.ilike(conv(username)) + for username in filters.usernames))) + elif filters.filterForNoUsername: + AND.append(DBTask.username.is_(None)) + + if filters.productIDs: + AND.append(or_(DBTask.product_id.in_(filters.productIDs))) + elif filters.filterForNoProductID: + AND.append(DBTask.product_id.is_(None)) + + if filters.enqueuedBeforeEpoch: + AND.append(DBTask.enqueued_at <= _posix_epoch_to_db_timestamp( + filters.enqueuedBeforeEpoch)) + + if filters.enqueuedAfterEpoch: + AND.append(DBTask.enqueued_at >= _posix_epoch_to_db_timestamp( + filters.enqueuedAfterEpoch)) + + if filters.startedBeforeEpoch: + AND.append(DBTask.started_at <= _posix_epoch_to_db_timestamp( + filters.startedBeforeEpoch)) + + if filters.startedAfterEpoch: + AND.append(DBTask.started_at >= _posix_epoch_to_db_timestamp( + filters.startedAfterEpoch)) + + if filters.completedBeforeEpoch: + AND.append(DBTask.finished_at <= _posix_epoch_to_db_timestamp( + filters.completedBeforeEpoch)) + + if filters.completedAfterEpoch: + AND.append(DBTask.finished_at >= _posix_epoch_to_db_timestamp( + filters.completedAfterEpoch)) + + if filters.heartbeatBeforeEpoch: + AND.append(DBTask.last_seen_at <= + _posix_epoch_to_db_timestamp( + filters.heartbeatBeforeEpoch)) + + if filters.heartbeatAfterEpoch: + AND.append(DBTask.last_seen_at >= + _posix_epoch_to_db_timestamp( + filters.heartbeatAfterEpoch)) + + if filters.cancelFlag: + if filters.cancelFlag == Ternary._NAMES_TO_VALUES["OFF"]: + AND.append(DBTask.cancel_flag.is_(False)) + elif filters.cancelFlag == Ternary._NAMES_TO_VALUES["ON"]: + AND.append(DBTask.cancel_flag.is_(True)) + + if filters.consumedFlag: + if filters.consumedFlag == Ternary._NAMES_TO_VALUES["OFF"]: + AND.append(DBTask.consumed.is_(False)) + elif filters.consumedFlag == Ternary._NAMES_TO_VALUES["ON"]: + AND.append(DBTask.consumed.is_(True)) + + ret: List[AdministratorTaskInfo] = [] + has_superuser: Optional[bool] = None + product_admin_rights: Dict[int, bool] = {} + for db_task in session.query(DBTask).filter(and_(*AND)).all(): + if not db_task.product_id: + # Tasks associated with the server, and not a specific + # product, should only be visible to SUPERUSERs. + if has_superuser is None: + has_superuser = permissions.require_permission( + permissions.SUPERUSER, + {"config_db_session": session}, + self._auth_session) + if not has_superuser: + continue + else: + # Tasks associated with a product should only be visible + # to PRODUCT_ADMINs of that product. + try: + if not product_admin_rights[db_task.product_id]: + continue + except KeyError: + product_admin_rights[db_task.product_id] = \ + permissions.require_permission( + permissions.PRODUCT_ADMIN, + {"config_db_session": session, + "productID": db_task.product_id}, + self._auth_session) + if not product_admin_rights[db_task.product_id]: + continue + + ret.append(_make_admin_task_info(db_task)) + + return ret + + @exc_to_thrift_reqfail + @timeit + def cancelTask(self, token: str) -> bool: + """ + Sets the ``cancel_flag`` of the task specified by `token` to `True` + in the database, **REQUESTING** that the task gracefully terminate + itself. + + There are no guarantees that tasks will respect this! + """ + with DBSession(self._config_db) as session: + if not permissions.require_permission( + permissions.SUPERUSER, + {"config_db_session": session}, + self._auth_session): + raise RequestFailed( + ErrorCode.UNAUTHORIZED, + "cancelTask() requires server-level SUPERUSER rights.") + + db_task: Optional[DBTask] = session.query(DBTask).get(token) + if not db_task: + raise RequestFailed(ErrorCode.GENERAL, + f"Task '{token}' does not exist!") + + if not db_task.can_be_cancelled: + return False + + db_task.add_comment("SUPERUSER requested cancellation.", + self._get_username()) + db_task.cancel_flag = True + session.commit() + + return True + + @exc_to_thrift_reqfail + @timeit + def createDummyTask(self, timeout: int, should_fail: bool) -> str: + """ + Used for testing purposes only. + + This function will **ALWAYS** throw an exception when ran outside of a + testing environment. + """ + if "TEST_WORKSPACE" not in os.environ: + raise RequestFailed(ErrorCode.GENERAL, + "createDummyTask() is only available in " + "testing environments!") + + token = self._task_manager.allocate_task_record( + "TaskService::DummyTask", + "Dummy task for testing purposes", + self._get_username(), + None) + + t = TestingDummyTask(token, timeout, should_fail) + self._task_manager.push_task(t) + + return token diff --git a/web/server/codechecker_server/cmd/server.py b/web/server/codechecker_server/cmd/server.py index 33bbbd20f1..7b49982669 100644 --- a/web/server/codechecker_server/cmd/server.py +++ b/web/server/codechecker_server/cmd/server.py @@ -18,13 +18,11 @@ import signal import socket import sys -import time from typing import List, Optional, Tuple, cast from alembic import config from alembic import script from alembic.util import CommandError -import psutil from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import sessionmaker @@ -32,7 +30,7 @@ from codechecker_report_converter import twodim -from codechecker_common import arg, cmd_config, logger, util +from codechecker_common import arg, cmd_config, logger, process, util from codechecker_common.compatibility.multiprocessing import Pool, cpu_count from codechecker_server import instance_manager, server @@ -101,6 +99,25 @@ def add_arguments_to_parser(parser): "authentication settings, TLS certificate" " (cert.pem) and key (key.pem)) from.") + parser.add_argument("--machine-id", + type=str, + dest="machine_id", + default=argparse.SUPPRESS, + required=False, + help=""" +A unique identifier to be used to identify the machine running subsequent +instances of the "same" server process. +This value is only used internally to maintain normal function and bookkeeping +of executed tasks following an unclean server shutdown, e.g., after a crash or +system-level interference. + +If unspecified, defaults to a reasonable default value that is generated from +the computer's hostname, as reported by the operating system. +In most scenarios, there is no need to fine-tune this, except if subsequent +executions of the "same" server is achieved in distinct environments, e.g., +if the server otherwise is running in a container. +""") + parser.add_argument('--host', type=str, dest="listen_address", @@ -424,7 +441,7 @@ def arg_match(options): setattr(args, "instance_manager", True) # If everything is fine, do call the handler for the subcommand. - main(args) + return main(args) parser.set_defaults( func=__handle, func_process_config_file=cmd_config.process_config_file) @@ -762,42 +779,6 @@ def _get_migration_decisions() -> List[Tuple[str, str, bool]]: return 0 -def kill_process_tree(parent_pid, recursive=False): - """Stop the process tree try it gracefully first. - - Try to stop the parent and child processes gracefuly - first if they do not stop in time send a kill signal - to every member of the process tree. - - There is a similar function in the analyzer part please - consider to update that in case of changing this. - """ - proc = psutil.Process(parent_pid) - children = proc.children(recursive) - - # Send a SIGTERM (Ctrl-C) to the main process - proc.terminate() - - # If children processes don't stop gracefully in time, - # slaughter them by force. - _, still_alive = psutil.wait_procs(children, timeout=5) - for p in still_alive: - p.kill() - - # Wait until this process is running. - n = 0 - timeout = 10 - while proc.is_running(): - if n > timeout: - LOG.warning("Waiting for process %s to stop has been timed out" - "(timeout = %s)! Process is still running!", - parent_pid, timeout) - break - - time.sleep(1) - n += 1 - - def __instance_management(args): """Handles the instance-manager commands --list/--stop/--stop-all.""" @@ -842,7 +823,7 @@ def __instance_management(args): continue try: - kill_process_tree(i['pid']) + process.kill_process_tree(i['pid']) LOG.info("Stopped CodeChecker server running on port %s " "in workspace %s (PID: %s)", i['port'], i['workspace'], i['pid']) @@ -1106,16 +1087,21 @@ def server_init_start(args): 'doc_root': context.doc_root, 'version': context.package_git_tag} + # Create a machine ID if the user did not specify one. + machine_id = getattr(args, "machine_id", + f"{socket.gethostname()}:{args.view_port}") + try: - server.start_server(args.config_directory, - package_data, - args.view_port, - cfg_sql_server, - args.listen_address, - 'force_auth' in args, - args.skip_db_cleanup, - context, - environ) + return server.start_server(args.config_directory, + package_data, + args.view_port, + cfg_sql_server, + args.listen_address, + 'force_auth' in args, + args.skip_db_cleanup, + context, + environ, + machine_id) except socket.error as err: if err.errno == errno.EADDRINUSE: LOG.error("Server can't be started, maybe port number (%s) is " @@ -1152,4 +1138,4 @@ def main(args): except FileNotFoundError as fnerr: LOG.error(fnerr) sys.exit(1) - server_init_start(args) + return server_init_start(args) diff --git a/web/server/codechecker_server/database/config_db_model.py b/web/server/codechecker_server/database/config_db_model.py index 00f0c4948e..e2ee5a550b 100644 --- a/web/server/codechecker_server/database/config_db_model.py +++ b/web/server/codechecker_server/database/config_db_model.py @@ -8,8 +8,9 @@ """ SQLAlchemy ORM model for the product configuration database. """ -from datetime import datetime +from datetime import datetime, timezone import sys +from typing import Optional from sqlalchemy import Boolean, CHAR, Column, DateTime, Enum, ForeignKey, \ Integer, MetaData, String, Text @@ -158,6 +159,200 @@ def __init__(self, config_key, config_value): self.config_value = config_value +class BackgroundTask(Base): + """ + Information about background tasks executed on a CodeChecker service, + potentially as part of a cluster, stored in the database. + These entities store the metadata for the task objects, but no information + about the actual "input" of the task exists in the database! + """ + __tablename__ = "background_tasks" + + _token_length = 64 + + machine_id = Column(String, index=True) + """ + A unique, implementation-specific identifier of the actual CodeChecker + server instance that knows how to execute the task. + """ + + token = Column(CHAR(length=_token_length), primary_key=True) + kind = Column(String, nullable=False, index=True) + status = Column(Enum( + # A job token (and thus a BackgroundTask record) was allocated, but + # the job is still under preparation. + "allocated", + + # The job is pending on the server, but the server has all the data + # available to eventually perform the job. + "enqueued", + + # The server is actually performing the job. + "running", + + # The server successfully finished completing the job. + "completed", + + # The execution of the job failed. + # In this stage, the "comments" field likely contains more information + # that is not machine-readable. + "failed", + + # The job never started, or its execution was terminated at the + # request of the administrators. + "cancelled", + + # The job never started, or its execution was terminated due to a + # system-level reason (such as the server's foced shutdown). + "dropped", + ), + nullable=False, + default="enqueued", + index=True) + + product_id = Column(Integer, + ForeignKey("products.id", + deferrable=False, + initially="IMMEDIATE", + ondelete="CASCADE"), + nullable=True, + index=True) + """ + If the job is tightly associated with a product, the ID of the `Product` + entity with which it is associated. + """ + + username = Column(String, nullable=True) + """ + The main actor who was responsible for the creation of the job task. + """ + + summary = Column(String, nullable=False) + comments = Column(Text, nullable=True) + + enqueued_at = Column(DateTime, nullable=True) + started_at = Column(DateTime, nullable=True) + finished_at = Column(DateTime, nullable=True) + + last_seen_at = Column(DateTime, nullable=True) + """ + Contains the timestamp, only when the job is not yet "finished", when the + job last synchronised against the database, e.g., when it last checked the + "cancel_flag" field. + + This is used for health checking whether the background worker is actually + doing something, as a second line of defence to uncover "dropped" jobs, + e.g., when the servers have failed and the new server can not identify + jobs from its "previous life". + """ + + consumed = Column(Boolean, nullable=False, + default=False, server_default=false()) + """ + Whether the status of the job was checked **BY THE MAIN ACTOR** (username). + """ + + cancel_flag = Column(Boolean, nullable=False, + default=False, server_default=false()) + """ + Whether a SUPERUSER has signalled that the job should be cancelled. + + Note, that cancelling is a co-operative action: jobs are never actually + "killed" on the O.S. level from the outside; rather, each job is expected + to be implemented in a way that they regularly query this bit, and if set, + act accordingly. + """ + + def __init__(self, + token: str, + kind: str, + summary: str, + machine_id: str, + user_name: Optional[str], + product: Optional[Product] = None, + ): + self.machine_id = machine_id + self.token = token + self.kind = kind + self.status = "allocated" + self.summary = summary + self.username = user_name + self.last_seen_at = datetime.now(timezone.utc) + + if product: + self.product_id = product.id + + def add_comment(self, comment: str, actor: Optional[str] = None): + if not self.comments: + self.comments = "" + elif self.comments: + self.comments += "\n----------\n" + + self.comments += f"{actor if actor else ''} " \ + f"at {str(datetime.now(timezone.utc))}:\n{comment}" + + def heartbeat(self): + """Update `last_seen_at`.""" + if self.status in ["enqueued", "running"]: + self.last_seen_at = datetime.now(timezone.utc) + + def set_enqueued(self): + """Marks the job as successfully enqueued.""" + if self.status != "allocated": + raise ValueError( + f"Invalid transition '{str(self.status)}' -> 'enqueued'") + + self.status = "enqueued" + self.enqueued_at = datetime.now(timezone.utc) + + def set_running(self): + """Marks the job as currently executing.""" + if self.status != "enqueued": + raise ValueError( + f"Invalid transition '{str(self.status)}' -> 'running'") + + self.status = "running" + self.started_at = datetime.now(timezone.utc) + + def set_finished(self, successfully: bool = True): + """Marks the job as successfully completed or failed.""" + new_status = "completed" if successfully else "failed" + if self.status != "running": + raise ValueError( + f"Invalid transition '{str(self.status)}' -> '{new_status}'") + + self.status = new_status + self.finished_at = datetime.now(timezone.utc) + + def set_abandoned(self, force_dropped_status: bool = False): + """ + Marks the job as cancelled or dropped based on whether the + cancel flag is set. + """ + new_status = "cancelled" \ + if not force_dropped_status and self.cancel_flag \ + else "dropped" + + self.status = new_status + self.finished_at = datetime.now(timezone.utc) + + @property + def is_in_terminated_state(self) -> bool: + """ + Returns whether the current task has finished execution in some way, + for some reason. + """ + return self.status not in ["allocated", "enqueued", "running"] + + @property + def can_be_cancelled(self) -> bool: + """ + Returns whether the task is in a state where setting `cancel_flag` + is meaningful. + """ + return not self.is_in_terminated_state and not self.cancel_flag + + IDENTIFIER = { 'identifier': "ConfigDatabase", 'orm_meta': CC_META diff --git a/web/server/codechecker_server/migrations/config/versions/73b04c41885b_implemented_keeping_track_of_background_tasks.py b/web/server/codechecker_server/migrations/config/versions/73b04c41885b_implemented_keeping_track_of_background_tasks.py new file mode 100644 index 0000000000..45b3ab7bc1 --- /dev/null +++ b/web/server/codechecker_server/migrations/config/versions/73b04c41885b_implemented_keeping_track_of_background_tasks.py @@ -0,0 +1,79 @@ +""" +Implemented keeping track of background tasks through corresponding records +in the server-wide configuration database. + +Revision ID: 73b04c41885b +Revises: 00099e8bc212 +Create Date: 2023-09-21 14:24:27.395597 +""" + +from alembic import op +import sqlalchemy as sa + + +# Revision identifiers, used by Alembic. +revision = '73b04c41885b' +down_revision = '00099e8bc212' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "background_tasks", + sa.Column("machine_id", sa.String(), nullable=True), + sa.Column("token", sa.CHAR(length=64), nullable=False), + sa.Column("kind", sa.String(), nullable=False), + sa.Column("status", sa.Enum("allocated", + "enqueued", + "running", + "completed", + "failed", + "cancelled", + "dropped", + name="background_task_statuses"), + nullable=False), + sa.Column("product_id", sa.Integer(), nullable=True), + sa.Column("summary", sa.String(), nullable=False), + sa.Column("comments", sa.Text(), nullable=True), + sa.Column("username", sa.String(), nullable=True), + sa.Column("enqueued_at", sa.DateTime(), nullable=True), + sa.Column("started_at", sa.DateTime(), nullable=True), + sa.Column("finished_at", sa.DateTime(), nullable=True), + sa.Column("last_seen_at", sa.DateTime(), nullable=True), + sa.Column("consumed", sa.Boolean(), nullable=False, + server_default=sa.false()), + sa.Column("cancel_flag", sa.Boolean(), nullable=False, + server_default=sa.false()), + + sa.ForeignKeyConstraint( + ["product_id"], ["products.id"], + name=op.f("fk_background_tasks_product_id_products"), + deferrable=False, + ondelete="CASCADE", + initially="IMMEDIATE"), + sa.PrimaryKeyConstraint("token", name=op.f("pk_background_tasks")) + ) + op.create_index(op.f("ix_background_tasks_kind"), "background_tasks", + ["kind"], unique=False) + op.create_index(op.f("ix_background_tasks_machine_id"), "background_tasks", + ["machine_id"], unique=False) + op.create_index(op.f("ix_background_tasks_product_id"), "background_tasks", + ["product_id"], unique=False) + op.create_index(op.f("ix_background_tasks_status"), "background_tasks", + ["status"], unique=False) + + +def downgrade(): + ctx = op.get_context() + dialect = ctx.dialect.name + + op.drop_index(op.f("ix_background_tasks_status"), "background_tasks") + op.drop_index(op.f("ix_background_tasks_product_id"), "background_tasks") + op.drop_index(op.f("ix_background_tasks_machine_id"), "background_tasks") + op.drop_index(op.f("ix_background_tasks_kind"), "background_tasks") + + op.drop_table("action_history") + + if dialect == "postgresql": + op.execute("DROP TYPE background_task_statuses;") diff --git a/web/server/codechecker_server/routing.py b/web/server/codechecker_server/routing.py index 79ac8d0686..34fbb82f87 100644 --- a/web/server/codechecker_server/routing.py +++ b/web/server/codechecker_server/routing.py @@ -15,25 +15,28 @@ from codechecker_web.shared.version import SUPPORTED_VERSIONS -# A list of top-level path elements under the webserver root -# which should not be considered as a product route. -NON_PRODUCT_ENDPOINTS = ['index.html', - 'images', - 'docs', - 'live', - 'ready'] +# A list of top-level path elements under the webserver root which should not +# be considered as a product route. +NON_PRODUCT_ENDPOINTS = ["index.html", + "images", + "docs", + "live", + "ready", + ] # A list of top-level path elements in requests (such as Thrift endpoints) # which should not be considered as a product route. -NON_PRODUCT_ENDPOINTS += ['Authentication', - 'Products', - 'CodeCheckerService'] +NON_PRODUCT_ENDPOINTS += ["Authentication", + "Products", + "CodeCheckerService", + "Tasks", + ] # A list of top-level path elements under the webserver root which should -# be protected by authentication requirement when accessing the server. +# be protected by authentication requirements when accessing the server. PROTECTED_ENTRY_POINTS = ['', # Empty string in a request is 'index.html'. - 'index.html'] + "index.html"] def is_valid_product_endpoint(uripart): @@ -68,9 +71,8 @@ def is_supported_version(version): If supported, returns the major and minor version as a tuple. """ - version = version.lstrip('v') - version_parts = version.split('.') + version_parts = version.split('.', 2) # We don't care if accidentally the version tag contains a revision number. major, minor = int(version_parts[0]), int(version_parts[1]) @@ -113,9 +115,8 @@ def split_client_POST_request(path): Returns the product endpoint, the API version and the API service endpoint as a tuple of 3. """ - # A standard POST request from an API client looks like: - # http://localhost:8001/[product-name]// + # http://localhost:8001/[product-name]/v/ # where specifying the product name is optional. split_path = urlparse(path).path.split('/', 3) diff --git a/web/server/codechecker_server/server.py b/web/server/codechecker_server/server.py index 40bdf6db4d..f10f28291e 100644 --- a/web/server/codechecker_server/server.py +++ b/web/server/codechecker_server/server.py @@ -12,6 +12,7 @@ import atexit +from collections import Counter import datetime from functools import partial from hashlib import sha256 @@ -25,10 +26,10 @@ import ssl import sys import stat -from typing import List, Optional, Tuple +import time +from typing import Dict, List, Optional, Tuple, cast import urllib -import multiprocess from sqlalchemy.orm import sessionmaker from sqlalchemy.sql.expression import func from thrift.protocol import TJSONProtocol @@ -47,11 +48,14 @@ codeCheckerProductService as ProductAPI_v6 from codechecker_api.ServerInfo_v6 import \ serverInfoService as ServerInfoAPI_v6 +from codechecker_api.codeCheckerServersideTasks_v6 import \ + codeCheckerServersideTaskService as TaskAPI_v6 from codechecker_common import util -from codechecker_common.logger import get_logger from codechecker_common.compatibility.multiprocessing import \ - Pool, cpu_count + Pool, Process, Queue, Value, cpu_count +from codechecker_common.logger import get_logger, signal_log +from codechecker_common.util import generate_random_token from codechecker_web.shared import database_status from codechecker_web.shared.version import get_version_str @@ -63,12 +67,15 @@ from .api.report_server import ThriftRequestHandler as ReportHandler_v6 from .api.server_info_handler import \ ThriftServerInfoHandler as ServerInfoHandler_v6 +from .api.tasks import ThriftTaskHandler as TaskHandler_v6 from .database import database, db_cleanup from .database.config_db_model import Product as ORMProduct, \ Configuration as ORMConfiguration from .database.database import DBSession from .database.run_db_model import IDENTIFIER as RUN_META, Run, RunLock -from .tmp import get_tmp_dir_hash +from .task_executors.main import executor as background_task_executor +from .task_executors.task_manager import \ + TaskManager as BackgroundTaskManager, drop_all_incomplete_tasks LOG = get_logger('server') @@ -85,8 +92,8 @@ def __init__(self, request, client_address, server): self.path = None super().__init__(request, client_address, server) - def log_message(self, *args): - """ Silencing http server. """ + def log_message(self, *_args): + """Silencing HTTP server.""" return def send_thrift_exception(self, error_msg, iprot, oprot, otrans): @@ -104,7 +111,7 @@ def send_thrift_exception(self, error_msg, iprot, oprot, otrans): result = otrans.getvalue() self.send_response(200) self.send_header("content-type", "application/x-thrift") - self.send_header("Content-Length", len(result)) + self.send_header("Content-Length", str(len(result))) self.end_headers() self.wfile.write(result) @@ -369,22 +376,22 @@ def do_POST(self): major_version, _ = version_supported if major_version == 6: - if request_endpoint == 'Authentication': + if request_endpoint == "Authentication": auth_handler = AuthHandler_v6( self.server.manager, self.auth_session, self.server.config_session) processor = AuthAPI_v6.Processor(auth_handler) - elif request_endpoint == 'Configuration': + elif request_endpoint == "Configuration": conf_handler = ConfigHandler_v6( self.auth_session, self.server.config_session) processor = ConfigAPI_v6.Processor(conf_handler) - elif request_endpoint == 'ServerInfo': + elif request_endpoint == "ServerInfo": server_info_handler = ServerInfoHandler_v6(version) processor = ServerInfoAPI_v6.Processor( server_info_handler) - elif request_endpoint == 'Products': + elif request_endpoint == "Products": prod_handler = ProductHandler_v6( self.server, self.auth_session, @@ -392,7 +399,13 @@ def do_POST(self): product, version) processor = ProductAPI_v6.Processor(prod_handler) - elif request_endpoint == 'CodeCheckerService': + elif request_endpoint == "Tasks": + task_handler = TaskHandler_v6( + self.server.config_session, + self.server.task_manager, + self.auth_session) + processor = TaskAPI_v6.Processor(task_handler) + elif request_endpoint == "CodeCheckerService": # This endpoint is a product's report_server. if not product: error_msg = \ @@ -745,7 +758,10 @@ def __init__(self, pckg_data, context, check_env, - manager): + manager: session_manager.SessionManager, + machine_id: str, + task_queue: Queue, + server_shutdown_flag: Value): LOG.debug("Initializing HTTP server...") @@ -756,6 +772,7 @@ def __init__(self, self.context = context self.check_env = check_env self.manager = manager + self.address, self.port = server_address self.__products = {} # Create a database engine for the configuration database. @@ -764,6 +781,12 @@ def __init__(self, self.config_session = sessionmaker(bind=self.__engine) self.manager.set_database_connection(self.config_session) + self.__task_queue = task_queue + self.task_manager = BackgroundTaskManager(task_queue, + self.config_session, + server_shutdown_flag, + machine_id) + # Load the initial list of products and set up the server. cfg_sess = self.config_session() permissions.initialise_defaults('SYSTEM', { @@ -780,7 +803,7 @@ def __init__(self, cfg_sess.close() try: - HTTPServer.__init__(self, server_address, + HTTPServer.__init__(self, (self.address, self.port), RequestHandlerClass, bind_and_activate=True) ssl_key_file = os.path.join(config_directory, "key.pem") @@ -806,13 +829,23 @@ def __init__(self, else: LOG.info("Searching for SSL key at %s, cert at %s, " - "not found...", ssl_key_file, ssl_cert_file) + "not found!", ssl_key_file, ssl_cert_file) LOG.info("Falling back to simple, insecure HTTP.") except Exception as e: LOG.error("Couldn't start the server: %s", e.__str__()) raise + # If the server was started with the port 0, the OS will pick an + # available port. + # For this reason, we will update the port variable after server + # ininitialisation. + self.port = self.socket.getsockname()[1] + + @property + def formatted_address(self) -> str: + return f"{str(self.address)}:{self.port}" + def configure_keepalive(self): """ Enable keepalive on the socket and some TCP keepalive configuration @@ -855,17 +888,40 @@ def configure_keepalive(self): LOG.error('Failed to set TCP max keepalive probe: %s', ret) def terminate(self): - """ - Terminating the server. - """ + """Terminates the server and releases associated resources.""" try: self.server_close() + self.__task_queue.close() + self.__task_queue.join_thread() self.__engine.dispose() + + sys.exit(128 + signal.SIGINT) except Exception as ex: LOG.error("Failed to shut down the WEB server!") LOG.error(str(ex)) sys.exit(1) + def serve_forever_with_shutdown_handler(self): + """ + Calls `HTTPServer.serve_forever` but handles SIGINT (2) signals + gracefully such that the open resources are properly cleaned up. + """ + def _handler(signum: int, _frame): + if signum not in [signal.SIGINT]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by " + "'serve_forever_with_shutdown_handler'!") + return + + signal_log(LOG, "DEBUG", f"{os.getpid()}: Received " + f"{signal.Signals(signum).name} ({signum}), " + "performing shutdown ...") + self.terminate() + + signal.signal(signal.SIGINT, _handler) + return self.serve_forever() + def add_product(self, orm_product, init_db=False): """ Adds a product to the list of product databases connected to @@ -990,6 +1046,10 @@ class CCSimpleHttpServerIPv6(CCSimpleHttpServer): address_family = socket.AF_INET6 + @property + def formatted_address(self) -> str: + return f"[{str(self.address)}]:{self.port}" + def __make_root_file(root_file): """ @@ -1000,7 +1060,7 @@ def __make_root_file(root_file): LOG.debug("Generating initial superuser (root) credentials...") username = ''.join(sample("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6)) - password = get_tmp_dir_hash()[:8] + password = generate_random_token(8) LOG.info("A NEW superuser credential was generated for the server. " "This information IS SAVED, thus subsequent server starts " @@ -1028,16 +1088,16 @@ def __make_root_file(root_file): return secret -def start_server(config_directory, package_data, port, config_sql_server, - listen_address, force_auth, skip_db_cleanup: bool, - context, check_env): +def start_server(config_directory: str, package_data, port: int, + config_sql_server, listen_address: str, + force_auth: bool, skip_db_cleanup: bool, + context, check_env, machine_id: str) -> int: """ - Start http server to handle web client and thrift requests. + Starts the HTTP server to handle Web client and Thrift requests, execute + background jobs. """ LOG.debug("Starting CodeChecker server...") - server_addr = (listen_address, port) - root_file = os.path.join(config_directory, 'root.user') if not os.path.exists(root_file): LOG.warning("Server started without 'root.user' present in " @@ -1103,92 +1163,445 @@ def start_server(config_directory, package_data, port, config_sql_server, else: LOG.debug("Skipping db_cleanup, as requested.") + def _cleanup_incomplete_tasks(action: str) -> int: + config_session_factory = config_sql_server.create_engine() + try: + return drop_all_incomplete_tasks( + sessionmaker(bind=config_session_factory), + machine_id, action) + finally: + config_session_factory.dispose() + + dropped_tasks = _cleanup_incomplete_tasks( + "New server started with the same machine_id, assuming the old " + "server is dead and won't be able to finish the task.") + if dropped_tasks: + LOG.info("At server startup, dropped %d background tasks left behind " + "by a previous server instance matching machine ID '%s'.", + dropped_tasks, machine_id) + + api_processes: Dict[int, Process] = {} + requested_api_threads = cast(int, manager.worker_processes) \ + or cpu_count() + + bg_processes: Dict[int, Process] = {} + requested_bg_threads = cast(int, + manager.background_worker_processes) \ + or requested_api_threads + # Note that Queue under the hood uses OS-level primitives such as a socket + # or a pipe, where the read-write buffers have a **LIMITED** capacity, and + # are usually **NOT** backed by the full amount of available system memory. + bg_task_queue: Queue = Queue() + is_server_shutting_down = Value('B', False) + server_clazz = CCSimpleHttpServer - if ':' in server_addr[0]: + if ':' in listen_address: # IPv6 address specified for listening. # FIXME: Python>=3.8 automatically handles IPv6 if ':' is in the bind # address, see https://bugs.python.org/issue24209. server_clazz = CCSimpleHttpServerIPv6 - http_server = server_clazz(server_addr, + http_server = server_clazz((listen_address, port), RequestHandler, config_directory, config_sql_server, package_data, context, check_env, - manager) + manager, + machine_id, + bg_task_queue, + is_server_shutting_down) + + try: + instance_manager.register(os.getpid(), + os.path.abspath( + context.codechecker_workspace), + port) + except IOError as ex: + LOG.debug(ex.strerror) - # If the server was started with the port 0, the OS will pick an available - # port. For this reason we will update the port variable after server - # initialization. - port = http_server.socket.getsockname()[1] + def unregister_handler(pid): + # Handle errors during instance unregistration. + # The workspace might be removed so updating the config content might + # fail. + try: + instance_manager.unregister(pid) + except IOError as ex: + LOG.debug(ex.strerror) - processes = [] + atexit.register(unregister_handler, os.getpid()) - def signal_handler(signum, _): + def _start_process_with_no_signal_handling(**kwargs): """ - Handle SIGTERM to stop the server running. + Starts a `multiprocessing.Process` in a context where the signal + handling is temporarily disabled, such that the child process does not + inherit any signal handling from the parent. + + Child processes spawned after the main process set up its signals + MUST NOT inherit the signal handling because that would result in + multiple children firing on the SIGTERM handler, for example. + + For this reason, we temporarily disable the signal handling here by + returning to the initial defaults, and then restore the main process's + signal handling to be the usual one. """ - LOG.info("Shutting down the WEB server on [%s:%d]", - '[' + listen_address + ']' - if server_clazz is CCSimpleHttpServerIPv6 else listen_address, - port) - http_server.terminate() + signals_to_disable = [signal.SIGINT, signal.SIGTERM] + if sys.platform != "win32": + signals_to_disable += [signal.SIGCHLD, signal.SIGHUP] - # Terminate child processes. - for pp in processes: - pp.terminate() + existing_signal_handlers = {} + for signum in signals_to_disable: + existing_signal_handlers[signum] = signal.signal( + signum, signal.SIG_DFL) + + p = Process(**kwargs) + p.start() + + for signum in signals_to_disable: + signal.signal(signum, existing_signal_handlers[signum]) + + return p + + # Save a process-wide but not shared counter in the main process for how + # many subprocesses of each kind had been spawned, as this will be used in + # the internal naming of the workers. + spawned_api_proc_count: int = 0 + spawned_bg_proc_count: int = 0 + + def spawn_api_process(): + """Starts a single HTTP API worker process for CodeChecker server.""" + nonlocal spawned_api_proc_count + spawned_api_proc_count += 1 + + p = _start_process_with_no_signal_handling( + target=http_server.serve_forever_with_shutdown_handler, + name=f"CodeChecker-API-{spawned_api_proc_count}") + api_processes[cast(int, p.pid)] = p + signal_log(LOG, "DEBUG", f"API handler child process {p.pid} started!") + return p + + LOG.info("Using %d API request handler processes ...", + requested_api_threads) + for _ in range(requested_api_threads): + spawn_api_process() + + def spawn_bg_process(): + """Starts a single Task worker process for CodeChecker server.""" + nonlocal spawned_bg_proc_count + spawned_bg_proc_count += 1 + + p = _start_process_with_no_signal_handling( + target=background_task_executor, + args=(bg_task_queue, + config_sql_server, + is_server_shutting_down, + machine_id, + ), + name=f"CodeChecker-Task-{spawned_bg_proc_count}") + bg_processes[cast(int, p.pid)] = p + signal_log(LOG, "DEBUG", f"Task child process {p.pid} started!") + return p + + LOG.info("Using %d Task handler processes ...", requested_bg_threads) + for _ in range(requested_bg_threads): + spawn_bg_process() + + termination_signal_timestamp = Value('d', 0) + + def forced_termination_signal_handler(signum: int, _frame): + """ + Handle SIGINT (2) and SIGTERM (15) received a second time to stop the + server ungracefully. + """ + if signum not in [signal.SIGINT, signal.SIGTERM]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by " + "'forced_termination_signal_handler'!") + return + if not is_server_shutting_down.value or \ + abs(termination_signal_timestamp.value) <= \ + sys.float_info.epsilon: + return + if time.time() - termination_signal_timestamp.value <= 2.0: + # Allow some time to pass between the handling of the normal + # termination vs. doing something in the "forced" handler, because + # a human's ^C keypress in a terminal can generate multiple SIGINTs + # in a quick succession. + return + + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + signal_log(LOG, "WARNING", "Termination signal " + f"<{signal.Signals(signum).name} ({signum})> " + "received a second time, **FORCE** killing the WEB server " + f"on [{http_server.formatted_address}] ...") + + for p in list(api_processes.values()) + list(bg_processes.values()): + try: + p.kill() + except (OSError, ValueError): + pass + # No mercy this time. sys.exit(128 + signum) - def reload_signal_handler(*_args, **_kwargs): + exit_code = Value('B', 0) + + def termination_signal_handler(signum: int, _frame): """ - Reloads server configuration file. + Handle SIGINT (2) and SIGTERM (15) to stop the server gracefully. """ + # Debounce termination signals at this point. + signal.signal(signal.SIGINT, forced_termination_signal_handler) + signal.signal(signal.SIGTERM, forced_termination_signal_handler) + + if is_server_shutting_down.value: + return + if signum not in [signal.SIGINT, signal.SIGTERM]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by 'termination_signal_handler'!") + return + + is_server_shutting_down.value = True + termination_signal_timestamp.value = time.time() + + exit_code.value = 128 + signum + signal_log(LOG, "INFO", "Shutting down the WEB server on " + f"[{http_server.formatted_address}] ... " + "Please allow some time for graceful clean-up!") + + # Terminate child processes. + # For these subprocesses, let the processes properly clean up after + # themselves in a graceful shutdown scenario. + # For this reason, we fire a bunch of SIGHUPs first, indicating + # that the main server process wants to exit, and then wait for + # the children to die once all of them got the signal. + for pid in api_processes: + try: + signal_log(LOG, "DEBUG", f"SIGINT! API child PID: {pid} ...") + os.kill(pid, signal.SIGINT) + except (OSError, ValueError): + pass + for pid in list(api_processes.keys()): + p = api_processes[pid] + try: + signal_log(LOG, "DEBUG", f"join() API child PID: {pid} ...") + p.join() + p.close() + except (OSError, ValueError): + pass + finally: + del api_processes[pid] + + bg_task_queue.close() + bg_task_queue.join_thread() + for pid in bg_processes: + try: + signal_log(LOG, "DEBUG", f"SIGHUP! Task child PID: {pid} ...") + os.kill(pid, signal.SIGHUP) + except (OSError, ValueError): + pass + for pid in list(bg_processes.keys()): + p = bg_processes[pid] + try: + signal_log(LOG, "DEBUG", f"join() Task child PID: {pid} ...") + p.join() + p.close() + except (OSError, ValueError): + pass + finally: + del bg_processes[pid] + + def reload_signal_handler(signum: int, _frame): + """ + Handle SIGHUP (1) to reload the server's configuration file to memory. + """ + if signum not in [signal.SIGHUP]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by 'reload_signal_handler'!") + return + + signal_log(LOG, "INFO", + "Received signal to reload server configuration ...") + manager.reload_config() - try: - instance_manager.register(os.getpid(), - os.path.abspath( - context.codechecker_workspace), - port) - except IOError as ex: - LOG.debug(ex.strerror) + signal_log(LOG, "INFO", "Server configuration reload: Done.") - LOG.info("Server waiting for client requests on [%s:%d]", - '[' + listen_address + ']' - if server_clazz is CCSimpleHttpServerIPv6 else listen_address, - port) + sigchild_event_counter = Value('I', 0) + is_already_handling_sigchild = Value('B', False) - def unregister_handler(pid): + def child_signal_handler(signum: int, _frame): """ - Handle errors during instance unregistration. - The workspace might be removed so updating the - config content might fail. + Handle SIGCHLD (17) that signals a child process's interruption or + death by creating a new child to ensure that the requested number of + workers are always alive. """ - try: - instance_manager.unregister(pid) - except IOError as ex: - LOG.debug(ex.strerror) + if is_already_handling_sigchild.value: + # Do not perform this handler recursively to prevent spawning too + # many children. + return + if is_server_shutting_down.value: + # Do not handle SIGCHLD events during normal shutdown, because + # our own subprocess termination calls would fire this handler. + return + if signum not in [signal.SIGCHLD]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by 'child_signal_handler'!") + return - atexit.register(unregister_handler, os.getpid()) + is_already_handling_sigchild.value = True - for _ in range(manager.worker_processes - 1): - p = multiprocess.Process(target=http_server.serve_forever) - processes.append(p) - p.start() + force_slow_path: bool = False + event_counter: int = sigchild_event_counter.value + if event_counter >= \ + min(requested_api_threads, requested_bg_threads) // 2: + force_slow_path = True + else: + sigchild_event_counter.value = event_counter + 1 - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) + # How many new processes need to be spawned for each type of worker + # process? + spawn_needs: Counter = Counter() + def _check_process_one(kind: str, proclist: Dict[int, Process], + pid: int): + try: + p = proclist[pid] + except KeyError: + return + + # Unfortunately, "Process.is_alive()" cannot be used here, because + # during the handling of SIGCHLD during a child's death, according + # to the state of Python's data structures, the child is still + # alive. + # We run a low-level non-blocking wait again, which will + # immediately return, but properly reap the child process if it has + # terminated. + try: + _, status_signal = os.waitpid(pid, os.WNOHANG) + if status_signal == 0: + # The process is still alive. + return + except ChildProcessError: + pass + + signal_log(LOG, "WARNING", + f"'{kind}' child process (PID {pid}, \"{p.name}\") " + "is not alive anymore!") + spawn_needs[kind] += 1 + + try: + del proclist[pid] + except KeyError: + # Due to the bunching up of signals and that Python runs the + # C-level signals with a custom logic inside the interpreter, + # coupled with the fact that PIDs can be reused, the same PID + # can be reported dead in a quick succession of signals, + # resulting in a KeyError here. + pass + + def _check_processes_many(kind: str, proclist: Dict[int, Process]): + for pid in sorted(proclist.keys()): + _check_process_one(kind, proclist, pid) + + # Try to find the type of the interrupted/dead process based on signal + # information first. + # This should be quicker and more deterministic. + try: + child_pid, child_signal = os.waitpid(-1, os.WNOHANG) + if child_signal == 0: + # Go to the slow path and check the children manually, we did + # not receive a reply from waitpid() with an actual dead child. + raise ChildProcessError() + + _check_process_one("api", api_processes, child_pid) + _check_process_one("background", bg_processes, child_pid) + except ChildProcessError: + # We have not gotten a PID, or it was not found, so we do not know + # who died; in this case, it is better to go on the slow path and + # query all our children individually. + spawn_needs.clear() # Forces the Counter to be empty. + + if force_slow_path: + # A clever sequence of child killings in variously sized batches + # can easily result in missing a few signals here and there, and + # missing a few dead children because 'os.waitpid()' allows us to + # fall into a false "fast path" situation. + # To remedy this, we every so often force a slow path to ensure + # the number of worker processes is as close to the requested + # amount of possible. + + # Forces the Counter to be empty, even if the fast path put an + # entry in there. + spawn_needs.clear() + + if not spawn_needs: + _check_processes_many("api", api_processes) + _check_processes_many("background", bg_processes) + + if force_slow_path: + sigchild_event_counter.value = 0 + signal_log(LOG, "WARNING", + "Too many children died since last full status " + "check, performing one ...") + + # If we came into the handler with a "forced slow path" situation, + # ensure that we spawn enough new processes to backfill the + # missing amount, even if due to the flakyness of signal handling, + # we might not have actually gotten "N" times SIGCHLD firings for + # the death of N children, if they happened in a bundle situation, + # e.g., kill N/4, then kill N/2, then kill 1 or 2, then kill the + # remaining. + spawn_needs["api"] = \ + util.clamp(0, requested_api_threads - len(api_processes), + requested_api_threads) + spawn_needs["background"] = \ + util.clamp(0, requested_bg_threads - len(bg_processes), + requested_bg_threads) + + for kind, num in spawn_needs.items(): + signal_log(LOG, "INFO", + f"(Re-)starting {num} '{kind}' child process(es) ...") + + if kind == "api": + for _ in range(num): + spawn_api_process() + elif kind == "background": + for _ in range(num): + spawn_bg_process() + + is_already_handling_sigchild.value = False + + signal.signal(signal.SIGINT, termination_signal_handler) + signal.signal(signal.SIGTERM, termination_signal_handler) if sys.platform != "win32": + signal.signal(signal.SIGCHLD, child_signal_handler) signal.signal(signal.SIGHUP, reload_signal_handler) - # Main process also acts as a worker. - http_server.serve_forever() + LOG.info("Server waiting for client requests on [%s]", + http_server.formatted_address) + + # We can not use a multiprocessing.Event here because that would result in + # a deadlock, as the process waiting on the event is the one receiving the + # shutdown signal. + while not is_server_shutting_down.value: + time.sleep(5) + + dropped_tasks = _cleanup_incomplete_tasks("Server shut down, task will " + "be never be completed.") + if dropped_tasks: + LOG.info("At server shutdown, dropped %d background tasks that will " + "never be completed.", dropped_tasks) - LOG.info("Webserver quit.") + LOG.info("CodeChecker server quit (main process).") + return exit_code.value def add_initial_run_database(config_sql_server, product_connection): diff --git a/web/server/codechecker_server/session_manager.py b/web/server/codechecker_server/session_manager.py index 276af909cd..662eaa62b0 100644 --- a/web/server/codechecker_server/session_manager.py +++ b/web/server/codechecker_server/session_manager.py @@ -11,16 +11,14 @@ import hashlib import json -import os import re -import uuid from datetime import datetime from typing import Optional from codechecker_common.compatibility.multiprocessing import cpu_count from codechecker_common.logger import get_logger -from codechecker_common.util import load_json +from codechecker_common.util import generate_random_token, load_json from codechecker_web.shared.env import check_file_owner_rw from codechecker_web.shared.version import SESSION_COOKIE_NAME as _SCN @@ -47,29 +45,29 @@ SESSION_COOKIE_NAME = _SCN -def generate_session_token(): - """ - Returns a random session token. - """ - return uuid.UUID(bytes=os.urandom(16)).hex - - def get_worker_processes(scfg_dict): """ Return number of worker processes from the config dictionary. - Return 'worker_processes' field from the config dictionary or returns the - default value if this field is not set or the value is negative. + Return 'worker_processes' and 'background_worker_processes' fields from + the config dictionary or returns the default value if this field is not + set or the value is negative. """ default = cpu_count() - worker_processes = scfg_dict.get('worker_processes', default) + worker_processes = scfg_dict.get("worker_processes", default) + background_worker_processes = scfg_dict.get("background_worker_processes", + default) - if worker_processes < 0: + if not worker_processes or worker_processes < 0: LOG.warning("Number of worker processes can not be negative! Default " "value will be used: %s", default) worker_processes = default + if not background_worker_processes or background_worker_processes < 0: + LOG.warning("Number of task worker processes can not be negative! " + "Default value will be used: %s", worker_processes) + background_worker_processes = worker_processes - return worker_processes + return worker_processes, background_worker_processes class _Session: @@ -182,7 +180,8 @@ def __init__(self, configuration_file, root_sha, force_auth=False): # so it should NOT be handled by session_manager. A separate config # handler for the server's stuff should be created, that can properly # instantiate SessionManager with the found configuration. - self.__worker_processes = get_worker_processes(scfg_dict) + self.__worker_processes, self.__background_worker_processes = \ + get_worker_processes(scfg_dict) self.__max_run_count = scfg_dict.get('max_run_count', None) self.__store_config = scfg_dict.get('store', {}) self.__keepalive_config = scfg_dict.get('keepalive', {}) @@ -328,6 +327,10 @@ def is_enabled(self): def worker_processes(self): return self.__worker_processes + @property + def background_worker_processes(self) -> int: + return self.__background_worker_processes + def get_realm(self): return { "realm": self.__auth_config.get('realm_name'), @@ -622,7 +625,7 @@ def create_session(self, auth_string): return False # Generate a new token and create a local session. - token = generate_session_token() + token = generate_random_token(32) user_name = validation.get('username') groups = validation.get('groups', []) is_root = validation.get('root', False) diff --git a/web/server/codechecker_server/task_executors/__init__.py b/web/server/codechecker_server/task_executors/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/web/server/codechecker_server/task_executors/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- diff --git a/web/server/codechecker_server/task_executors/abstract_task.py b/web/server/codechecker_server/task_executors/abstract_task.py new file mode 100644 index 0000000000..34e1cd4d7a --- /dev/null +++ b/web/server/codechecker_server/task_executors/abstract_task.py @@ -0,0 +1,183 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Contains the base class to be inherited and implemented by all background task +types. +""" +import os +import pathlib +import shutil +from typing import Optional + +from codechecker_common.logger import get_logger + +from ..database.config_db_model import BackgroundTask as DBTask + + +LOG = get_logger("server") + + +class TaskCancelHonoured(Exception): + """ + Specialised tag exception raised by `AbstractTask` implementations in a + checkpoint after having checked that their ``cancel_flag`` was set, in + order to terminate task-specific execution and to register the + cancellation's success by the `AbstractTask.execute` method. + + This exception should **NOT** be caught by user code. + """ + + def __init__(self, task_obj: "AbstractTask"): + super().__init__(f"Task '{task_obj.token}' honoured CANCEL request.") + self.task_obj = task_obj + + +class AbstractTask: + """ + Base class implementing common execution and bookkeeping methods to + facilitate the dispatch of tasks to background worker processes. + + Instances of this class **MUST** be marshallable by ``pickle``, as they + are transported over an IPC `Queue`. + It is important that instances do not grow too large, as the underlying + OS-level primitives of a `Queue` can get full, which can result in a + deadlock situation. + + The run-time contents of the instance should only contain the bare minimum + metadata required for the implementation to execute in the background. + + Implementors of subclasses **MAY REASONABLY ASSUME** that an + `AbstractTask` scheduled in the API handler process of a server will be + actually executed by a background worker in the same process group, on the + same machine instance. + """ + + def __init__(self, token: str, data_path: Optional[pathlib.Path]): + self._token = token + self._data_path = data_path + + @property + def token(self) -> str: + """Returns the task's identifying token, its primary ID.""" + return self._token + + @property + def data_path(self) -> Optional[pathlib.Path]: + """ + Returns the filesystem path where the task's input data is prepared. + """ + return self._data_path + + def destroy_data(self): + """ + Deletes the contents of `data_path`. + """ + if not self._data_path: + return + + try: + shutil.rmtree(self._data_path) + except Exception as ex: + LOG.warning("Failed to remove background task's data_dir at " + "'%s':\n%s", self.data_path, str(ex)) + + def _implementation(self, _task_manager: "TaskManager") -> None: + """ + Implemented by subclasses to perform the logic specific to the task. + + Subclasses should use the `task_manager` object, injected from the + context of the executed subprocess, to query and mutate service-level + information about the current task. + """ + raise NotImplementedError() + + def execute(self, task_manager: "TaskManager") -> None: + """ + Executes the `_implementation` of the task, overridden by subclasses, + to perform a task-specific business logic. + + This high-level wrapper deals with capturing `Exception`s, setting + appropriate status information in the database (through the + injected `task_manager`) and logging failures accordingly. + """ + if task_manager.should_cancel(self): + return + + try: + task_manager._mutate_task_record( + self, lambda dbt: dbt.set_running()) + except KeyError: + # KeyError is thrown if a task without a corresponding database + # record is attempted to be executed. + LOG.error("Failed to execute task '%s' due to database exception", + self.token) + except Exception as ex: + LOG.error("Failed to execute task '%s' due to database exception" + "\n%s", + self.token, str(ex)) + # For any other record, try to set the task abandoned due to an + # exception. + try: + task_manager._mutate_task_record( + self, lambda dbt: + dbt.set_abandoned(force_dropped_status=True)) + except Exception: + return + + LOG.debug("Task '%s' running on machine '%s' executor #%d", + self.token, task_manager.machine_id, os.getpid()) + + try: + self._implementation(task_manager) + LOG.debug("Task '%s' finished on machine '%s' executor #%d", + self.token, + task_manager.machine_id, + os.getpid()) + + try: + task_manager._mutate_task_record( + self, lambda dbt: dbt.set_finished(successfully=True)) + except Exception as ex: + LOG.error("Failed to set task '%s' finished due to " + "database exception:\n%s", + self.token, str(ex)) + except TaskCancelHonoured: + def _log_cancel_and_abandon(db_task: DBTask): + db_task.add_comment("CANCEL!\nCancel request of admin " + "honoured by task.", + "SYSTEM[AbstractTask::execute()]") + db_task.set_abandoned(force_dropped_status=False) + + def _log_drop_and_abandon(db_task: DBTask): + db_task.add_comment("SHUTDOWN!\nTask honoured graceful " + "cancel signal generated by " + "server shutdown.", + "SYSTEM[AbstractTask::execute()]") + db_task.set_abandoned(force_dropped_status=True) + + if not task_manager.is_shutting_down: + task_manager._mutate_task_record(self, _log_cancel_and_abandon) + else: + task_manager._mutate_task_record(self, _log_drop_and_abandon) + except Exception as ex: + LOG.error("Failed to execute task '%s' on machine '%s' " + "executor #%d: %s", + self.token, task_manager.machine_id, os.getpid(), + str(ex)) + import traceback + traceback.print_exc() + + def _log_exception_and_fail(db_task: DBTask): + db_task.add_comment( + f"FAILED!\nException during execution:\n{str(ex)}", + "SYSTEM[AbstractTask::execute()]") + db_task.set_finished(successfully=False) + + task_manager._mutate_task_record(self, _log_exception_and_fail) + finally: + self.destroy_data() diff --git a/web/server/codechecker_server/task_executors/main.py b/web/server/codechecker_server/task_executors/main.py new file mode 100644 index 0000000000..dc9dd4e543 --- /dev/null +++ b/web/server/codechecker_server/task_executors/main.py @@ -0,0 +1,142 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Implements a dedicated subprocess that deals with running `AbstractTask` +subclasses in the background. +""" +from datetime import timedelta +import os +from queue import Empty +import signal + +from sqlalchemy.orm import sessionmaker + +from codechecker_common.compatibility.multiprocessing import Queue, Value +from codechecker_common.logger import get_logger, signal_log + +from ..database.config_db_model import BackgroundTask as DBTask +from .abstract_task import AbstractTask +from .task_manager import TaskManager + + +WAIT_TIME_FOR_TASK_QUEUE_CLEARING_AT_SERVER_SHUTDOWN = timedelta(seconds=5) + +LOG = get_logger("server") + + +def executor(queue: Queue, + config_db_sql_server, + server_shutdown_flag: "Value", + machine_id: str): + """ + The "main()" function implementation for a background task executor + process. + + This process sets up the state of the local process, and then deals with + popping jobs from the queue and executing them in the local context. + """ + # First things first, a background worker process should NOT respect the + # termination signals received from the parent process, because it has to + # run its own cleanup logic before shutting down. + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + kill_flag = Value('B', False) + + def executor_hangup_handler(signum: int, _frame): + """ + Handle SIGHUP (1) to do a graceful shutdown of the background worker. + """ + if signum not in [signal.SIGHUP]: + signal_log(LOG, "ERROR", "Signal " + f"<{signal.Signals(signum).name} ({signum})> " + "handling attempted by 'executor_hangup_handler'!") + return + + signal_log(LOG, "DEBUG", f"{os.getpid()}: Received " + f"{signal.Signals(signum).name} ({signum}), preparing for " + "shutdown ...") + kill_flag.value = True + + signal.signal(signal.SIGHUP, executor_hangup_handler) + + config_db_engine = config_db_sql_server.create_engine() + tm = TaskManager(queue, sessionmaker(bind=config_db_engine), kill_flag, + machine_id) + + while not kill_flag.value: + try: + # Do not block indefinitely when waiting for a job, to allow + # checking whether the kill flags were set. + t: AbstractTask = queue.get(block=True, timeout=1) + except Empty: + continue + + import pprint + LOG.info("Executor #%d received task object:\n\n%s:\n%s\n\n", + os.getpid(), t, pprint.pformat(t.__dict__)) + + t.execute(tm) + + # Once the main loop of task execution process has finished, there might + # still be tasks left in the queue. + # If the server is shutting down (this is distinguished from the local kill + # flag, because a 'SIGHUP' might arrive from any source, not just a valid + # graceful shutdown!), then these jobs would be lost if the process just + # exited, with no information reported to the database. + # We need set these tasks to dropped as much as possible. + def _log_shutdown_and_abandon(db_task: DBTask): + db_task.add_comment("SHUTDOWN!\nTask never started due to the " + "server shutdown!", "SYSTEM") + db_task.set_abandoned(force_dropped_status=True) + + def _drop_task_at_shutdown(t: AbstractTask): + try: + LOG.debug("Dropping task '%s' due to server shutdown...", t.token) + tm._mutate_task_record(t, _log_shutdown_and_abandon) + except Exception: + pass + finally: + t.destroy_data() + + if server_shutdown_flag.value: + # Unfortunately, it is not guaranteed which process will wake up first + # when popping objects from the queue. + # Blocking indefinitely would not be a solution here, because all + # producers (API threads) had likely already exited at this point. + # However, simply observing no elements for a short period of time is + # also not enough, as at the very last moments of a server's lifetime, + # one process might observe the queue to be empty, simply because + # another process stole the object that was put into it. + # + # To be on the safe side of things, we require to observe the queue to + # be *constantly* empty over a longer period of repetitive sampling. + empty_sample_count: int = 0 + while empty_sample_count < int( + WAIT_TIME_FOR_TASK_QUEUE_CLEARING_AT_SERVER_SHUTDOWN + .total_seconds()): + try: + t: AbstractTask = queue.get(block=True, timeout=1) + except Empty: + empty_sample_count += 1 + continue + + empty_sample_count = 0 + _drop_task_at_shutdown(t) + + queue.close() + queue.join_thread() + + try: + config_db_engine.dispose() + except Exception as ex: + LOG.error("Failed to shut down task executor!\n%s", str(ex)) + return + + LOG.debug("Task executor subprocess PID %d exited main loop.", + os.getpid()) diff --git a/web/server/codechecker_server/task_executors/task_manager.py b/web/server/codechecker_server/task_executors/task_manager.py new file mode 100644 index 0000000000..ddd3b31053 --- /dev/null +++ b/web/server/codechecker_server/task_executors/task_manager.py @@ -0,0 +1,229 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Contains status management and query methods to handle bookkeeping for +dispatched background tasks. +""" +import os +from pathlib import Path +import tempfile +from typing import Callable, Optional + +import sqlalchemy + +from codechecker_common.compatibility.multiprocessing import Queue, Value +from codechecker_common.logger import get_logger, signal_log +from codechecker_common.util import generate_random_token + +from ..database.config_db_model import BackgroundTask as DBTask, Product +from ..database.database import DBSession + +MAX_TOKEN_RANDOM_RETRIES = 10 + +LOG = get_logger("server") + + +class ExecutorInProgressShutdownError(Exception): + """ + Exception raised to indicate that the background executors are under + shutdown. + """ + def __init__(self): + super().__init__("Task executor is shutting down!") + + +class TaskManager: + """ + Handles the creation of "Task" status objects in the database and pushing + in-memory `AbstractTask` subclass instances to a `Queue`. + + This class is instantiatied for EVERY WORKER separately, and is not a + shared resource! + """ + + def __init__(self, q: Queue, config_db_session_factory, + executor_kill_flag: Value, machine_id: str): + self._queue = q + self._database_factory = config_db_session_factory + self._is_shutting_down = executor_kill_flag + self._machine_id = machine_id + + @property + def machine_id(self) -> str: + """Returns the ``machine_id`` the instance was constructed with.""" + return self._machine_id + + def allocate_task_record(self, kind: str, summary: str, + user_name: Optional[str], + product: Optional[Product] = None) -> str: + """ + Creates the token and the status record for a new task with the given + initial metadata. + + Returns the token of the task, which is a unique identifier of the + allocated record. + """ + try_count: int = 0 + while True: + with DBSession(self._database_factory) as session: + try: + token = generate_random_token(DBTask._token_length) + + task = DBTask(token, kind, summary, self.machine_id, + user_name, product) + session.add(task) + session.commit() + + return token + except sqlalchemy.exc.IntegrityError as ie: + # The only failure that can happen is the PRIMARY KEY's + # UNIQUE violation, which means we hit jackpot by + # generating an already used token! + try_count += 1 + + if try_count >= MAX_TOKEN_RANDOM_RETRIES: + raise KeyError( + "Failed to generate a unique ID for task " + f"{kind} ({summary}) after " + f"{MAX_TOKEN_RANDOM_RETRIES} retries!") from ie + + def create_task_data(self, token: str) -> Path: + """ + Creates a temporary directory which is **NOT** cleaned up + automatically, and suitable for putting arbitrary files underneath + to communicate large inputs (that should not be put in the `Queue`) + to the `execute` method of an `AbstractTask`. + """ + task_tmp_root = Path(tempfile.gettempdir()) / "codechecker_tasks" \ + / self.machine_id + os.makedirs(task_tmp_root, exist_ok=True) + + task_tmp_dir = tempfile.mkdtemp(prefix=f"{token}-") + return Path(task_tmp_dir) + + def _get_task_record(self, task_obj: "AbstractTask") -> DBTask: + """ + Retrieves the `DBTask` for the task identified by `task_obj`. + + This class should not be mutated, only the fields queried. + """ + with DBSession(self._database_factory) as session: + try: + db_task = session.query(DBTask).get(task_obj.token) + session.expunge(db_task) + return db_task + except sqlalchemy.exc.SQLAlchemyError as sql_err: + raise KeyError(f"No task record for token '{task_obj.token}' " + "in the database") from sql_err + + def _mutate_task_record(self, task_obj: "AbstractTask", + mutator: Callable[[DBTask], None]): + """ + Executes the given `mutator` function for the `DBTask` record + corresponding to the `task_obj` description available in memory. + """ + with DBSession(self._database_factory) as session: + try: + db_record = session.query(DBTask).get(task_obj.token) + except sqlalchemy.exc.SQLAlchemyError as sql_err: + raise KeyError(f"No task record for token '{task_obj.token}' " + "in the database") from sql_err + + try: + mutator(db_record) + except Exception: + session.rollback() + + import traceback + traceback.print_exc() + raise + + session.commit() + + def push_task(self, task_obj: "AbstractTask"): + """Enqueues the given `task_obj` onto the `Queue`.""" + if self.is_shutting_down: + raise ExecutorInProgressShutdownError() + + # Note, that the API handler process calling push_task() might be + # killed before writing to the queue, so an actually enqueued task + # (according to the DB) might never be consumed by a background + # process. + # As we have to COMMIT the status change before the actual processing + # in order to show the time stamp to the user(s), there is no better + # way to make this more atomic. + try: + self._mutate_task_record(task_obj, lambda dbt: dbt.set_enqueued()) + self._queue.put(task_obj) + except SystemExit as sex: + try: + signal_log(LOG, "WARNING", f"Process #{os.getpid()}: " + "push_task() killed via SystemExit during " + f"enqueue of task '{task_obj.token}'!") + + def _log_and_abandon(db_task: DBTask): + db_task.add_comment( + "SHUTDOWN!\nEnqueueing process terminated during the " + "ongoing enqueue! The task will never be executed!", + "SYSTEM[TaskManager::push_task()]") + db_task.set_abandoned(force_dropped_status=True) + + self._mutate_task_record(task_obj, _log_and_abandon) + finally: + raise sex + + @property + def is_shutting_down(self) -> bool: + """ + Returns whether the shutdown flag for the executor associated with the + `TaskManager` had been set. + """ + return self._is_shutting_down.value + + def should_cancel(self, task_obj: "AbstractTask") -> bool: + """ + Returns whether the task identified by `task_obj` should be + co-operatively cancelled. + """ + db_task = self._get_task_record(task_obj) + return self.is_shutting_down or \ + (db_task.status in ["enqueued", "running"] + and db_task.cancel_flag) + + def heartbeat(self, task_obj: "AbstractTask"): + """ + Triggers ``heartbeat()`` timestamp update in the database for + `task_obj`. + """ + self._mutate_task_record(task_obj, lambda dbt: dbt.heartbeat()) + + +def drop_all_incomplete_tasks(config_db_session_factory, machine_id: str, + action: str) -> int: + """ + Sets all tasks in the database (reachable via `config_db_session_factory`) + that were associated with the given `machine_id` to ``"dropped"`` status, + indicating that the status was changed during the `action`. + + Returns the number of `DBTask`s actually changed. + """ + count: int = 0 + with DBSession(config_db_session_factory) as session: + for t in session.query(DBTask) \ + .filter(DBTask.machine_id == machine_id, + DBTask.status.in_(["allocated", + "enqueued", + "running"])) \ + .all(): + count += 1 + t.add_comment(f"DROPPED!\n{action}", + "SYSTEM") + t.set_abandoned(force_dropped_status=True) + + session.commit() + return count diff --git a/web/server/codechecker_server/tmp.py b/web/server/codechecker_server/tmp.py deleted file mode 100644 index bbc5e77bea..0000000000 --- a/web/server/codechecker_server/tmp.py +++ /dev/null @@ -1,37 +0,0 @@ -# ------------------------------------------------------------------------- -# -# Part of the CodeChecker project, under the Apache License v2.0 with -# LLVM Exceptions. See LICENSE for license information. -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -# -# ------------------------------------------------------------------------- -""" -Temporary directory module. -""" - - -import datetime -import hashlib -import os - - -from codechecker_common.logger import get_logger - -LOG = get_logger('system') - - -def get_tmp_dir_hash(): - """Generate a hash based on the current time and process id.""" - - pid = os.getpid() - time = datetime.datetime.now() - - data = str(pid) + str(time) - - dir_hash = hashlib.md5() - dir_hash.update(data.encode("utf-8")) - - LOG.debug('The generated temporary directory hash is %s.', - dir_hash.hexdigest()) - - return dir_hash.hexdigest() diff --git a/web/server/config/server_config.json b/web/server/config/server_config.json index e42745f08d..a5ad4999c8 100644 --- a/web/server/config/server_config.json +++ b/web/server/config/server_config.json @@ -1,4 +1,6 @@ { + "background_worker_processes": null, + "worker_processes": null, "max_run_count": null, "store": { "analysis_statistics_dir": null, diff --git a/web/server/vue-cli/package-lock.json b/web/server/vue-cli/package-lock.json index d908b8c278..d0943fd772 100644 --- a/web/server/vue-cli/package-lock.json +++ b/web/server/vue-cli/package-lock.json @@ -11,7 +11,7 @@ "@mdi/font": "^6.5.95", "chart.js": "^2.9.4", "chartjs-plugin-datalabels": "^0.7.0", - "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", "codemirror": "^5.65.0", "date-fns": "^2.28.0", "js-cookie": "^3.0.1", @@ -5113,9 +5113,9 @@ } }, "node_modules/codechecker-api": { - "version": "6.58.0", - "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz", - "integrity": "sha512-N6qK5cnLt32jnJlSyyGMmW6FCzybDljyH1RrGOZ1Gk9n1vV7WluJbC9InYWsZ5lbK7xVyIrphTKXhqC4ARKF6g==", + "version": "6.59.0", + "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", + "integrity": "sha512-BZCBDRjVFS5UerrXsoPNioQppTfrCdDgToHqfFfaQtk6FPVrER42LchfU+cZl254PgWh58H5bLfqdLyFfqntCg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "thrift": "0.13.0-hotfix.1" @@ -21145,8 +21145,8 @@ "dev": true }, "codechecker-api": { - "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz", - "integrity": "sha512-N6qK5cnLt32jnJlSyyGMmW6FCzybDljyH1RrGOZ1Gk9n1vV7WluJbC9InYWsZ5lbK7xVyIrphTKXhqC4ARKF6g==", + "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", + "integrity": "sha512-BZCBDRjVFS5UerrXsoPNioQppTfrCdDgToHqfFfaQtk6FPVrER42LchfU+cZl254PgWh58H5bLfqdLyFfqntCg==", "requires": { "thrift": "0.13.0-hotfix.1" } diff --git a/web/server/vue-cli/package.json b/web/server/vue-cli/package.json index 2239777668..f31789b897 100644 --- a/web/server/vue-cli/package.json +++ b/web/server/vue-cli/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@mdi/font": "^6.5.95", - "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.58.0.tgz", + "codechecker-api": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", "chart.js": "^2.9.4", "chartjs-plugin-datalabels": "^0.7.0", "codemirror": "^5.65.0", diff --git a/web/tests/functional/instance_manager/test_instances.py b/web/tests/functional/instance_manager/test_instances.py index 0e7fc3a1d6..0851548100 100644 --- a/web/tests/functional/instance_manager/test_instances.py +++ b/web/tests/functional/instance_manager/test_instances.py @@ -10,7 +10,6 @@ Instance manager tests. """ - import os import shutil import subprocess @@ -178,7 +177,7 @@ def test_shutdown_record_keeping(self): EVENT_2.set() # Give the server some grace period to react to the kill command. - time.sleep(5) + time.sleep(30) test_cfg = env.import_test_cfg(self._test_workspace) codechecker_1 = test_cfg['codechecker_1'] diff --git a/web/tests/functional/tasks/__init__.py b/web/tests/functional/tasks/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/web/tests/functional/tasks/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- diff --git a/web/tests/functional/tasks/test_task_management.py b/web/tests/functional/tasks/test_task_management.py new file mode 100644 index 0000000000..a53a1e1b4f --- /dev/null +++ b/web/tests/functional/tasks/test_task_management.py @@ -0,0 +1,494 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Contains tests of the ``"/Tasks"`` API endpoint to query, using the +``DummyTask``, normal task management related API functions. +""" +from copy import deepcopy +from datetime import datetime, timezone +import os +import pathlib +import shutil +import unittest +import time +from typing import List, Optional, cast + +import multiprocess + +from codechecker_api_shared.ttypes import RequestFailed, Ternary +from codechecker_api.codeCheckerServersideTasks_v6.ttypes import \ + AdministratorTaskInfo, TaskFilter, TaskInfo, TaskStatus + +from libtest import codechecker, env + + +# Stop events for the CodeChecker servers. +STOP_SERVER = multiprocess.Event() +STOP_SERVER_AUTH = multiprocess.Event() +STOP_SERVER_NO_AUTH = multiprocess.Event() + +TEST_WORKSPACE: Optional[str] = None + + +# Note: Test names in this file follow a strict ordinal convention, because +# the assertions are created with a specific execution history! + +class TaskManagementAPITests(unittest.TestCase): + def setup_class(self): + global TEST_WORKSPACE + TEST_WORKSPACE = env.get_workspace("tasks") + os.environ["TEST_WORKSPACE"] = TEST_WORKSPACE + + codechecker_cfg = { + "check_env": env.test_env(TEST_WORKSPACE), + "workspace": TEST_WORKSPACE, + "checkers": [], + "viewer_host": "localhost", + "viewer_port": env.get_free_port(), + "viewer_product": "tasks", + } + + # Run a normal server that is only used to manage the + # "test_package_product". + codechecker.start_server(codechecker_cfg, STOP_SERVER, + ["--machine-id", "workspace-manager"]) + + codechecker_cfg_no_auth = deepcopy(codechecker_cfg) + codechecker_cfg_no_auth.update({ + "viewer_port": env.get_free_port(), + }) + + # Run a normal server which does not require authentication. + codechecker.start_server(codechecker_cfg_no_auth, STOP_SERVER_NO_AUTH, + ["--machine-id", "unprivileged"]) + + codechecker_cfg_auth = deepcopy(codechecker_cfg) + codechecker_cfg_auth.update({ + "viewer_port": env.get_free_port(), + }) + + # Run a privileged server which does require authentication. + (pathlib.Path(TEST_WORKSPACE) / "root.user").unlink() + env.enable_auth(TEST_WORKSPACE) + codechecker.start_server(codechecker_cfg_auth, STOP_SERVER_AUTH, + ["--machine-id", "privileged"]) + + env.export_test_cfg(TEST_WORKSPACE, + {"codechecker_cfg": codechecker_cfg, + "codechecker_cfg_no_auth": + codechecker_cfg_no_auth, + "codechecker_cfg_auth": codechecker_cfg_auth}) + + codechecker.add_test_package_product(codechecker_cfg, TEST_WORKSPACE) + + def teardown_class(self): + # TODO: If environment variable is set keep the workspace and print + # out the path. + global TEST_WORKSPACE + + STOP_SERVER_NO_AUTH.set() + STOP_SERVER_NO_AUTH.clear() + STOP_SERVER_AUTH.set() + STOP_SERVER_AUTH.clear() + + codechecker.remove_test_package_product(TEST_WORKSPACE) + STOP_SERVER.set() + STOP_SERVER.clear() + + print(f"Removing: {TEST_WORKSPACE}") + shutil.rmtree(cast(str, TEST_WORKSPACE), ignore_errors=True) + + def setup_method(self, _): + test_workspace = os.environ["TEST_WORKSPACE"] + self._test_env = env.import_test_cfg(test_workspace) + + print(f"Running {self.__class__.__name__} tests in {test_workspace}") + + auth_server = self._test_env["codechecker_cfg_auth"] + no_auth_server = self._test_env["codechecker_cfg_no_auth"] + + self._auth_client = env.setup_auth_client(test_workspace, + auth_server["viewer_host"], + auth_server["viewer_port"]) + + root_token = self._auth_client.performLogin("Username:Password", + "root:root") + admin_token = self._auth_client.performLogin("Username:Password", + "admin:admin123") + + self._anonymous_task_client = env.setup_task_client( + test_workspace, + no_auth_server["viewer_host"], no_auth_server["viewer_port"]) + self._admin_task_client = env.setup_task_client( + test_workspace, + auth_server["viewer_host"], auth_server["viewer_port"], + session_token=admin_token) + self._privileged_task_client = env.setup_task_client( + test_workspace, + auth_server["viewer_host"], auth_server["viewer_port"], + session_token=root_token) + + def test_task_1_query_status(self): + task_token = self._anonymous_task_client.createDummyTask(10, False) + + time.sleep(5) + task_info: TaskInfo = self._anonymous_task_client.getTaskInfo( + task_token) + self.assertEqual(task_info.token, task_token) + self.assertEqual(task_info.status, + TaskStatus._NAMES_TO_VALUES["RUNNING"]) + self.assertEqual(task_info.productId, 0) + self.assertIsNone(task_info.actorUsername) + self.assertIn("Dummy task", task_info.summary) + self.assertEqual(task_info.cancelFlagSet, False) + + time.sleep(10) # A bit more than exactly what remains of 10 seconds! + task_info = self._anonymous_task_client.getTaskInfo(task_token) + self.assertEqual(task_info.status, + TaskStatus._NAMES_TO_VALUES["COMPLETED"]) + self.assertEqual(task_info.cancelFlagSet, False) + self.assertIsNotNone(task_info.enqueuedAtEpoch) + self.assertIsNotNone(task_info.startedAtEpoch) + self.assertLessEqual(task_info.enqueuedAtEpoch, + task_info.startedAtEpoch) + self.assertIsNotNone(task_info.completedAtEpoch) + self.assertLess(task_info.startedAtEpoch, task_info.completedAtEpoch) + self.assertEqual(task_info.cancelFlagSet, False) + + def test_task_2_query_status_of_failed(self): + task_token = self._anonymous_task_client.createDummyTask(10, True) + + time.sleep(5) + task_info: TaskInfo = self._anonymous_task_client.getTaskInfo( + task_token) + self.assertEqual(task_info.token, task_token) + self.assertEqual(task_info.status, + TaskStatus._NAMES_TO_VALUES["RUNNING"]) + self.assertEqual(task_info.cancelFlagSet, False) + + time.sleep(10) # A bit more than exactly what remains of 10 seconds! + task_info = self._anonymous_task_client.getTaskInfo(task_token) + self.assertEqual(task_info.status, + TaskStatus._NAMES_TO_VALUES["FAILED"]) + self.assertEqual(task_info.cancelFlagSet, False) + + def test_task_3_cancel(self): + task_token = self._anonymous_task_client.createDummyTask(10, False) + + time.sleep(3) + cancel_req: bool = self._privileged_task_client.cancelTask(task_token) + self.assertTrue(cancel_req) + + time.sleep(3) + cancel_req_2: bool = self._privileged_task_client.cancelTask( + task_token) + # The task was already cancelled, so cancel_req_2 is not the API call + # that cancelled the task. + self.assertFalse(cancel_req_2) + + time.sleep(5) # A bit more than exactly what remains of 10 seconds! + task_info: TaskInfo = self._anonymous_task_client.getTaskInfo( + task_token) + self.assertEqual(task_info.status, + TaskStatus._NAMES_TO_VALUES["CANCELLED"]) + self.assertEqual(task_info.cancelFlagSet, True) + self.assertIn("root", task_info.comments) + self.assertIn("SUPERUSER requested cancellation.", task_info.comments) + self.assertIn("CANCEL!\nCancel request of admin honoured by task.", + task_info.comments) + self.assertIsNotNone(task_info.enqueuedAtEpoch) + self.assertIsNotNone(task_info.startedAtEpoch) + self.assertLessEqual(task_info.enqueuedAtEpoch, + task_info.startedAtEpoch) + self.assertIsNotNone(task_info.completedAtEpoch) + self.assertLess(task_info.startedAtEpoch, task_info.completedAtEpoch) + + def test_task_4_get_tasks_as_admin(self): + with self.assertRaises(RequestFailed): + self._admin_task_client.getTasks(TaskFilter( + # No SUPERUSER rights of test admin. + filterForNoProductID=True + )) + with self.assertRaises(RequestFailed): + self._admin_task_client.getTasks(TaskFilter( + # Default product, no PRODUCT_ADMIN rights of test admin. + productIDs=[1] + )) + with self.assertRaises(RequestFailed): + self._privileged_task_client.getTasks(TaskFilter( + productIDs=[1], + filterForNoProductID=True + )) + with self.assertRaises(RequestFailed): + self._privileged_task_client.getTasks(TaskFilter( + usernames=["foo", "bar"], + filterForNoUsername=True + )) + + # PRODUCT_ADMIN rights on test-specific product... + task_infos: List[AdministratorTaskInfo] = \ + self._admin_task_client.getTasks(TaskFilter(productIDs=[2])) + # ... but no product-specific tasks exist in this test suite. + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter()) + self.assertEqual(len(task_infos), 3) + + self.assertEqual(sum(1 for t in task_infos + if t.normalInfo.status == + TaskStatus._NAMES_TO_VALUES["COMPLETED"]), 1) + self.assertEqual(sum(1 for t in task_infos + if t.normalInfo.status == + TaskStatus._NAMES_TO_VALUES["FAILED"]), 1) + self.assertEqual(sum(1 for t in task_infos + if t.normalInfo.status == + TaskStatus._NAMES_TO_VALUES["CANCELLED"]), 1) + + def test_task_5_info_query_filters(self): + current_time_epoch = int(datetime.now(timezone.utc).timestamp()) + + task_infos: List[AdministratorTaskInfo] = \ + self._privileged_task_client.getTasks(TaskFilter( + machineIDs=["nonexistent"] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + machineIDs=["unprivileged"] + )) + self.assertEqual(len(task_infos), 3) + + tokens_from_previous_test = [t.normalInfo.token for t in task_infos] + + task_infos = self._admin_task_client.getTasks(TaskFilter( + tokens=tokens_from_previous_test + )) + # Admin client is not a SUPERUSER, it should not get the list of + # tasks visible only to superusers because they are "server-level". + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + machineIDs=["privileged"] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + startedBeforeEpoch=current_time_epoch + )) + self.assertEqual(len(task_infos), 3) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + startedAfterEpoch=current_time_epoch + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + cancelFlag=Ternary._NAMES_TO_VALUES["ON"] + )) + self.assertEqual(len(task_infos), 1) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + cancelFlag=Ternary._NAMES_TO_VALUES["OFF"] + )) + self.assertEqual(len(task_infos), 2) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + consumedFlag=Ternary._NAMES_TO_VALUES["ON"] + )) + self.assertEqual(len(task_infos), 3) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + consumedFlag=Ternary._NAMES_TO_VALUES["OFF"] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter()) + + current_time_epoch = int(datetime.now(timezone.utc).timestamp()) + for i in range(10): + target_api = self._anonymous_task_client if i % 2 == 0 \ + else self._admin_task_client + for j in range(10): + target_api.createDummyTask(1, bool(j % 2 == 0)) + + task_infos = self._privileged_task_client.getTasks(TaskFilter()) + self.assertEqual(len(task_infos), 103) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + )) + self.assertEqual(len(task_infos), 100) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + machineIDs=["unprivileged"] + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + machineIDs=["privileged"] + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + filterForNoUsername=True, + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + usernames=["admin"], + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + usernames=["root"], + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch + )) + # Some tasks ought to have started at least. + self.assertGreater(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch + )) + # Some tasks ought to have also finished at least. + self.assertGreater(len(task_infos), 0) + + # Let every task terminate. We should only need 1 second per task, + # running likely in a multithreaded environment. + # Let's have some leeway, though... + time.sleep(2 * (100 * 1 // cast(int, os.cpu_count()))) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch + )) + # All tasks should have finished. + self.assertEqual(len(task_infos), 100) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch, + statuses=[TaskStatus._NAMES_TO_VALUES["COMPLETED"]] + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch, + statuses=[TaskStatus._NAMES_TO_VALUES["FAILED"]] + )) + self.assertEqual(len(task_infos), 50) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch, + cancelFlag=Ternary._NAMES_TO_VALUES["ON"] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch, + consumedFlag=Ternary._NAMES_TO_VALUES["ON"] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + machineIDs=["*privileged"] + )) + self.assertEqual(len(task_infos), 103) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + kinds=["*Dummy*"] + )) + self.assertEqual(len(task_infos), 103) + + # Try to consume the task status from the wrong user! + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedAfterEpoch=current_time_epoch, + completedAfterEpoch=current_time_epoch, + filterForNoUsername=True, + statuses=[TaskStatus._NAMES_TO_VALUES["COMPLETED"]] + )) + self.assertEqual(len(task_infos), 25) + a_token: str = task_infos[0].normalInfo.token + with self.assertRaises(RequestFailed): + self._admin_task_client.getTaskInfo(a_token) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + machineIDs=["workspace-manager"] + )) + self.assertEqual(len(task_infos), 0) + + def test_task_6_dropping(self): + current_time_epoch = int(datetime.now(timezone.utc).timestamp()) + many_task_count = 4 * cast(int, os.cpu_count()) + for _ in range(many_task_count): + self._anonymous_task_client.createDummyTask(600, False) + + STOP_SERVER_NO_AUTH.set() + time.sleep(30) + STOP_SERVER_NO_AUTH.clear() + after_shutdown_time_epoch = int(datetime.now(timezone.utc) + .timestamp()) + + task_infos: List[AdministratorTaskInfo] = \ + self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + statuses=[ + TaskStatus._NAMES_TO_VALUES["ENQUEUED"], + TaskStatus._NAMES_TO_VALUES["RUNNING"], + TaskStatus._NAMES_TO_VALUES["COMPLETED"], + TaskStatus._NAMES_TO_VALUES["FAILED"], + TaskStatus._NAMES_TO_VALUES["CANCELLED"] + ] + )) + self.assertEqual(len(task_infos), 0) + + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + statuses=[TaskStatus._NAMES_TO_VALUES["DROPPED"]], + # System-level dropping is not a "cancellation" action! + cancelFlag=Ternary._NAMES_TO_VALUES["OFF"] + )) + self.assertEqual(len(task_infos), many_task_count) + dropped_task_infos = {ti.normalInfo.token: ti for ti in task_infos} + + # Some tasks will have started, and the server pulled out from under. + task_infos = self._privileged_task_client.getTasks(TaskFilter( + enqueuedAfterEpoch=current_time_epoch, + startedBeforeEpoch=after_shutdown_time_epoch, + statuses=[TaskStatus._NAMES_TO_VALUES["DROPPED"]] + )) + for ti in task_infos: + self.assertIn("SHUTDOWN!\nTask honoured graceful cancel signal " + "generated by server shutdown.", + ti.normalInfo.comments) + del dropped_task_infos[ti.normalInfo.token] + + # The rest could have never started. + for ti in dropped_task_infos.values(): + self.assertTrue("DROPPED!\n" in ti.normalInfo.comments or + "SHUTDOWN!\n" in ti.normalInfo.comments) diff --git a/web/tests/libtest/env.py b/web/tests/libtest/env.py index 1610db8bef..f89e495992 100644 --- a/web/tests/libtest/env.py +++ b/web/tests/libtest/env.py @@ -18,16 +18,18 @@ import socket import stat import subprocess +from typing import cast from codechecker_common.util import load_json -from .thrift_client_to_db import get_auth_client -from .thrift_client_to_db import get_config_client -from .thrift_client_to_db import get_product_client -from .thrift_client_to_db import get_viewer_client +from .thrift_client_to_db import \ + get_auth_client, \ + get_config_client, \ + get_product_client, \ + get_task_client, \ + get_viewer_client -from functional import PKG_ROOT -from functional import REPO_ROOT +from functional import PKG_ROOT, REPO_ROOT def get_free_port(): @@ -236,6 +238,30 @@ def setup_config_client(workspace, session_token=session_token, protocol=proto) +def setup_task_client(workspace, + host=None, port=None, + uri="/Tasks", + auto_handle_connection=True, + session_token=None, + protocol="http"): + if not host and not port: + codechecker_cfg = import_test_cfg(workspace)["codechecker_cfg"] + port = codechecker_cfg["viewer_port"] + host = codechecker_cfg["viewer_host"] + + if session_token is None: + session_token = get_session_token(workspace, host, port) + if session_token == "_PROHIBIT": + session_token = None + + return get_task_client(port=port, + host=cast(str, host), + uri=uri, + auto_handle_connection=auto_handle_connection, + session_token=session_token, + protocol=protocol) + + def repository_root(): return os.path.abspath(os.environ['REPO_ROOT']) diff --git a/web/tests/libtest/thrift_client_to_db.py b/web/tests/libtest/thrift_client_to_db.py index de7788c929..2b5c5a11e8 100644 --- a/web/tests/libtest/thrift_client_to_db.py +++ b/web/tests/libtest/thrift_client_to_db.py @@ -238,6 +238,26 @@ def __getattr__(self, attr): return partial(self._thrift_client_call, attr) +class CCTaskHelper(ThriftAPIHelper): + def __init__(self, proto, host, port, uri, auto_handle_connection=True, + session_token=None): + from codechecker_api.codeCheckerServersideTasks_v6 \ + import codeCheckerServersideTaskService + from codechecker_client.credential_manager import SESSION_COOKIE_NAME + + url = create_product_url(proto, host, port, f"/v{VERSION}{uri}") + transport = THttpClient.THttpClient(url) + protocol = TJSONProtocol.TJSONProtocol(transport) + client = codeCheckerServersideTaskService.Client(protocol) + if session_token: + headers = {'Cookie': f"{SESSION_COOKIE_NAME}={session_token}"} + transport.setCustomHeaders(headers) + super().__init__(transport, client, auto_handle_connection) + + def __getattr__(self, attr): + return partial(self._thrift_client_call, attr) + + def get_all_run_results( client, run_id=None, @@ -303,3 +323,10 @@ def get_config_client(port, host='localhost', uri='/Configuration', return CCConfigHelper(protocol, host, port, uri, auto_handle_connection, session_token) + + +def get_task_client(port, host="localhost", uri="/Tasks", + auto_handle_connection=True, session_token=None, + protocol="http"): + return CCTaskHelper(protocol, host, port, uri, auto_handle_connection, + session_token) From d73c1da9a180ac21682ab9891ac6bf0b36ca0439 Mon Sep 17 00:00:00 2001 From: Whisperity Date: Tue, 20 Aug 2024 15:03:52 +0200 Subject: [PATCH 2/4] feat(cmd): Implemented a CLI for task management This patch extends `CodeChecker cmd` with a new sub-command, `serverside-tasks`, which lets users and administrators deal with querying the status of running server-side tasks. By default, the CLI queries the information of the task(s) specified by their token(s) in the `--token` argument from the server using `getTaskInfo(token)`, and shows this information in either verbose "plain text" (available if precisely **one** task was specified), "table" or JSON formats. In addition to `--token`, it also supports 19 more parameters, each of which correspond to a filter option in the `TaskFilter` API type. If any filters in addition to `--token` is specified, it will exercise `getTasks(filter)` instead. This mode is only available to administrators. The resulting, more detailed information structs are printed in "table" or JSON formats. Apart from querying the current status, two additional flags are available, irrespective of which query method is used to obtain a list of "matching tasks": * `--kill` will call `cancelTask(token)` for each task. * `--await` will block execution until the specified task(s) terminate (in one way or another). `--await` is implemented by calling the new **`await_task_termination`** library function, which is implemented with the goal of being reusable by other clients later. --- codechecker_common/typehints.py | 35 + codechecker_common/util.py | 4 +- docs/web/user_guide.md | 290 +++++++++ web/client/codechecker_client/client.py | 36 +- web/client/codechecker_client/cmd/cmd.py | 271 +++++++- .../codechecker_client/helpers/tasks.py | 49 ++ web/client/codechecker_client/task_client.py | 596 ++++++++++++++++++ .../task_executors/abstract_task.py | 7 + .../codechecker_server/task_executors/main.py | 8 +- 9 files changed, 1282 insertions(+), 14 deletions(-) create mode 100644 codechecker_common/typehints.py create mode 100644 web/client/codechecker_client/helpers/tasks.py create mode 100644 web/client/codechecker_client/task_client.py diff --git a/codechecker_common/typehints.py b/codechecker_common/typehints.py new file mode 100644 index 0000000000..642d5ce0ec --- /dev/null +++ b/codechecker_common/typehints.py @@ -0,0 +1,35 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Type hint (`typing`) extensions. +""" +from typing import Any, Protocol, TypeVar + + +_T_contra = TypeVar("_T_contra", contravariant=True) + + +class LTComparable(Protocol[_T_contra]): + def __lt__(self, other: _T_contra, /) -> bool: ... + + +class LEComparable(Protocol[_T_contra]): + def __le__(self, other: _T_contra, /) -> bool: ... + + +class GTComparable(Protocol[_T_contra]): + def __gt__(self, other: _T_contra, /) -> bool: ... + + +class GEComparable(Protocol[_T_contra]): + def __ge__(self, other: _T_contra, /) -> bool: ... + + +class Orderable(LTComparable[Any], LEComparable[Any], + GTComparable[Any], GEComparable[Any], Protocol): + """Type hint for something that supports rich comparison operators.""" diff --git a/codechecker_common/util.py b/codechecker_common/util.py index c9075d5599..a9e966393d 100644 --- a/codechecker_common/util.py +++ b/codechecker_common/util.py @@ -20,6 +20,8 @@ from codechecker_common.logger import get_logger +from .typehints import Orderable + LOG = get_logger('system') @@ -35,7 +37,7 @@ def arg_match(options, args): return matched_args -def clamp(min_: int, value: int, max_: int) -> int: +def clamp(min_: Orderable, value: Orderable, max_: Orderable) -> Orderable: """Clamps ``value`` such that ``min_ <= value <= max_``.""" if min_ > max_: raise ValueError("min <= max required") diff --git a/docs/web/user_guide.md b/docs/web/user_guide.md index bca75ee459..bda19706a2 100644 --- a/docs/web/user_guide.md +++ b/docs/web/user_guide.md @@ -39,6 +39,7 @@ - [Manage product configuration of a server (`products`)](#manage-product-configuration-of-a-server-products) - [Query authorization settings (`permissions`)](#query-authorization-settings-permissions) - [Authenticate to the server (`login`)](#authenticate-to-the-server-login) + - [Server-side task management (`serverside-tasks`)](#server-side-task-management-serverside-tasks) - [Exporting source code suppression to suppress file](#exporting-source-code-suppression-to-suppress-file) - [Export comments and review statuses (`export`)](#export-comments-and-review-statuses-export) - [Import comments and review statuses into Codechecker (`import`)](#import-comments-and-review-statuses-into-codechecker-import) @@ -1394,6 +1395,295 @@ can be used normally. The password can be saved on the disk. If such "preconfigured" password is not found, the user will be asked, in the command-line, to provide credentials. +#### Server-side task management (`serverside-tasks`) +

    + + $ CodeChecker cmd serverside-tasks --help (click to expand) + + +``` +usage: CodeChecker cmd serverside-tasks [-h] [-t [TOKEN [TOKEN ...]]] + [--await] [--kill] + [--output {plaintext,table,json}] + [--machine-id [MACHINE_ID [MACHINE_ID ...]]] + [--type [TYPE [TYPE ...]]] + [--status [{allocated,enqueued,running,completed,failed,cancelled,dropped} [{allocated,enqueued,running,completed,failed,cancelled,dropped} ...]]] + [--username [USERNAME [USERNAME ...]] + | --no-username] + [--product [PRODUCT [PRODUCT ...]] | + --no-product] + [--enqueued-before TIMESTAMP] + [--enqueued-after TIMESTAMP] + [--started-before TIMESTAMP] + [--started-after TIMESTAMP] + [--finished-before TIMESTAMP] + [--finished-after TIMESTAMP] + [--last-seen-before TIMESTAMP] + [--last-seen-after TIMESTAMP] + [--only-cancelled | --no-cancelled] + [--only-consumed | --no-consumed] + [--url SERVER_URL] + [--verbose {info,debug_analyzer,debug}] + +Query the status of and otherwise filter information for server-side +background tasks executing on a CodeChecker server. In addition, for server +administartors, allows requesting tasks to cancel execution. + +Normally, the querying of a task's status is available only to the following +users: + - The user who caused the creation of the task. + - For tasks that are associated with a specific product, the PRODUCT_ADMIN + users of that product. + - Accounts with SUPERUSER rights (server administrators). + +optional arguments: + -h, --help show this help message and exit + -t [TOKEN [TOKEN ...]], --token [TOKEN [TOKEN ...]] + The identifying token(s) of the task(s) to query. Each + task is associated with a unique token. (default: + None) + --await Instead of querying the status and reporting that, + followed by an exit, block execution of the + 'CodeChecker cmd serverside-tasks' program until the + queried task(s) terminate(s). Makes the CLI's return + code '0' if the task(s) completed successfully, and + non-zero otherwise. If '--kill' is also specified, the + CLI will await the shutdown of the task(s), but will + return '0' if the task(s) were successfully killed as + well. (default: False) + --kill Request the co-operative and graceful termination of + the tasks matching the filter(s) specified. '--kill' + is only available to SUPERUSERs! Note, that this + action only submits a *REQUEST* of termination to the + server, and tasks are free to not support in-progress + kills. Even for tasks that support getting killed, due + to its graceful nature, it might take a considerable + time for the killing to conclude. Killing a task that + has not started RUNNING yet results in it + automatically terminating before it would start. + (default: False) + +output arguments: + --output {plaintext,table,json} + The format of the output to use when showing the + result of the request. (default: plaintext) + +task list filter arguments: + These options can be used to obtain and filter the list of tasks + associated with the 'CodeChecker server' specified by '--url', based on the + various information columns stored for tasks. + + '--token' is usable with the following filters as well. + + Filters with a variable number of options (e.g., '--machine-id A B') will be + in a Boolean OR relation with each other (meaning: machine ID is either "A" + or "B"). + Specifying multiple filters (e.g., '--machine-id A B --username John') will + be considered in a Boolean AND relation (meaning: [machine ID is either "A" or + "B"] and [the task was created by "John"]). + + Listing is only available for the following, privileged users: + - For tasks that are associated with a specific product, the PRODUCT_ADMINs + of that product. + - Server administrators (SUPERUSERs). + + Unprivileged users MUST use only the task's token to query information about + the task. + + + --machine-id [MACHINE_ID [MACHINE_ID ...]] + The IDs of the server instance executing the tasks. + This is an internal identifier set by server + administrators via the 'CodeChecker server' command. + (default: None) + --type [TYPE [TYPE ...]] + The descriptive, but still machine-readable "type" of + the tasks to filter for. (default: None) + --status [{allocated,enqueued,running,completed,failed,cancelled,dropped} [{allocated,enqueued,running,completed,failed,cancelled,dropped} ...]] + The task's execution status(es) in the pipeline. + (default: None) + --username [USERNAME [USERNAME ...]] + The user(s) who executed the action that caused the + tasks' creation. (default: None) + --no-username Filter for tasks without a responsible user that + created them. (default: False) + --product [PRODUCT [PRODUCT ...]] + Filter for tasks that execute in the context of + products specified by the given ENDPOINTs. This query + is only available if you are a PRODUCT_ADMIN of the + specified product(s). (default: None) + --no-product Filter for server-wide tasks (not associated with any + products). This query is only available to SUPERUSERs. + (default: False) + --enqueued-before TIMESTAMP + Filter for tasks that were created BEFORE (or on) the + specified TIMESTAMP, which is given in the format of + 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --enqueued-after TIMESTAMP + Filter for tasks that were created AFTER (or on) the + specified TIMESTAMP, which is given in the format of + 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --started-before TIMESTAMP + Filter for tasks that were started execution BEFORE + (or on) the specified TIMESTAMP, which is given in the + format of 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --started-after TIMESTAMP + Filter for tasks that were started execution AFTER (or + on) the specified TIMESTAMP, which is given in the + format of 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --finished-before TIMESTAMP + Filter for tasks that concluded execution BEFORE (or + on) the specified TIMESTAMP, which is given in the + format of 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --finished-after TIMESTAMP + Filter for tasks that concluded execution execution + AFTER (or on) the specified TIMESTAMP, which is given + in the format of 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --last-seen-before TIMESTAMP + Filter for tasks that reported actual forward progress + in its execution ("heartbeat") BEFORE (or on) the + specified TIMESTAMP, which is given in the format of + 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --last-seen-after TIMESTAMP + Filter for tasks that reported actual forward progress + in its execution ("heartbeat") AFTER (or on) the + specified TIMESTAMP, which is given in the format of + 'year:month:day' or + 'year:month:day:hour:minute:second'. If the "time" + part (':hour:minute:second') is not given, 00:00:00 + (midnight) is assumed instead. Timestamps for tasks + are always understood as Coordinated Universal Time + (UTC). (default: None) + --only-cancelled Show only tasks that received a cancel request from a + SUPERUSER (see '--kill'). (default: False) + --no-cancelled Show only tasks that had not received a cancel request + from a SUPERUSER (see '--kill'). (default: False) + --only-consumed Show only tasks that concluded their execution and the + responsible user (see '--username') "downloaded" this + fact. (default: False) + --no-consumed Show only tasks that concluded their execution but the + responsible user (see '--username') did not "check" on + the task. (default: False) + +common arguments: + --url SERVER_URL The URL of the server to access, in the format of + '[http[s]://]host:port'. (default: localhost:8001) + --verbose {info,debug_analyzer,debug} + Set verbosity level. + +The return code of 'CodeChecker cmd serverside-tasks' is almost always '0', +unless there is an error. +If **EXACTLY** one '--token' is specified in the arguments without the use of +'--await' or '--kill', the return code is based on the current status of the +task, as identified by the token: + - 0: The task completed successfully. + - 1: (Reserved for operational errors.) + - 2: (Reserved for command-line errors.) + - 4: The task failed to complete due to an error during execution. + - 8: The task is still running... + - 16: The task was cancelled by the administrators, or the server was shut + down. +``` +
    + +The `serverside-tasks` subcommand allows users and administrators to query the status of (and for administrators, request the cancellation) of **server-side background tasks**. +These background tasks are created by a limited set of user actions, where the user's client not waiting for the completion of the task can be beneficial. +A task is always identified by its **token**, which is a random generated value. +This token is presented to the user when appropriate. + +##### Querying the status of a single job + +The primary purpose of `CodeChecker cmd serverside-tasks` is to query the status of a running task, with the `--token TOKEN` flag, e.g., `CodeChecker cmd serverside-tasks --token ABCDEF`. +This will return the task's details: + +``` +Task 'ABCDEF': + - Type: TaskService::DummyTask + - Summary: Dummy task for testing purposes + - Status: CANCELLED + - Enqueued at: 2024-08-19 15:55:34 + - Started at: 2024-08-19 15:55:34 + - Last seen: 2024-08-19 15:55:35 + - Completed at: 2024-08-19 15:55:35 + +Comments on task '8b62497c7d1b7e3945445f5b9c3951d97ae07e58f97cad60a0187221e7d1e2ba': +... +``` + +If `--await` is also specified, the execution of `CodeChecker cmd serverside-task` blocks the caller prompt or script until the task terminates on the server. +This is useful in situations where the side effect of a task is needed to be ready before the script may process further instructions. + +A task can have the following statuses: + + * **Allocated**: The task's token was minted, but the complete input to the task has not yet fully processed. + * **Enqueued**: The task is ready for execution, and the system is waiting for free resources to begin running the implementation. + * **Running**: The task is actively executing. + * **Completed**: The task successfully finished executing. (The side effects of the operations are available at this point.) + * **Failed**: The task's execution was started, but failed for some reason. This could be an error detected in the input, a database issue, or any other _Exception_. The "Comments" field of the task, when queried, will likely contain the details of the error. + * **Cancelled**: The task was cancelled by an administrator ([see later](#requesting-the-termination-of-a-task-only-for-SUPERUSERs)) and the task shut down to this request. + * **Dropped**: The task's execution was interrupted due to an external reason (system crash, service shutdown). + +##### Querying multiple tasks via filters + +For product and server administrators (`PRODUCT_ADMIN` and `SUPERUSER` rights), the `serverside-tasks` subcommand exposes various filter options, which can be used to create even a combination of criteria tasks must match to be returned. +Please refer to the `--help` of the subcommand for the exact list of filters available. +In this mode, the statuses of the tasks are printed in a concise table. + +```sh +$ CodeChecker cmd serverside-tasks --enqueued-after 2024:08:19 --status cancelled + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +Token | Machine | Type | Summary | Status | Product | User | Enqueued | Started | Last seen | Completed | Cancelled? +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +8b62497c7d1b7e3945445f5b9c3951d97ae07e58f97cad60a0187221e7d1e2ba | xxxxxxxxxxxxx:8001 | TaskService::DummyTask | Dummy task for testing purposes | CANCELLED | | | 2024-08-19 15:55:34 | 2024-08-19 15:55:34 | 2024-08-19 15:55:35 | 2024-08-19 15:55:35 | Yes +6fa0097a9bd1799572c7ccd2afc0272684ed036c11145da7eaf40cc8a07c7241 | xxxxxxxxxxxxx:8001 | TaskService::DummyTask | Dummy task for testing purposes | CANCELLED | | | 2024-08-19 15:55:53 | 2024-08-19 15:55:53 | 2024-08-19 15:55:53 | 2024-08-19 15:55:53 | Yes +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +``` + +##### Requesting the termination of a task (only for `SUPERUSER`s) + +Tasks matching the query filters can be requested for termination ("killed") by specifying `--kill` in addition to the filters. +This will send a request to the server to shut the tasks down. + +**Note**, that this shutdown is not deterministic and is not immediate. +Due to technical reasons, it is up for the task's implementation to find the appropriate position to honour the shutdown request. +Depending on the task's semantics, the input, or simply circumstance, a task may completely ignore the shutdown request and decide to nevertheless complete. + ### Exporting source code suppression to suppress file diff --git a/web/client/codechecker_client/client.py b/web/client/codechecker_client/client.py index 0bbe2f69fe..570d7e28dc 100644 --- a/web/client/codechecker_client/client.py +++ b/web/client/codechecker_client/client.py @@ -23,10 +23,11 @@ from codechecker_web.shared import env from codechecker_web.shared.version import CLIENT_API -from codechecker_client.helpers.authentication import ThriftAuthHelper -from codechecker_client.helpers.product import ThriftProductHelper -from codechecker_client.helpers.results import ThriftResultsHelper from .credential_manager import UserCredentials +from .helpers.authentication import ThriftAuthHelper +from .helpers.product import ThriftProductHelper +from .helpers.results import ThriftResultsHelper +from .helpers.tasks import ThriftServersideTaskHelper from .product import split_product_url LOG = get_logger('system') @@ -65,7 +66,7 @@ def setup_auth_client(protocol, host, port, session_token=None): session token for the session. """ client = ThriftAuthHelper(protocol, host, port, - '/v' + CLIENT_API + '/Authentication', + f"/v{CLIENT_API}/Authentication", session_token) return client @@ -78,7 +79,7 @@ def login_user(protocol, host, port, username, login=False): """ session = UserCredentials() auth_client = ThriftAuthHelper(protocol, host, port, - '/v' + CLIENT_API + '/Authentication') + f"/v{CLIENT_API}/Authentication") if not login: logout_done = auth_client.destroySession() @@ -213,7 +214,7 @@ def setup_product_client(protocol, host, port, auth_client=None, # as "viewpoint" from which the product service is called. product_client = ThriftProductHelper( protocol, host, port, - '/' + product_name + '/v' + CLIENT_API + '/Products', + f"/{product_name}/v{CLIENT_API}/Products", session_token, lambda: get_new_token(protocol, host, port, cred_manager)) @@ -263,3 +264,26 @@ def setup_client(product_url) -> ThriftResultsHelper: f"/{product_name}/v{CLIENT_API}/CodeCheckerService", session_token, lambda: get_new_token(protocol, host, port, cred_manager)) + + +def setup_task_client(protocol, host, port, auth_client=None, + session_token=None): + """ + Setup the Thrift client for the server-side task management endpoint. + """ + cred_manager = UserCredentials() + session_token = cred_manager.get_token(host, port) + + if not session_token: + auth_client = setup_auth_client(protocol, host, port) + session_token = perform_auth_for_handler(auth_client, host, port, + cred_manager) + + # Attach to the server-wide task management service. + task_client = ThriftServersideTaskHelper( + protocol, host, port, + f"/v{CLIENT_API}/Tasks", + session_token, + lambda: get_new_token(protocol, host, port, cred_manager)) + + return task_client diff --git a/web/client/codechecker_client/cmd/cmd.py b/web/client/codechecker_client/cmd/cmd.py index 6be5de36b6..f68b8a148e 100644 --- a/web/client/codechecker_client/cmd/cmd.py +++ b/web/client/codechecker_client/cmd/cmd.py @@ -14,13 +14,17 @@ import argparse import getpass import datetime +import os import sys from codechecker_api.codeCheckerDBAccess_v6 import ttypes -from codechecker_client import cmd_line_client -from codechecker_client import product_client -from codechecker_client import permission_client, source_component_client, \ +from codechecker_client import \ + cmd_line_client, \ + permission_client, \ + product_client, \ + source_component_client, \ + task_client, \ token_client from codechecker_common import arg, logger, util @@ -1227,6 +1231,231 @@ def __register_permissions(parser): help="The output format to use in showing the data.") +def __register_tasks(parser): + """ + Add `argparse` subcommand `parser` options for the "handle server-side + tasks" action. + """ + if "TEST_WORKSPACE" in os.environ: + testing_args = parser.add_argument_group("testing arguments") + testing_args.add_argument("--create-dummy-task", + dest="dummy_task_args", + metavar="ARG", + default=argparse.SUPPRESS, + type=str, + nargs=2, + help=""" +Exercises the 'createDummyTask(int timeout, bool shouldFail)' API endpoint. +Used for testing purposes. +Note, that the server **MUST** be started in a testing environment as well, +otherwise, the request will be rejected by the server! +""") + + parser.add_argument("-t", "--token", + dest="token", + metavar="TOKEN", + type=str, + nargs='*', + help="The identifying token(s) of the task(s) to " + "query. Each task is associated with a unique " + "token.") + + parser.add_argument("--await", + dest="wait_and_block", + action="store_true", + help=""" +Instead of querying the status and reporting that, followed by an exit, block +execution of the 'CodeChecker cmd serverside-tasks' program until the queried +task(s) terminate(s). +Makes the CLI's return code '0' if the task(s) completed successfully, and +non-zero otherwise. +If '--kill' is also specified, the CLI will await the shutdown of the task(s), +but will return '0' if the task(s) were successfully killed as well. +""") + + parser.add_argument("--kill", + dest="cancel_task", + action="store_true", + help=""" +Request the co-operative and graceful termination of the tasks matching the +filter(s) specified. +'--kill' is only available to SUPERUSERs! +Note, that this action only submits a *REQUEST* of termination to the server, +and tasks are free to not support in-progress kills. +Even for tasks that support getting killed, due to its graceful nature, it +might take a considerable time for the killing to conclude. +Killing a task that has not started RUNNING yet results in it automatically +terminating before it would start. +""") + + output = parser.add_argument_group("output arguments") + output.add_argument("--output", + dest="output_format", + required=False, + default="plaintext", + choices=["plaintext", "table", "json"], + help="The format of the output to use when showing " + "the result of the request.") + + task_list = parser.add_argument_group( + "task list filter arguments", + """These options can be used to obtain and filter the list of tasks +associated with the 'CodeChecker server' specified by '--url', based on the +various information columns stored for tasks. + +'--token' is usable with the following filters as well. + +Filters with a variable number of options (e.g., '--machine-id A B') will be +in a Boolean OR relation with each other (meaning: machine ID is either "A" +or "B"). +Specifying multiple filters (e.g., '--machine-id A B --username John') will +be considered in a Boolean AND relation (meaning: [machine ID is either "A" or +"B"] and [the task was created by "John"]). + +Listing is only available for the following, privileged users: + - For tasks that are associated with a specific product, the PRODUCT_ADMINs + of that product. + - Server administrators (SUPERUSERs). + +Unprivileged users MUST use only the task's token to query information about +the task. + """) + + task_list.add_argument("--machine-id", + type=str, + nargs='*', + help="The IDs of the server instance executing " + "the tasks. This is an internal identifier " + "set by server administrators via the " + "'CodeChecker server' command.") + + task_list.add_argument("--type", + type=str, + nargs='*', + help="The descriptive, but still " + "machine-readable \"type\" of the tasks to " + "filter for.") + + task_list.add_argument("--status", + type=str, + nargs='*', + choices=["allocated", "enqueued", "running", + "completed", "failed", "cancelled", + "dropped"], + help="The task's execution status(es) in the " + "pipeline.") + + username = task_list.add_mutually_exclusive_group(required=False) + username.add_argument("--username", + type=str, + nargs='*', + help="The user(s) who executed the action that " + "caused the tasks' creation.") + username.add_argument("--no-username", + action="store_true", + help="Filter for tasks without a responsible user " + "that created them.") + + product = task_list.add_mutually_exclusive_group(required=False) + product.add_argument("--product", + type=str, + nargs='*', + help="Filter for tasks that execute in the context " + "of products specified by the given ENDPOINTs. " + "This query is only available if you are a " + "PRODUCT_ADMIN of the specified product(s).") + product.add_argument("--no-product", + action="store_true", + help="Filter for server-wide tasks (not associated " + "with any products). This query is only " + "available to SUPERUSERs.") + + timestamp_documentation: str = """ +TIMESTAMP, which is given in the format of 'year:month:day' or +'year:month:day:hour:minute:second'. +If the "time" part (':hour:minute:second') is not given, 00:00:00 (midnight) +is assumed instead. +Timestamps for tasks are always understood as Coordinated Universal Time (UTC). +""" + + task_list.add_argument("--enqueued-before", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that were created BEFORE " + "(or on) the specified " + + timestamp_documentation) + task_list.add_argument("--enqueued-after", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that were created AFTER " + "(or on) the specified " + + timestamp_documentation) + + task_list.add_argument("--started-before", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that were started " + "execution BEFORE (or on) the specified " + + timestamp_documentation) + task_list.add_argument("--started-after", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that were started " + "execution AFTER (or on) the specified " + + timestamp_documentation) + + task_list.add_argument("--finished-before", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that concluded execution " + "BEFORE (or on) the specified " + + timestamp_documentation) + task_list.add_argument("--finished-after", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that concluded execution " + "execution AFTER (or on) the specified " + + timestamp_documentation) + + task_list.add_argument("--last-seen-before", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that reported actual " + "forward progress in its execution " + "(\"heartbeat\") BEFORE (or on) the " + "specified " + timestamp_documentation) + task_list.add_argument("--last-seen-after", + type=valid_time, + metavar="TIMESTAMP", + help="Filter for tasks that reported actual " + "forward progress in its execution " + "(\"heartbeat\") AFTER (or on) the " + "specified " + timestamp_documentation) + + cancel = task_list.add_mutually_exclusive_group(required=False) + cancel.add_argument("--only-cancelled", + action="store_true", + help="Show only tasks that received a cancel request " + "from a SUPERUSER (see '--kill').") + cancel.add_argument("--no-cancelled", + action="store_true", + help="Show only tasks that had not received a " + "cancel request from a SUPERUSER " + "(see '--kill').") + + consumed = task_list.add_mutually_exclusive_group(required=False) + consumed.add_argument("--only-consumed", + action="store_true", + help="Show only tasks that concluded their " + "execution and the responsible user (see " + "'--username') \"downloaded\" this fact.") + consumed.add_argument("--no-consumed", + action="store_true", + help="Show only tasks that concluded their " + "execution but the responsible user (see " + "'--username') did not \"check\" on the task.") + + def __register_token(parser): """ Add argparse subcommand parser for the "handle token" action. @@ -1538,5 +1767,41 @@ def add_arguments_to_parser(parser): permissions.set_defaults(func=permission_client.handle_permissions) __add_common_arguments(permissions, needs_product_url=False) + tasks = subcommands.add_parser( + "serverside-tasks", + formatter_class=arg.RawDescriptionDefaultHelpFormatter, + description=""" +Query the status of and otherwise filter information for server-side +background tasks executing on a CodeChecker server. In addition, for server +administartors, allows requesting tasks to cancel execution. + +Normally, the querying of a task's status is available only to the following +users: + - The user who caused the creation of the task. + - For tasks that are associated with a specific product, the PRODUCT_ADMIN + users of that product. + - Accounts with SUPERUSER rights (server administrators). +""", + help="Await, query, and cancel background tasks executing on the " + "server.", + epilog=""" +The return code of 'CodeChecker cmd serverside-tasks' is almost always '0', +unless there is an error. +If **EXACTLY** one '--token' is specified in the arguments without the use of +'--await' or '--kill', the return code is based on the current status of the +task, as identified by the token: + - 0: The task completed successfully. + - 1: (Reserved for operational errors.) + - 2: (Reserved for command-line errors.) + - 4: The task failed to complete due to an error during execution. + - 8: The task is still running... + - 16: The task was cancelled by the administrators, or the server was shut + down. +""" + ) + __register_tasks(tasks) + tasks.set_defaults(func=task_client.handle_tasks) + __add_common_arguments(tasks, needs_product_url=False) + # 'cmd' does not have a main() method in itself, as individual subcommands are # handled later on separately. diff --git a/web/client/codechecker_client/helpers/tasks.py b/web/client/codechecker_client/helpers/tasks.py new file mode 100644 index 0000000000..026b2665fa --- /dev/null +++ b/web/client/codechecker_client/helpers/tasks.py @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Helper for the "serverside tasks" Thrift API. +""" +from typing import Callable, List, Optional + +from codechecker_api.codeCheckerServersideTasks_v6 import \ + codeCheckerServersideTaskService +from codechecker_api.codeCheckerServersideTasks_v6.ttypes import \ + AdministratorTaskInfo, TaskFilter, TaskInfo + +from ..thrift_call import thrift_client_call +from .base import BaseClientHelper + + +# These names are inherited from Thrift stubs. +# pylint: disable=invalid-name +class ThriftServersideTaskHelper(BaseClientHelper): + """Clientside Thrift stub for the `codeCheckerServersideTaskService`.""" + + def __init__(self, protocol: str, host: str, port: int, uri: str, + session_token: Optional[str] = None, + get_new_token: Optional[Callable] = None): + super().__init__(protocol, host, port, uri, + session_token, get_new_token) + + self.client = codeCheckerServersideTaskService.Client(self.protocol) + + @thrift_client_call + def getTaskInfo(self, _token: str) -> TaskInfo: + raise NotImplementedError("Should have called Thrift code!") + + @thrift_client_call + def getTasks(self, _filters: TaskFilter) -> List[AdministratorTaskInfo]: + raise NotImplementedError("Should have called Thrift code!") + + @thrift_client_call + def cancelTask(self, _token: str) -> bool: + raise NotImplementedError("Should have called Thrift code!") + + @thrift_client_call + def createDummyTask(self, _timeout: int, _should_fail: bool) -> str: + raise NotImplementedError("Should have called Thrift code!") diff --git a/web/client/codechecker_client/task_client.py b/web/client/codechecker_client/task_client.py new file mode 100644 index 0000000000..f6666923ef --- /dev/null +++ b/web/client/codechecker_client/task_client.py @@ -0,0 +1,596 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +Implementation for the ``CodeChecker cmd serverside-tasks`` subcommand. +""" +from argparse import Namespace +from copy import deepcopy +from datetime import datetime, timedelta, timezone +import json +import os +import sys +import time +from typing import Callable, Dict, List, Optional, Tuple, cast + +from codechecker_api_shared.ttypes import Ternary +from codechecker_api.ProductManagement_v6.ttypes import Product +from codechecker_api.codeCheckerServersideTasks_v6.ttypes import \ + AdministratorTaskInfo, TaskFilter, TaskInfo, TaskStatus + +from codechecker_common import logger +from codechecker_common.util import clamp +from codechecker_report_converter import twodim + +from .client import setup_product_client, setup_task_client +from .helpers.product import ThriftProductHelper +from .helpers.tasks import ThriftServersideTaskHelper +from .product import split_server_url + + +# Needs to be set in the handler functions. +LOG: Optional[logger.logging.Logger] = None + + +def init_logger(level, stream=None, logger_name="system"): + logger.setup_logger(level, stream) + + global LOG + LOG = logger.get_logger(logger_name) + + +class TaskTimeoutError(Exception): + """Indicates that `await_task_termination` timed out.""" + + def __init__(self, token: str, task_status: int, delta: timedelta): + super().__init__(f"Task '{token}' is still " + f"'{TaskStatus._VALUES_TO_NAMES[task_status]}', " + f"but did not have any progress for '{delta}' " + f"({delta.total_seconds()} seconds)!") + + +def await_task_termination( + log: logger.logging.Logger, + token: str, + probe_delta_min: timedelta = timedelta(seconds=5), + probe_delta_max: timedelta = timedelta(minutes=2), + timeout_from_last_task_progress: Optional[timedelta] = timedelta(hours=1), + max_consecutive_request_failures: Optional[int] = 10, + task_api_client: Optional[ThriftServersideTaskHelper] = None, + server_address: Optional[Tuple[str, str, str]] = None, +) -> str: + """ + Blocks the execution of the current process until the task specified by + `token` terminates. + When terminated, returns the task's `TaskStatus` as a string. + + `await_task_termination` sleeps the current process (as with + `time.sleep`), and periodically wakes up, with a distance of wake-ups + calculated between `probe_delta_min` and `probe_delta_max`, to check the + status of the task by downloading ``getTaskInfo()`` result from the + server. + + The server to use is specified by either providing a valid + `task_api_client`, at which point the connection of the existing client + will be reused; or by providing a + ``(protocol: str, host: str, port: int)`` tuple under `server_address`, + which will cause `await_task_termination` to set the Task API client up + internally. + + This call blocks the caller stack frame indefinitely, unless + `timeout_from_last_task_progress` is specified. + If so, the function will unblock by raising `TaskTimeoutError` if the + specified time has elapsed since the queried task last exhibited forward + progress. + Forward progress is calculated from the task's ``startedAt`` timestamp, + the ``completedAt`` timestamp, or the ``lastHeartbeat``, whichever is + later in time. + For tasks that had not started executing yet (their ``startedAt`` is + `None`), this timeout does not apply. + + This function is resillient against network problems and request failures + through the connection to the server, if + `max_consecutive_request_failures` is specified. + If so, it will wait the given number of Thrift client failures before + giving up. + """ + if not task_api_client and not server_address: + raise ValueError("Specify 'task_api_client' or 'server_address' " + "to point the function at a server to probe!") + if not task_api_client and server_address: + protocol, host, port = server_address + task_api_client = setup_task_client(protocol, host, port) + if not task_api_client: + raise ConnectionError("Failed to set up Task API client!") + + probe_distance: timedelta = deepcopy(probe_delta_min) + request_failures: int = 0 + last_forward_progress_by_task: Optional[datetime] = None + task_status: int = TaskStatus.ALLOCATED + + def _query_task_status(): + while True: + nonlocal request_failures + try: + ti = task_api_client.getTaskInfo(token) + request_failures = 0 + break + except SystemExit: + # getTaskInfo() is decorated by @thrift_client_call, which + # raises SystemExit by calling sys.exit() internally, if + # something fails. + request_failures += 1 + if max_consecutive_request_failures and request_failures > \ + max_consecutive_request_failures: + raise + log.info("Retrying task status query [%d / %d retries] ...", + request_failures, max_consecutive_request_failures) + + last_forward_progress_by_task: Optional[datetime] = None + epoch_to_consider: int = 0 + if ti.completedAtEpoch: + epoch_to_consider = ti.completedAtEpoch + elif ti.lastHeartbeatEpoch: + epoch_to_consider = ti.lastHeartbeatEpoch + elif ti.startedAtEpoch: + epoch_to_consider = ti.startedAtEpoch + if epoch_to_consider: + last_forward_progress_by_task = cast( + datetime, _utc_epoch_to_datetime(epoch_to_consider)) + + task_status = cast(int, ti.status) + + return last_forward_progress_by_task, task_status + + while True: + task_forward_progressed_at, task_status = _query_task_status() + if task_status in [TaskStatus.COMPLETED, TaskStatus.FAILED, + TaskStatus.CANCELLED, TaskStatus.DROPPED]: + break + + if task_forward_progressed_at: + time_since_last_progress = datetime.now(timezone.utc) \ + - task_forward_progressed_at + if timeout_from_last_task_progress and \ + time_since_last_progress >= \ + timeout_from_last_task_progress: + log.error("'%s' timeout elapsed since task last progressed " + "at '%s', considering " + "hung/locked out/lost/failed...", + timeout_from_last_task_progress, + task_forward_progressed_at) + raise TaskTimeoutError(token, task_status, + time_since_last_progress) + + if last_forward_progress_by_task: + # Tune the next probe's wait period in a fashion similar to + # TCP's low-level AIMD (addition increment, + # multiplicative decrement) algorithm. + time_between_last_two_progresses = \ + last_forward_progress_by_task - task_forward_progressed_at + if not time_between_last_two_progresses: + # No progress since the last probe, increase the timeout + # until the next probe, and hope that some progress will + # have been made by that time. + probe_distance += timedelta(seconds=1) + elif time_between_last_two_progresses <= 2 * probe_distance: + # time_between_last_two_progresses is always at least + # probe_distance, because it is the distance between two + # queried and observed forward progress measurements. + # However, if they are "close enough" to each other, it + # means that the server is progressing well with the task + # and it is likely that the task might be finished "soon". + # + # In this case, it is beneficial to INCREASE the probing + # frequency, in order not to make the user wait "too much" + # before observing a "likely" soon available success. + probe_distance /= 2 + else: + # If the progresses detected from the server are + # "far apart", it can indicate that the server is busy + # with processing the task. + # + # In this case, DECREASING the frequency if beneficial, + # because it is "likely" that a final result will not + # arrive soon, and keeping the current frequency would + # just keep "flooding" the server with queries that do + # not return a meaningfully different result. + probe_distance += timedelta(seconds=1) + else: + # If the forward progress has not been observed yet at all, + # increase the timeout until the next probe, and hope that + # some progress will have been made by that time. + probe_distance += timedelta(seconds=1) + + # At any rate, always keep the probe_distance between the + # requested limits. + probe_distance = \ + clamp(probe_delta_min, probe_distance, probe_delta_max) + + last_forward_progress_by_task = task_forward_progressed_at + + log.debug("Waiting %f seconds (%s) before querying the server...", + probe_distance.total_seconds(), probe_distance) + time.sleep(probe_distance.total_seconds()) + + return TaskStatus._VALUES_TO_NAMES[task_status] + + +def _datetime_to_utc_epoch(d: Optional[datetime]) -> Optional[int]: + return int(d.replace(tzinfo=timezone.utc).timestamp()) if d else None + + +def _utc_epoch_to_datetime(s: Optional[int]) -> Optional[datetime]: + return datetime.fromtimestamp(s, timezone.utc) if s else None + + +def _datetime_to_str(d: Optional[datetime]) -> Optional[str]: + return d.strftime("%Y-%m-%d %H:%M:%S") if d else None + + +def _build_filter(args: Namespace, + product_id_to_endpoint: Dict[int, str], + get_product_api: Callable[[], ThriftProductHelper]) \ + -> Optional[TaskFilter]: + """Build a `TaskFilter` from the command-line `args`.""" + filter_: Optional[TaskFilter] = None + + def get_filter() -> TaskFilter: + nonlocal filter_ + if not filter_: + filter_ = TaskFilter() + return filter_ + + if args.machine_id: + get_filter().machineIDs = args.machine_id + if args.type: + get_filter().kinds = args.type + if args.status: + get_filter().statuses = [TaskStatus._NAMES_TO_VALUES[s.upper()] + for s in args.status] + if args.username: + get_filter().usernames = args.username + elif args.no_username: + get_filter().filterForNoUsername = True + if args.product: + # Users specify products via ENDPOINTs for U.X. friendliness, but the + # API works with product IDs. + def _get_product_id_or_log(endpoint: str) -> Optional[int]: + try: + products: List[Product] = cast( + List[Product], + get_product_api().getProducts(endpoint, None)) + # Endpoints substring-match. + product = next(p for p in products if p.endpoint == endpoint) + p_id = cast(int, product.id) + product_id_to_endpoint[p_id] = endpoint + return p_id + except StopIteration: + LOG.warning("No product with endpoint '%s', omitting it from " + "the query.", + endpoint) + return None + + get_filter().productIDs = list(filter(lambda i: i is not None, + map(_get_product_id_or_log, + args.product))) + elif args.no_product: + get_filter().filterForNoProductID = True + if args.enqueued_before: + get_filter().enqueuedBeforeEpoch = _datetime_to_utc_epoch( + args.enqueued_before) + if args.enqueued_after: + get_filter().enqueuedAfterEpoch = _datetime_to_utc_epoch( + args.enqueued_after) + if args.started_before: + get_filter().startedBeforeEpoch = _datetime_to_utc_epoch( + args.started_before) + if args.started_after: + get_filter().startedAfterEpoch = _datetime_to_utc_epoch( + args.started_after) + if args.finished_before: + get_filter().completedBeforeEpoch = _datetime_to_utc_epoch( + args.finished_before) + if args.finished_after: + get_filter().completedAfterEpoch = _datetime_to_utc_epoch( + args.finished_after) + if args.last_seen_before: + get_filter().heartbeatBeforeEpoch = _datetime_to_utc_epoch( + args.started_before) + if args.last_seen_after: + get_filter().heartbeatAfterEpoch = _datetime_to_utc_epoch( + args.started_after) + if args.only_cancelled: + get_filter().cancelFlag = Ternary._NAMES_TO_VALUES["ON"] + elif args.no_cancelled: + get_filter().cancelFlag = Ternary._NAMES_TO_VALUES["OFF"] + if args.only_consumed: + get_filter().consumedFlag = Ternary._NAMES_TO_VALUES["ON"] + elif args.no_consumed: + get_filter().consumedFlag = Ternary._NAMES_TO_VALUES["OFF"] + + return filter_ + + +def _unapi_info(ti: TaskInfo) -> dict: + """ + Converts a `TaskInfo` API structure into a flat Pythonic `dict` of + non-API types. + """ + return {**{k: v + for k, v in ti.__dict__.items() + if k != "status" and not k.endswith("Epoch")}, + **{k.replace("Epoch", "", 1): + _datetime_to_str(_utc_epoch_to_datetime(v)) + for k, v in ti.__dict__.items() + if k.endswith("Epoch")}, + **{"status": TaskStatus._VALUES_TO_NAMES[cast(int, ti.status)]}, + } + + +def _unapi_admin_info(ati: AdministratorTaskInfo) -> dict: + """ + Converts a `AdministratorTaskInfo` API structure into a flat Pythonic + `dict` of non-API types. + """ + return {**{k: v + for k, v in ati.__dict__.items() + if k != "normalInfo"}, + **_unapi_info(cast(TaskInfo, ati.normalInfo)), + } + + +def _transform_product_ids_to_endpoints( + task_infos: List[dict], + product_id_to_endpoint: Dict[int, str], + get_product_api: Callable[[], ThriftProductHelper] +): + """Replace ``task_infos[N]["productId"]`` with + ``task_infos[N]["productEndpoint"]`` for all elements. + """ + for ti in task_infos: + try: + ti["productEndpoint"] = \ + product_id_to_endpoint[ti["productId"]] \ + if ti["productId"] != 0 else None + except KeyError: + # Take the slow path, and get the ID->Endpoint map from the server. + product_id_to_endpoint = { + product.id: product.endpoint + for product + in get_product_api().getProducts(None, None)} + del ti["productId"] + + +def handle_tasks(args: Namespace) -> int: + """Main method for the ``CodeChecker cmd serverside-tasks`` subcommand.""" + # If the given output format is not `table`, redirect the logger's output + # to standard error. + init_logger(args.verbose if "verbose" in args else None, + "stderr" if "output_format" in args + and args.output_format != "table" + else None) + + rc: int = 0 + protocol, host, port = split_server_url(args.server_url) + api = setup_task_client(protocol, host, port) + + if "TEST_WORKSPACE" in os.environ and "dummy_task_args" in args: + timeout, should_fail = \ + int(args.dummy_task_args[0]), \ + args.dummy_task_args[1].lower() in ["y", "yes", "true", "1", "on"] + + dummy_task_token = api.createDummyTask(timeout, should_fail) + LOG.info("Dummy task created with token '%s'.", dummy_task_token) + if not args.token: + args.token = [dummy_task_token] + else: + args.token.append(dummy_task_token) + + # Lazily initialise a Product manager API client as well, it can be needed + # if products are being put into a request filter, or product-specific + # tasks appear on the output. + product_api: Optional[ThriftProductHelper] = None + product_id_to_endpoint: Dict[int, str] = {} + + def get_product_api() -> ThriftProductHelper: + nonlocal product_api + if not product_api: + product_api = setup_product_client(protocol, host, port) + return product_api + + tokens_of_tasks: List[str] = [] + task_filter = _build_filter(args, + product_id_to_endpoint, + get_product_api) + if task_filter: + # If the "filtering" API must be used, the args.token list should also + # be part of the filter. + task_filter.tokens = args.token + + admin_task_infos: List[AdministratorTaskInfo] = \ + api.getTasks(task_filter) + + # Save the tokens of matched tasks for later, in case we have to do + # some further processing. + if args.cancel_task or args.wait_and_block: + tokens_of_tasks = [cast(str, ti.normalInfo.token) + for ti in admin_task_infos] + + task_info_for_print = list(map(_unapi_admin_info, + admin_task_infos)) + _transform_product_ids_to_endpoints(task_info_for_print, + product_id_to_endpoint, + get_product_api) + + if args.output_format == "json": + print(json.dumps(task_info_for_print)) + else: + if args.output_format == "plaintext": + # For the listing of the tasks, the "table" format is more + # appropriate, so we intelligently switch over to that. + args.output_format = "table" + + headers = ["Token", "Machine", "Type", "Summary", "Status", + "Product", "User", "Enqueued", "Started", "Last seen", + "Completed", "Cancelled?"] + rows = [] + for ti in task_info_for_print: + rows.append((ti["token"], + ti["machineId"], + ti["taskKind"], + ti["summary"], + ti["status"], + ti["productEndpoint"] or "", + ti["actorUsername"] or "", + ti["enqueuedAt"] or "", + ti["startedAt"] or "", + ti["lastHeartbeat"] or "", + ti["completedAt"] or "", + "Yes" if ti["cancelFlagSet"] else "", + )) + print(twodim.to_str(args.output_format, headers, rows)) + else: + # If the filtering API was not used, we need to query the tasks + # directly, based on their token. + if not args.token: + LOG.error("ERROR! To use 'CodeChecker cmd serverside-tasks', " + "a '--token' list or some other filter criteria " + "**MUST** be specified!") + sys.exit(2) # Simulate argparse error code. + + # Otherwise, query the tasks, and print their info. + task_infos: List[TaskInfo] = [api.getTaskInfo(token) + for token in args.token] + if not task_infos: + LOG.error("No tasks retrieved for the specified tokens!") + return 1 + + if args.wait_and_block or args.cancel_task: + # If we need to do something with the tasks later, save the tokens. + tokens_of_tasks = args.token + + task_info_for_print = list(map(_unapi_info, task_infos)) + _transform_product_ids_to_endpoints(task_info_for_print, + product_id_to_endpoint, + get_product_api) + + if len(task_infos) == 1: + # If there was exactly one task in the query, the return code + # of the program should be based on the status of the task. + ti = task_info_for_print[0] + if ti["status"] == "COMPLETED": + rc = 0 + elif ti["status"] == "FAILED": + rc = 4 + elif ti["status"] in ["ALLOCATED", "ENQUEUED", "RUNNING"]: + rc = 8 + elif ti["status"] in ["CANCELLED", "DROPPED"]: + rc = 16 + else: + raise ValueError(f"Unknown task status '{ti['status']}'!") + + if args.output_format == "json": + print(json.dumps(task_info_for_print)) + else: + if len(task_infos) > 1 or args.output_format != "plaintext": + if args.output_format == "plaintext": + # For the listing of the tasks, if there are multiple, the + # "table" format is more appropriate, so we intelligently + # switch over to that. + args.output_format = "table" + + headers = ["Token", "Type", "Summary", "Status", "Product", + "User", "Enqueued", "Started", "Last seen", + "Completed", "Cancelled by administrators?"] + rows = [] + for ti in task_info_for_print: + rows.append((ti["token"], + ti["taskKind"], + ti["summary"], + ti["status"], + ti["productEndpoint"] or "", + ti["actorUsername"] or "", + ti["enqueuedAt"] or "", + ti["startedAt"] or "", + ti["lastHeartbeat"] or "", + ti["completedAt"] or "", + "Yes" if ti["cancelFlagSet"] else "", + )) + + print(twodim.to_str(args.output_format, headers, rows)) + else: + # Otherwise, for exactly ONE task, in "plaintext" mode, print + # the details for humans to read. + ti = task_info_for_print[0] + product_line = \ + f" - Product: {ti['productEndpoint']}\n" \ + if ti["productEndpoint"] else "" + user_line = f" - User: {ti['actorUsername']}\n" \ + if ti["actorUsername"] else "" + cancel_line = " - Cancelled by administrators!\n" \ + if ti["cancelFlagSet"] else "" + print(f"Task '{ti['token']}':\n" + f" - Type: {ti['taskKind']}\n" + f" - Summary: {ti['summary']}\n" + f" - Status: {ti['status']}\n" + f"{product_line}" + f"{user_line}" + f" - Enqueued at: {ti['enqueuedAt'] or ''}\n" + f" - Started at: {ti['startedAt'] or ''}\n" + f" - Last seen: {ti['lastHeartbeat'] or ''}\n" + f" - Completed at: {ti['completedAt'] or ''}\n" + f"{cancel_line}" + ) + if ti["comments"]: + print(f"Comments on task '{ti['token']}':\n") + for line in ti["comments"].split("\n"): + if not line or line == "----------": + # Empty or separator lines. + print(line) + elif " at " in line and line.endswith(":"): + # Lines with the "header" for who made the comment + # and when. + print(line) + else: + print(f"> {line}") + + if args.cancel_task: + for token in tokens_of_tasks: + this_call_cancelled = api.cancelTask(token) + if this_call_cancelled: + LOG.info("Submitted cancellation request for task '%s'.", + token) + else: + LOG.debug("Task '%s' had already been cancelled.", token) + + if args.wait_and_block: + rc = 0 + for token in tokens_of_tasks: + LOG.info("Awaiting the completion of task '%s' ...", token) + status: str = await_task_termination( + cast(logger.logging.Logger, LOG), + token, + task_api_client=api) + if status != "COMPLETED": + if args.cancel_task: + # If '--kill' was specified, keep the return code 0 + # if the task was successfully cancelled as well. + if status != "CANCELLED": + LOG.error("Task '%s' error status: %s!", + token, status) + rc = 1 + else: + LOG.info("Task '%s' terminated in status: %s.", + token, status) + else: + LOG.error("Task '%s' error status: %s!", token, status) + rc = 1 + else: + LOG.info("Task '%s' terminated in status: %s.", token, status) + + return rc diff --git a/web/server/codechecker_server/task_executors/abstract_task.py b/web/server/codechecker_server/task_executors/abstract_task.py index 34e1cd4d7a..f38830ad33 100644 --- a/web/server/codechecker_server/task_executors/abstract_task.py +++ b/web/server/codechecker_server/task_executors/abstract_task.py @@ -106,6 +106,13 @@ def execute(self, task_manager: "TaskManager") -> None: injected `task_manager`) and logging failures accordingly. """ if task_manager.should_cancel(self): + def _log_cancel_and_abandon(db_task: DBTask): + db_task.add_comment("CANCEL!\nTask cancelled before " + "execution began!", + "SYSTEM[AbstractTask::execute()]") + db_task.set_abandoned(force_dropped_status=False) + + task_manager._mutate_task_record(self, _log_cancel_and_abandon) return try: diff --git a/web/server/codechecker_server/task_executors/main.py b/web/server/codechecker_server/task_executors/main.py index dc9dd4e543..580a27c4b9 100644 --- a/web/server/codechecker_server/task_executors/main.py +++ b/web/server/codechecker_server/task_executors/main.py @@ -77,9 +77,8 @@ def executor_hangup_handler(signum: int, _frame): except Empty: continue - import pprint - LOG.info("Executor #%d received task object:\n\n%s:\n%s\n\n", - os.getpid(), t, pprint.pformat(t.__dict__)) + LOG.debug("Executor PID %d popped task '%s' (%s) ...", + os.getpid(), t.token, str(t)) t.execute(tm) @@ -135,7 +134,8 @@ def _drop_task_at_shutdown(t: AbstractTask): try: config_db_engine.dispose() except Exception as ex: - LOG.error("Failed to shut down task executor!\n%s", str(ex)) + LOG.error("Failed to shut down task executor %d!\n%s", + os.getpid(), str(ex)) return LOG.debug("Task executor subprocess PID %d exited main loop.", From d42164b3550872841c1093b50026233ef697b9cd Mon Sep 17 00:00:00 2001 From: Whisperity Date: Sat, 24 Aug 2024 19:56:53 +0200 Subject: [PATCH 3/4] refactor(server): `massStoreRun()` as a background task Separate the previously blocking execution of `massStoreRun()`, which was done in the context of the "API handler process", into a "foreground" and a "background" part, exploiting the previously implemented background task library support. The foreground part remains executed in the context of the API handler process, and deals with receiving and unpacking the to-be-stored data, saving configuration and checking constraints that are cheap to check. The foreground part can report issues synchronously to the client. Everything else that was part of the previous `massStoreRun()` pipeline, as implemented by the `mass_store_run.MassStoreRun` class becomes a background task, such as the parsing of the uploaded reports and the storing of data to the database. This background task, implemented using the new library, executes in a separate background worker process, and can not communicate directly with the user. Errors are logged to the `comments` fields. The `massStoreRun()` API handler will continue to work as previously, and block while waiting for the background task to terminate. In case of an error, it synchronously reports a `RequestFailed` exception, passing the `comments` field (into which the background process had written the exception details) to the client. Due to the inability for most of the exceptions previously caused in `MassStoreRun` to "escape" as `RequestFailed`s, some parts of the API had been deprecated and removed. Namely, `ErrorCode.SOURCE_FILE` and `ErrorCode.REPORT_FORMAT` are no longer sent over the API. This does not break existing behaviour and does not cause an incompatibility with clients: in cases where the request exceptions were raised earlier, now a different type of exception is raised, but the error message still precisely explains the problem as it did previously. --- codechecker_common/util.py | 21 +- web/api/codechecker_api_shared.thrift | 37 +- .../dist/codechecker-api-6.59.0.tgz | Bin 69705 -> 69677 bytes .../dist/codechecker_api.tar.gz | Bin 62389 -> 62374 bytes .../dist/codechecker_api_shared.tar.gz | Bin 3723 -> 3682 bytes web/client/codechecker_client/cmd/store.py | 37 +- web/client/codechecker_client/thrift_call.py | 6 +- .../codechecker_server/api/mass_store_run.py | 1379 ++++++++++------- .../codechecker_server/api/report_server.py | 135 +- web/server/codechecker_server/product.py | 236 +++ web/server/codechecker_server/server.py | 241 +-- .../task_executors/abstract_task.py | 9 +- .../codechecker_server/task_executors/main.py | 5 +- .../task_executors/task_manager.py | 151 +- web/server/vue-cli/package-lock.json | 4 +- 15 files changed, 1313 insertions(+), 948 deletions(-) create mode 100644 web/server/codechecker_server/product.py diff --git a/codechecker_common/util.py b/codechecker_common/util.py index a9e966393d..5827a99b10 100644 --- a/codechecker_common/util.py +++ b/codechecker_common/util.py @@ -13,8 +13,9 @@ import itertools import json import os +import pathlib import random -from typing import TextIO +from typing import TextIO, Union import portalocker @@ -55,7 +56,10 @@ def chunks(iterator, n): yield itertools.chain([first], rest_of_chunk) -def load_json(path: str, default=None, lock=False, display_warning=True): +def load_json(path: Union[str, pathlib.Path], + default=None, + lock=False, + display_warning=True): """ Load the contents of the given file as a JSON and return it's value, or default if the file can't be loaded. @@ -133,3 +137,16 @@ def generate_random_token(num_bytes: int = 32) -> str: for _ in range(0, -(num_bytes // -64))]) idx = random.randrange(0, len(hash_value) - num_bytes + 1) return hash_value[idx:(idx + num_bytes)] + + +def format_size(num: float, suffix: str = 'B') -> str: + """ + Pretty print storage units. + Source: http://stackoverflow.com/questions/1094841/ + reusable-library-to-get-human-readable-version-of-file-size + """ + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi', 'Ri']: + if abs(num) < 1024.0: + return f"{num:3.1f} {unit}{suffix}" + num /= 1024.0 + return f"{num:.1f} Qi{suffix}" diff --git a/web/api/codechecker_api_shared.thrift b/web/api/codechecker_api_shared.thrift index 3e01ea5fbd..4a41889bbc 100644 --- a/web/api/codechecker_api_shared.thrift +++ b/web/api/codechecker_api_shared.thrift @@ -14,14 +14,35 @@ enum Ternary { } enum ErrorCode { - DATABASE, - IOERROR, - GENERAL, - AUTH_DENIED, // Authentication denied. We do not allow access to the service. - UNAUTHORIZED, // Authorization denied. User does not have right to perform an action. - API_MISMATCH, // The client attempted to query an API version that is not supported by the server. - SOURCE_FILE, // The client sent a source code which contains errors (e.g., source code comment errors). - REPORT_FORMAT, // The client sent a report with wrong format (e.g., report annotation has bad type in a .plist). + // Any other sort of error encountered during RPC execution. + GENERAL = 2, + + // Executing the request triggered a database-level fault, constraint violation. + DATABASE = 0, + + // The request is malformed or an internal I/O operation failed. + IOERROR = 1, + + // Authentication denied. We do not allow access to the service. + AUTH_DENIED = 3, + + // User does not have the necessary rights to perform an action. + UNAUTHORIZED = 4, + + // The client attempted to query an API version that is not supported by the + // server. + API_MISMATCH = 5, + + // REMOVED IN API v6.59 (CodeChecker v6.25.0)! + // Previously sent by report_server.thrif/codeCheckerDBAccess::massStoreRun() + // when the client uploaded a source file which contained errors, such as + // review status source-code-comment errors. + /* SOURCE_FILE = 6, */ // Never reuse the value of the enum constant! + + // REMOVED IN API v6.59 (CodeChecker v6.25.0)! + // Previously sent by report_server.thrif/codeCheckerDBAccess::massStoreRun() + // when the client uploaded a report with annotations that had invalid types. + /* REPORT_FORMAT = 7, */ // Never reuse the value of the enum constant! } exception RequestFailed { diff --git a/web/api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz b/web/api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz index 270aaf28fce6581ebdd7167ab89aa50f240f88ca..3bfe9dd373b3c10123ea738654a506f1842f341f 100644 GIT binary patch delta 64111 zcmV)IK)k=npaiX;1b-ik2mk;800092?7eGq6I+rfI-mI!8Y1psN4t$wZvi`Qc!TNf zGhk?Jx=&w%AcR#mYGlcyl3=@g_J6?+Nf5pXRds%%)A?2eIMP# zPv4xqJUzcS{f{>O>-YPC=k=i9`+X-2y`c9Wt{?OVgCOXmc7Odr-|hS8fB$0x|E-ec zXo05Ez#`FqJ@LO=@?Wp$zmvPs)wj6!W^xtJlK4?h%fH2oWHO)ioa6pc@BfTutI^`V z=k)uY5$M};dH3w;)1NqfQ?^S#~nN5~xddcx4 zHlN3AO^=-e@-NUCn1>3e~%wFM6vaKI$#vyTyFGy23vn zv9562@g!L;CSOU=y6g2^|`3=yEZi z9`#0xn1BCcN+x}TSA&0D&Bh4qYJPh=pRt};!`{!y@*C}k4tU&qJztP{+^rUObA&T; zSH*$zGkVN=c}$j)^bRJ66m46 zXohGfK0G{bvby@lCU(^O`7I{PN2gE59g)7(oqzc`!Pta)IhdfCkmE_dP3~|{*OP0s zn|JZz3U__r_y3aosTDIX(Mv0_yJF5SJM^Q3Ump3 zv-z?&o!n0F=;$1i`StQAMz4enhz@0pcAwup(h=*1HaOz<|9Wz>T9EJPSf;T;KHh!( zCqjkn)JL;>`Wrfq)s)&5&QeZ2ZMnt!2L^9zEoVwT{RBaTqymnr+_b#K(8+e5lL z(ih13NG%gxo4X0VP;)Xlw#*xZ31|)IXKleM^+GHCLFspb`=NLDHXct#z2*I#y6nHt z7vD2P;V1f$%$U$2jA`-;OlEu;;sVgEW6Qc7jS(UFF`7(AU#I;3D|G1yQ&Wt9SAQdh zN+UrRICVqYfEo+(Mt2G|o{()FEti-JK zi_yrxqsug%|2&kt@G4$RexP0a5%=(hB#+Y=fX9IMfUTbOME8JC{Oc&ehc_cvag4{n z*pIM>(keU@IWBw`etw%=eN%`5I)AJs;sNMpF5({(a+Da0(blkg(u>g!&lmh(s0((? z>TP13VJ;UZ2vf-JkI-QADY+B~Y;rT1p;5ADo~6f}PF-u)|EQPVKDK)po7vfs?itru z#J8h~xS#ReXhARq?+aPRZM=x5_vjwXzLSmnim(Oa$!v5RAM!(=AVRdb9)DdCCUK2O}O_?jYDjW=^KZt^yf|(D95|!B3g6yIgLe3M!Z? zli@MGe~r;*UZbrr3oqrkeShzP=x;LKI5w zlVyCHs3aWm+*OQO$Q9v{tOY%O%=xIaB4jOf(~lG~qG5ru+j!rQjDLHzN(h%HBi<5v z$;8ON6N)EUQ~W#M1bwX>AJI~hyUEpRzDm&Ly&WySW74;f(h(QA;$(6&BeWl(B0eLs zu^=X5V)ghO?f0ms-qqvB*(;oquf?+Y4J;IS)D6V+@isMR?;D!T*BIdw5*cGc$CA-6joFR3Rp5Lv$ zB3QJI2vhH-BZTSV7c^(eYZLMdlPpxlO_lQnF(%S-W<-FW6XL>7$W>10{RorY9XOr? z1Qy>d@fAZ#$C3+4Xo3krbgjzqA%Pur_HFbdCUPF15YeRP*VkCmLu??PA_7eR zFQO^)#gd+kpnn8RRAzD^AvAntcnjzu@li*2cT=oz&S&V<$u48+#in*O9Zk@F)3(au z&@Pfr)ZG=7bB6A3l8hD;azC#Zi2iV$CZ2Har(XSoaqsLJpTE$C5=5&oQh$p>Sx#=~6h=2A{F#tBrn(N<9 zkvW1$Sg1k&I~Pn@*PJOAH_);psSS}Gk+^jm$Ml%5=ToE>Q5MgM*Rw}LwK}9LT_IGv z!Slt_qbdhF*9lr8rdq1>D-=ZhUuK~r!XeVi#D8#VG@e z0VP5l;VUNj;}JveBTN^^F&3SU6nTQ+=u+M{wl->o0OpgXsk-n+$tUlFOl~|UGA&{= zcz>CAJL#2MEG6gSLVS9m`lgK!G1U_1&vZY0)Q`{KU7o!>eT;73?@O}Z_};NGu+*)L zquwrs1^_oDv$N#9l&-k;IYMVR8WS~BB3%q_E2c^#Z0c3IVN{Kf2D%WkgrhS1s&oVI zFu1P-*-#(Tc$8p`N;TCp5P4gXXoLoR#($?d;&YUHEq6%6sw8&~&sA!!A(?uwbsJP~ zaxLi$=9xE=1!nu7FOIUiJmS)xV(4P(0kBKgshdJ12&4qk!$KV{#(&0(yccIP!zMbU zGh@jj9w9yS@*7peF#XDIvvS@qgA0bmqR)`-Y@i?Z=Abby$GhqW!@q!vj(Jl(5=mu$Z1oQ9N6(KrV{Cj+* z(0D?5#fB{6o6&;$meSe>_Xh-s7;+IL!Guu}rN?tZJC{`IR@_2(=b2ZH3NYN4F}lUZ zGa=Jp(^tIsferiYpJ-AHi+^cjj<$TtN7D4fG}T4?KdT9IAz_xDphLk-ot!CR{qtMw zrN*;C+l(ZSE3`s(7DDg9=Gu&rmft6SaEu=SOn_}wc-(t6Nr?J{UANc0zatS8ZOpy6 z24b#X@2L_?v~R3tNlHM@kti_I7Iq|$j9vdk&h-FK78?%IYO2~A8-Fpi<2}UIEkyMn zpIr3LE*|%OJ-Il$;Jf?xv&-M!eZ1`b{p7=klk>~7(~I7_52|HcANh1s`&WA0i~gNe*8SNVD9H;{+iN#zQQmr>$|U z4`EXujekstuk@O|;y_n`Z32Jez`AEwV0hd+xx#D#Zv&^jc<2+!td(2w_itE!*Dq|^ zX_)JFDZ;GVp(p;qA0kaHa#c6U)DKx(E^um+9 zxfXjfj<1oYiXAc?F zXMFe@naoe=RqFLFO9C9R2Nk+efeY7jbZ-_&8((tc#*tL$uP56U zxPSzGF%PCAD6CuY(&F2a+#{0yyKspT-JVVWQ3?b&O|Ykmwr1v1PRL1Hia7|+u-Hnq zFCuH+aemC5tY2gKc!l^%`AiZF_!v!&7=N%a8Gek}h3*4o&tUL05$a8ZXP)gG_rMA( zjkI-)7FXY}@0uZlbSr+kNB@29eIgS_6HPf>KhyRMO~#7uq$ALg`aHCEfE$SSmxs?u zA6zlP)POQEW=ZGb{A9)yQbHdEQVB^%H2}=N62sX@v)*xJ9xXY_7d=5`&!Yc~CVx#q z%82SF8H7DSd!(5xwLRk##7`QO21Z$YO-Y=~;B@OQ;#ivmM|L9NAlyN#o87Ds;vwk( zF^F0Etsgg@NZ~!nE)C(=qd))o&%HNq{`R(aO3szPIa81Z;|QMyYW?R&k4}gYD&_## zj>ldmNkNGa3i<*L&+VS_8|AqlW`CwcZQxSX1%DNY|9^b@H6GKqU&P+3`a*^?nEnECOyNpi}f7|;%im5e$uIwX{_gL+zw>|L@2}pSo&N=Q^BD7ZOf|2TkDot!^kanS2^sYl&R!3m z&|jV)2?2k<}6gXd>x2+R%KEJGi1PJcW?#|$0bF@ItL%Q?mlh70+PjoZ{-u4I{AeERz6L}{KI7YTpSF8_=Vlc<4e4(i_3TKvwE`C-7&hDCyJEyZToyZ>mMv9LJDOL+~Wf8s?Av{)s zVin_&*eg(;{!QDcNdE-t^vNNoXL-lZuRSIWMePI~;eY3)&PTaQPvAbD9Aysvs5fA{ zt9C_>H)k>c2yhsD;W@NUEV_tr7A-gn4=&cpU2L|KQVY#|y;#og=;tiHfGCNn*9i?b zNXQ1iK}uSd8ADA>oEV8LizLl>Zx-{_T~dbGQgE1e$!`F=;^VTt$2JW-Q>E9V-X{$A z`4HrwY=4UI$jV1xXt6qQZ2A=#oUqEw)@Jx*P7BK#^8Ek9#x z)B%~s#3xfAf4pE|&wGFR6Z-d|X#Q{I)ewe?rylWbpxjaM&XN7DVbo-HqfTAjm*cxt z^6h{?DJ}j;lk*jN0nL+c78ieaKWFb3bNtr#{(!en)^n2mwc|DophaVX+8famXqH!!p#(Z@(n!P0fs%3u!Y1)H9oE6_7 zX_gHwTY~bkoD4jM{v8-cU5yTr5|Pu}mXVOn4Xl9NoE3@VMF)*WDk|-Uttbbj>&X)7 z4pX9pP7x%N$`X-69)&?_7<>p^HP)IXF=FBd>Qpp&0an9^BVaekb1tai=yLu&p1m3^ zM_HZ&v@r3UC2o`EIqH8KoYbf3C|Tm?f=MaE4(%Q074jRvC-}5vOOPL_6NN`fw*bb{ zSDS_c$5nO#9yZkg7**TI6ikX2rjIkj3(XBIFRaB2skoeBIUFm^ituLmp<#MDBDXt9 zx9%i0yl-ue8|K_O7qA}ZPIs8%+&b+P3=kl7m+!075U;W8xA;%aL$~)wl(#ksUmUDxBnL!AXG|3G^bBbt2{X zkn&tex)AR?%RHXk- z2UOiYs^iBA(>?KCid{cFwGR%KcK-B~8H+ahp6k0l6uUY;?a5F1JIpprI)U82_Wchnt%%VPq}`HFGXVvYZW&?$ z%#+a>A^|s(^chMLFMNbg^oP&-jW>94QP1$r|HG3|8dnPNh~LoDVPaT%$&;}fSqkzp z`yQh_HXc4NlN1}w0S}Ym8xwyCymbDLz;Q#H|HJ10sGt9%M7|Ct%w5Kd8O{Ox3!&{# zetmcO+mqh2ULR-Jdh+h|Yy6J^{l_`}+j%5mPZx{%;w7%j#i0LkdVc!h+s23e0+cU;p4@rG8Xy)!C#!cJOA>xv(vxJd9o_HUrt`VJtNB; z1p4nUU!qecA42T|(hon7)eR!;ci#PAsLb)zuNTYFa+UBIzWZ;vFJxZlf01*2eRB5Z z^wpQkcVAw~sjwjKt>* zkEVVF#*zzGTZ7}sO)#uW`wp#3*+np7UNkTT%`>RU>|2Z^u;f5H-lHt8Ab72bqE|5t zd7^!PVs39uSanQTXRed6U#aLVHvqNl$P;RLB_8_u^!oV<)GKFm7WCk~dAPg<8@4f- zXj?rSlTJBx8&iLl#|gWOfjt(PTLf6j%7W+y{H_2K6n(&IHl#G;6bk_PZPD}@P|c_5 z47%C^viw+{0nEw^&wzu0k?3x{NOXA(&B5rQFkgw*2CJ3q4RGy-hJbYOghN0RziwC@ zksfv}Al+JR+Oz+>r1l>@GufI+VDs!hu800#CjYDNg#&+U|FQO;uI7JLAfcqm7~<*E z-%j7WKmE{q{qg)IO~#H(Hc)p5+M^3ONAS*lA=5c|f5!6Wf5F-#WIjL?v05jY7Y%j6 z_f%gr$ggWluC^&j#9g8>tR&?Suym(8VS}w?zV|79T3QJoFNceZey6jl*wq!A1UjGtXVWtsb#`stzFh+C*!E=-313y*?eCV}G z5R`qcLf|y7#dz}1VjF8gF%0yzZSpR z2!>s(BT$GnBA{6v zfwla&$yy~Ec6GF1iH424QVXOW?l;XEGh?NgB0!!5Uop|PBCaCgY8Dic9aLkJ0Nv9T z0};(Ki=l8e$R*>B}K^8pW`sBz8vyBL|Ubg9)>=-g|#Px8A!I zWZQi2fwY;MczibdnBePK^PN+slWK1Ua=@)DySZ0RdB@O#8#y!F#@;w-BU^EH=fqj~ zh}WP=R-m=kSu518%ThM+HVGKe+GzJwv{h=K6g^Q&uI6`fsenDw(7;f|H;A3G5oF6M z>@2;MYP`%0Z5dgU?|=9ix66OVwmc3j2N_%72kFt$%s{)ck`-icc_AafAT{6M5ZEqK z&3y=U&aSsUakY9vyP?gYF%f_0RSazo!9@IF=}i2gOvN9+Q=qD+aG8WM5$rNqPgK16aQb}x*ImMEc7N&AZ zVx?&)a9p+n@Q}(!U^KaTP%YorRX%ECt$eLu~P7ZdZR)Q3u^}9bB({lj13Q$sdR%l`8r}0c%FVC_PkLP>N%TwLomH zXc-j7U`KXUJ2Ja}9k(EJ1h($thAlMR_9GiHU~O5-+KO7X6-6dnQRGy#6-5KcRunm9 zZAAp01HrbUNNr)X6-6$$6`{Ylttg_lqR7p+6-A!GRup-4Y(;;O_Yk%s>%#AXCeQQ_b8?0TC4_Uh+zrfms`fJqMWuGq8sh%)jOp+I&Msv#u3=$U**= zYCfb#ObH19)e?VP$3*gqc#edtTp?S6?x(@&0T`$?+f+$p$~KSeX2yoIJ;_W#99WN+ zTdU3H6PjkQ(Iz$B`J|>r*KPxHgiUO^kIMIGJeH=X`g%9cM0ZY!wD$WX`O9Lyy4ylE zU98BQ2UUDFWI<&cqO}Ld;=2S7rG5oQl3T2{?!4(Ci+Fz(B$Yg1E28X=!9lrY5B)?3 z%w8hzW}DedJZrBhCu!x9jiyDc>|ncTrO9wYEiZ1Cg_g;Ri=o!YiHpIjEh5ch#Tr1? zT(krR1SWrRbCbU?$nCI`d0S>D({H3Tk?mX}4CsJsOO@92WwW|jLsot9s&yL{G@Q-p zwr>WkMSOod{}EdombGDR-i8I@UIl@p0NzR*){}jGVEk`y#m9{ z1Gc=@MnmnYO(GZXn`V&N!E6lFKF2`m_+S;`ol}2=p#!eXl&uPERbZ%Y)& zR{7n-%pjNZ?{Q8&lcIQ51-x1yEC^_;^KGs=AAA5U#6cpS`^BPvhC(oi~>_$#bLi{Ud17lS8>=comX)vvL+7uU|vNsS<%9n zS8;zh;CU6%UoSW)&8s*Z5M-Qv(yh#&m{(kyJQ#fQ+S5D_96U*t&~$H~=V za<|l7iAHHpZ4L`o=@x)orH`40DlJuMORRq&QW@Z^Jk?~?5B-d7;mE4U*fa;!#!0b1 z=*kL$YP|79^i%+A>eUiADx+A@hA^*_-vB1ZN02Qmh8)z=q0FF`u{Ehi@zb>mR1JSj zZ0D`MQH|2Csi2y`%`PIB1f-_YL|{8dCauP_qXBC*^`bmr%R%fLJ*(+Fs(X9d${WclUBSN)nC1(od){{Xh`5%&%HnW(i>Flo)FEG z_h)~L7c5pv401J^PQQ+>zAN&Um`;sZ87$`gOB+Dziv z_)BW*_(IsZurAR~-%b+3`?XPXmdp3B)Odg76MXk&ODaRG^OR9?IT<$7jl z#al#5M>jEN5BfNgqOoR#u)REa^M*fIpqoH?#ZzEr63*aZPLmph=vZ`bh?JV4iokSJ zv`r;I$JbN*zUc6I#eH7HS3iI9_L=bVqg&keXhGI3cV1p16-{WX&%OWsU+uIAT8`ss zjM#U!HNTg)De+1@fYbl`^7Q@X*}HS~n*PrAowJ~mySph{>*?=T@!e9v_}{ZqlN43b zTP!x2ukzq`!H}rj&2+wMr%u86DH7o(H7V1S*bWg6j+wQ9maLU;OnQIjP*c7Z(THeM z)mYCltG_}5=+MC~5=)oM)8)+OwN9B0Bi5qLxkuk9brH(yW5sVuScV^ zC*L@g=GvQqK;z~|>9%|&>BiX$(Yq!VgJ+0hexJ>M&U#XltxV3Z@PWi*-12R_{5Bt3 znqHBnXLoQTgxxf3ZR&rXj|q*F`0Q})(fRCHT_g7Sn{O%p`V=3)f$o!i04HoyJkma*KR!q3)+)6Wf30A7!Udi=$M-mqvMv zI&@|Ja>sMr(QTE}eWv_{ux@0~U%S_g0ChEy|-l>1BqCqQU|Mh&47FZbdSIR#e z^{yw=WxTK~p&CnIo13$vn`IXz=hFZysf1Q7vKcmyCb0YMbv6YYx#d<(ax0Dpd#L~ch}PiaF4SVaKqPmJ%P?yKmn`a(B3+gfmWtp|w$-$k z8;a~GJClEPB0j1iQ5%sU$_&4*`hX| zHaQ|+(=)j`NDi)-=lQw86K+aH^Nj?i}kfo~Gqn>+vn9Z2CSiEgi9%^8wN-k@i)5 zB@cfe-Ly@-hgV0c>!HgYanl|+Iznr&-<3M%-Ff$(-BnBC*Y=2MHEZ|G5ylXzA=1t( zHmL}zlozzRM-iFl8+ec{%GesgH;!40^cSE&qfCI0wXP1K%JD0y*k$0?odSX5%RjbfHMb zenPF-Uud{TIs8fVt7f00zh$pQPp$k!kV2~ircoiSq|>J-tj{k`dVlIIXg##gsXc!; zH$28WOgchGaZIn!QBR*!hS)2$w1C24#n++qZOi*ehWvQ`-{E&HVINtPy#Bu{xppxT(2f7<&`@DPg7u66Lnrn5*?+8 z7r#d78c4^}cy_b=MkK91N8i65UkiWyG(&!!pi#6YhlfnoImU}Q5F3&fwN(21W&ZV_ zbqk>Un~|?)tgkYf6g>~p2Jc6L4^ZKtN)2thmQp~W2E(%Ce*D20w?7M6cPuEr7T z(4aLK?)8C&hFLvHzq-#A?TO&yFT6k?k{&VQA-v>2KM|_jlSBNu8T4JBjF5T%XUTd0 zzvSm;_rAv5ro?3zz9`OWFPAc-?>6dzr)Q9*DzbOO|bf$JOLg#v8CfKDExZ=`5#O7k|6X ztMbeJG{mrqs~)Y%wXy?LWL-c98|Gi;szCK`W42Gr$=0F-WjC~&>)^(m{z7-p0 zY-?SmPII@lae50_99a3NIp$_8`m{b<6J1wlXcBbutSnOwX|0~P=6QdeThMi`U-u4lnctgAM>W9fvo*6C;qz*@Dvx*a zdAtS&@b@D+F|G6&|AEy%Ye@ots!Y|pdumUX^RH9ot321Qz2}|f_{A&gGPlTsW)&2_U~-+8Q!|-f;y@KLxl!Js++kR_|`< zeOXSwUM;usync;+?<}`3v!*Wd`&6CE-R0*n@@|l$*T(+N^7OTEzvEVXQvtYUjQDw# z@7A;7t36jU;cJkawctY~;cA=D)Xpq#U$Zb@CG&mlo$s>szIcC89XH-<#YcDAb`K%& zW=;1sZ$OtV_iNyP$636oSYjg_K2wsgDF&}}tn&9kA%9l^%?5m0DzZIR4;1XHecP12 z>^rk{?s(xwcJh6hS~^wY?uKmA^|=&q12)T~>Rp>I^QijxyFDxlD#F+_r_zcOw!pAh z|0>^dtMDxjU%-F9*xy=GV)6^CP`MHAmMZYq#BF6=TBWv;l8cp8+ci(2%L;DUrFC4* z1uEl~rU<+2$IiV24<>k2zeHOif7H7|TM%JvD45t>h;gQBWRo$*O6L_Gy#c^x(L<~4 zlA<;(2Oi%tG$6=WB)t-3ck#`y8eF-K2>?R@ZRsZU}3=b&BuV( zyD(i30vMw@9ckRq3pEPt(0w(PG_dspX}Mwf4TV0!F!2by>#96uh=# zM%7EOK}lncThv)$W6e9%WqHF`1+@WT11JF7h@_$QTfy_8!*$E)0TxZIcLTMqtN^iA z+1m2bL;YRvtmsf;NnMs3^lIRH?bZNsdxJtj-OGQ`S&3kstI_dy5!q_F%{+&IO9QX> z3WD}oEhLb^Y&IBBua;ZeEl9huJiT_E;!3X;YV3BGA1mZ7r{ixFSZ(#4es`cF>SmuC zRL1l!zcQ$W=UryXP#NL|c(Pu1xe=DkyQ~nzCqT^lxLOIjB^I5oN?h+I@m?&0u3HVd zJb!<#wbz|x&-ttAGI!=PKS*EIUDl`5XuCmfT?6ks%dBf+efRzMZLsT13G4>F_Enj- z3T5Wov>bV>e)k5Lw$hR1+iTS3uIgytA?{tcpzgcZ6&_Bm?e2TYABut3!~Tx@#yiZ$ z|HJ;5^WWX*>O0!dr_;&Tr^=0a^(!rIaQuJIWc@Mv{eIwiJ?M95;Cchc?fr-A2mQey z2>Pg9e}H})p#R;(2CNX_S)i#jut@Y@PyFwe{P*pi>2 zy_fUbJ49#~y@USoU~t?&d_>#7de=LDciB6Ab#~eNc>d<};-dGDcOQEvA5ME0AJKpR zx%}IP z{pAU|&G`Ez|Gs48jQkJ%_E-7yUs-p#oJ1Yv_^GKWZiz={^8+9k@!fo}{F20r9|%?e zJbn7x>6`bbA9}App1;J@@1l2d{;Gd>arxon%S*IH7jlZJD~D~tA6BzhqvhxeQw)UE zjQ>rU9^v7O)$BEi_3u%}^_+j0PHrd5^pErF>m<(niqx3pTQm#hcg)1)Nb=vxc$ifR zme~8R|LOtXad(nKXD5t!7H8`G=hQHqnCr8ki-Tt%zX7B3U6C!p{@Fgk11NtVff2+O zD{a9s#ID1mXrF;`)I-y@@q&HOM^W}Mt=XEePO7$OjDJ^!A%8HZFI?0(8QZ6Azx48=~w}zt*&RI+nC%=Bn{xdl}~IX>SWnF(Yz%s=~g3c$!^a0jdG<5=!nl`#TM?D9a5x!M&plVZ2ab7WJh+P*=n!-S{cJpRU z!KzI=!^*|PVHu!k>KuQ{fGM7eDS{AGMwHCgi{<=|e$HMb@amS3ZiVsAB;PXoN?K%H zP#e(cVliI;V(Xg5BxnfSE!{T0M0v`fq|9e-5bu<)kaQ=O%})4ges}+7k}S2pk9waZ zFTnfx85Wndr5NP)bQ4H)&+dr!TQw1r**YrEy$tKqbR}aXPChl-Zro;YxA;6#a{`4pE+o1qcJq#E+p2oLczpv05{-5$ilg%)? z9vlKKA(*l1 zk`ids{78x@5F0C$P$*DJK*2=ksMiOi4@m(afkwL+)I-hs5SJVEvGDo>bP8H)oi?Ty zi^D_S{lvcfyLhp@KVY4RN8k!lvF?A-djG_JR%jpl1@?bp{Gp&ydiBZ$E-6)l99wE3 zMwVZwh$xVybP@H2c23HCIbY3|b&RN8w15_fRdTHrq$&u=3RSypyX!wIhVCk(1o5?L zJ}C#;8N1JB2(8uUi}ZQ5^;RhB>cLj#D^^`^L|unG780debXiz6@FBQc>w2I%%mXNb z#LV8$GqZnl{mp7b%`$58)64no?PxYGp%wjxG}HOw{%oA#*Q=TKW1hAoJ25IudJ|Y@ zQsC2!5XVd*QHyNTsXM}#dxJc-rvg{xeHwo~Q`b>r0r1S7f|-|`^5*!PRYrG9 z83hiv7K61)X(LK0l6}w4|58dXMSC}y-B{IhH`sQ&>M1bP1E{DJxnV7pVY5|KTV&)X zv(fZEnIvbk>v@r)I#SxM6AWSGw!i=|FElg+I3he}=^Z-Bd^b&H$08*11b_@ulx6=p zfR=yxH|guFf!1k$n(e}dFmxe=p&Noo-c4>)49N_X5<+rFz2M|Ue4~h;XW`3mb>ITo zD1pvK32aV^vgVLx-rB31x(yHR0VJp;H(9euyHD@EzGHfN@AZR6X`>(^rjyV zn)6g7$$cwg6dvhUBs*fr&%2w{>RL1G<}%4tB&3q$5WK7=(X91)z-$8UX7FQUC^_NdXvyTmhiqpV+UT;dG_= z{_!5$Q30%(d{fDEj3OlPcC%(Yo(5ee$xenWc2@baK7hvZ?XWG~qO?0Y64cOitOuZF z55OQY>N<{7)dS!RKxM~qN-I002LWbKb|S5fS=n)1q3obPg|ZVdWyf&~lpTM^Gbuat z3na!Ga_~4_MIV6USs%de2VWY=Tw5~#&f5PSzXAKd@DJW-Qy7 zMMifZL`L-I3jxkVMt4vkGP-|`Nn~`LI`(nb>85?$8haZO{Wg+ZDl^tDnSEV%ZL_cI zn&dv$h0MOLTV(ci-L@pZZWtq-tu#j3jA1*PF$_50y0W9SINJPS)`V44j@gtQ%VrbX zQ>)Ia$@TRIa_q@(qvTsns}8B2hvDPQw=o*vk`h?GVja3zui|A)hPi)Oj+Uz=3%nfN z=)-l?NTjac#Jy8ZK<=$=BRn0YP5z|B#={fRJ_D0c&P{6%7s3M%(;ze%+d?9g0ZU*P zFVr}HlNf@*GgUqUQxmue&V_yj2G0|`pvzcR?8=9=LRZ5kJJ&Ot?4pV$J2!+)cBMjRBY_fAbtxb-}ZYY0EcA~@z0_P^Xq7o~fvBZkkuVb?F`rS0ynUTXA7?H#CGh(eq z^bIKDc_4~-erXi(l;ih&7)88+*2at?ULa5e{rN(GGZgWHA`}UYC=%voodUN_#g*_O zaHYg6W7o;(M+%2k?u6@X`kiqO9-H5?_?fpX22O22Xdiyr;+KDa+2Sk6>Kk6Rc#+n| zjL^P(+2YHWEgpZ_;upPa@r^HAe9wHo`J1Ppwf`WGSNt_l$M?;sGfbh5kfcnw62Z7K zERQQH9t>ez@qMk08CQb7z!mi83jxk>CFmF6N-!|t3i(pRAL(u>$Po-Sj2zuCl@tiP zjT%azv3AMj6F7fsyLB_0I9AvkQgoyoDs8WH z9Yb6MV(;2;70V3g+D4@1wbFpsn4v5U2 z5t%*l5)tZ+VYO$XR_MU_0PUc3$_gcr05V+_ojQd9kJ+!mSePi1cdqgkDQ#>xNY)d0 za>h5Lfp|ko!P!yyg^a~-_NX>$ptsKUHN$^$?f4z?!ovq&*BorU@GZyv1m-uKVZUnJ zPXIsS49kB%;tZ5WoT1?nXAo#@%o=YfA905A5of?3afU^YI78zj&d@1m?#ssWgpQ5p z+408^g*2>H9eYeBlES(yC

    z>h^yQQ@Hfc*jN!S5miWRV&n}ZMiwV$5R=r;kennB9`;PI?=myl1^VSJLfG7BgZy+9 z=_W??8VoB}zGR1lC&f_}o{#z!n2LhuauzS=w|9seN?DS%g2z|x1U!XQ17H@~wMxO1 z4xCiUNri|>7|hC%4ZqqC&9C;us;~A#_|<=YSpL<1sJz+_4X^e?PivFoqmi%nL-}ey z(zeLw~45CBoVdwK1Jq0hbSHBpzuM%jf&tPhp2pzL!{i6$PnZZ z4YW4q8x_eQhe!rFL_EkLDhhIlj6n|ci)3}2;RAUNqcA_uA=^~-;)UIOD{j<^ONoCW zhtJJmZE5;J-8B`;GwIbZfVLJA7B1x1ckkY0mZ1qm5`TW3&!-1F7j|!uz?zP?SBKf9 z7vxd2_6zbTGP#ta2zo&tMMV$&qR8fo-|}4X8SPpc;6Sy!&;rMx)_m%$XH(@&IPZQa z<K8+b>I1=S*1P>prKZb^TpqFS_}ifVufBIQ)9l}}57G*};f-TJ5q3iXYt zX$yu}3268zS_A_!%s)`)&qag!QwJ;`V)iDpNzno%TxtzikT~jvzzm7lkQ%g87GUe> zZK$w8|F`AG)kAXuxuI9}arF?cAU7;uL2js2kQ*8*$PIt{S{t*O zVJIue4P^znA+I1eEUF+kG**zKUy6L@qH)E73d6uYz~23AbVd@_mSViK1r>&21JAC9 z#(HYQ5PWt$4AbKOFth~~I`rWBWHy^GNB9OM>m=VR`+%U!3G}Q>yUY{CG{RJSt7wL{ zLMtG&u9F39Xw07E_nm6aX1{+Arb|Nol$9)9V{|25u#Iio#zYfLFtKghxUqF(CzH&L zGqG*kwv&nNiJiQB@4X*=dev$C=+)h|YuBzi+SaHL6J+%gI)wQXr;p-oRBy%c+S+YY zce-6)w*1NO`=jy^y$8<;AbW!$%8@7v}uUIz+J}7Iy+MwLs*Jz=>Gtd{hr$*L(o4gy?o`m|`8AfvBpw?f<;z{V; zTYho2)Ui8hB%;qbOpv0>^^Gr1dDbbYY{+~VmXny4uJVMZ?hX|qn3I_O3r<#?@Z`?8 z&SbMd@yxsasgatGj7e8W#CYdynCvsOe+c@Lvhlx3y znkj)r63z#16+yH}PwH)NP4WXso!J0Gka(&)ZNGAHfLkSQ^?a@pm1#3opEovDAH zZ8suIze+|C%NKxS)pB(E@7>)#jS~9)_8+2w(rhL}oAi(mw7Qcu$Lu83!hDAmRBQU> z&4|A4ZnC?t)NaP*(cd_ESVfPA;(?MnxluD>2fPehvfFGt>bS(=KU%CV;}peB%UxX3 zO{5SJ*i7GTJ_6^^jr$n z1Q>C<1zEFIqBt%5{xDVbOoEJHO>Y&}!Jr>ef6EEPFME~u4W8+o6(cDgiGq9S=hjk@ zfeKkM6pwn=j=NvSl(AYZzbqT^1fMMA-qN1TXgcBizxYIUDd*&yFFTW2Y#pnR{TZEo z|174r@;oz;qO=5jo9`U^!IzWuD!$pfIh98*A3R*x?1k-A{Ornpc_Jyya!8*V?Cj$B8!<2mgDLI_R1R9T z^3ImS=%`wx_?e+U-AvBF7){`|Pgq{WombQ?;Dw6F)Ai#lgcLux_TP>*5N#{cclTAu zQt-GK49EX22J2YE%$&5)0o?6Pfclj#e?&r%41Dylf$Imv?+Klq@e1sp(>gmxW|dDQ zTd&}W4G_RLw)B@?uqV2l8<&XDengc7*R^Ztz>h%WGKAuEPlBhDuef~>!bb5-9IQxu zA&b{(RYnl<9SZ_q8FwTC*cKqU04*by>5mdsLLj8z4~Lg=H|zt(yP{ zma@GLzwwCk&FJ#XrVH|wz$~KNA0IzvU$yEau5ANMZ9h0RaSh}f>1KRgY0_R%)#INA ze7E#%^c5`tZm6X!L3d%c@@a8I{Cb2qh736cO&bdmj}7!K{HP?b=LC}t4-FJPI!ztq8@zyc zfRqu`A0L4(lS%W~)Nv#kJNOB{5P$^$`ccR4tvB_EMrG>ER3JcMsxhvs%+2vn6{(2L zeX)r4wn>+c-rx#Vu&F^bR*CI`f@=0aHOo=GIsdp>)Gwf=)!)(cqL`sU-tg9f8}#j8 zCI&LP@ucOdoTvp&tT8mp|H>TtecMiS!oSqGY1z))iIXmw#InViKukhxgn(QXcHt-j#AJko ze-sun#z2Phd8~?bey~}0z$KY2T|F2~st$CQqGPFuv)5d|hOS(gWkgcXM3EIf5rZE_ zmFWmeY*i|k-cbm??P`h9>dd44x*Xy5=rWdbg_G&tP%PGs_Sw*-4tLZy-HhKcjSlCT zetXOG6q!$M>gMx9hZk60ND79M+EW%mbxeTr@4&~(dq~3TYGsD%Uv-ORr(ssPi!lsV z5+>_S^7Jy<9&S~}>bbGyA{grxI1TMON>kXjk(@dkmRGLX+*X~Vt8xsz6H|Im3hpxC zqoRK9+I!c$Rr)QWs_1sAx;1oM{Hm-z5Gz_51wN2|Tp>a~O9Z(;^~Rl_>pHcvwjAri zXMlRC-{f$MICh%JA4TC(N)gAuwxpi68Kt6cu3u)s1Y3!wF2GxT7kb=q#bQ&(B{MkkMXAViDZZUq+F8r;e*oUc+L@M2r93d9T4ieDm`c05Gkd!~Wr>ifYjC@)8YW?|xG{ zO!?*M|D2nV%?*`3Is-W$_OG7kW!n-w%_Uu6RoK4@*FP-Vs+#|Pc!ouR_P4?(xCLe; z#HKmcnjHlnV702pmb)MUAYFtG+?AXPSe{^KM@k?08l3B6kVJXLM_5Osf6{q}WhAbr zt7+f>Jf8ldRsR>Xk49+U28bd(uoJFf`cfbQ>M%%6(E7g{J6(KW^aq+OL$kr20J!G zelzqBSrIfWp2HyuW+4WWh`l3x`8;`me$Xr-N&}V)OGqP<-*_GVlFtx@Qb(pF6E&uy zQn0Fp^NmI7#o0Ow36|mDp%_HJ7GJ`GvJ4xqa*7C24?Kc79)#-iDlBw4*Twe?v|E@~ z+1Ua78O)VBMDssJ+_J=VJ{ZxBYUlBVeV$!z;&`@6H5%}?NhLsL_XewI);J4hG#?Em zA(NhP^~k!KWzY$`ff*B;6q9eXgw*HHuO2<@+U#F#H>5k5(tIFLEhYqd1^ZfQSNInT z`d&;7L0qRw{}`2Pd4uV4{KL$^&0Quytwxo17Rd;!!|jPvYFH2#1a zbUb0As<|e+E_)yx_PFKwRCrUKB=c)zV7q3EzO{ma@H)boyNzVAW_v-JmD@vZrCVzw zlm1&c#poIgSa|A9^vVD>5|k^#8+paK(b@P{JNm7 z9}I|7KhW;<>FyBR)VPkoE`$RDf509mniHC8sn6gVt3OTSLi~Z*&)K?hL4kVjiFXO` zV~g%vb%g}NQE_j80ex98kjMhUrC5ys>8K1)&HB5rBv;_)>EG{#x(!aj{k11XVS@Zy z3>Fa22I7^#JhG!aj0KBT;BpMP5=43kT*_t3SdPk9>*~atL5C|c>}h%)fLf%}J{b!i zW6{66ybb2@E&A?^;eT!B=S63%Z8cVMo4<<>chCS$>m-6DVqoGOAiTBaTh<3ob5N&o zxho>df`0~Z$sk?6^9Q&4na)GB8xAzzl3u?|+cnA3kL$Y8Pia;`j^9^^ENO>UP$6&+ zyWYSwjA>Dy+NswPs|?7-EpE0BD7V;&8pbB)1K@h(GlAv?CX3lk{QyeSmW!TJ0((BZ zfSo4DV%L@np=4e$qcF#HDO*#NX5xq!k0(WR`|;DQ+%1GY0Bjch-(>#;)apcCcroA_ zN=H;e6XdkS+T|0YB}b^Oq~7Xk%Ua-;klsZP_uwLmFKdQiA>Dob z`^{-?-H!l`nj~3Icmbtx6ls@O6M#XMJy( z&x=fYCU8T=RTHbjR{ZFxVLNxBx(6sVmTvU=XKmEDRZ`%TE5&aVvR`Yc)g^QX=}$cx z>U>86`1$!fQGck_oj@G#kbRQ5tf~N;PrMe6d=_}pXvySkT}I3_Lq+?&uZG}qS;taY z=|gRWx*DU)zWdh5Y5q!a?Q4Ec)#XsWxt@xoxwC|Zw7BLo;MqujRle%MY6+;e=yi-n z_NZn7i<)W!7E{8^3auJsYvIv~wLSq^?_1BttS(+0uA!QXr;hyW{zHxvcPqoxbJbXd zh($JFu1rr|UM)afLmb+SvkNx;)aK@nPo|>t= z$|e4-GP&Lb-_zFW$8$iktrqjJ&5vKFfB!UjW-dBEu>FjC|K828;A}PiTPM!SZpOLc zDn2=eeIB3e$|nQ-z7uWsL8GU32e+emzbHrGPVigbuu(LD|I#R|(LK&?X1~}z>R#3{R?e-;@$2m3 zg3492ta!(Hy%)tX6N7y-y0-Rr3TF0qLs8APA&U6eUOlfIi`%YxQs7gdj2v zX>ehXjm9##3rIv$4SXAvqG=7Q;o)(-4O#}lYKcQ`b^ie=Y6arCyQWk%fXd}vNB_VQ z`^eXBq~8&+opcI-jV=?>}!NIXE5anJczfw$X|`W zG;!C9H^`ts?e7@t`xY*TjyJci&V1TMz(0nDwDrTTcN0^yMV2Q;oF>j3p$H zIb=@g5av$XXlzle=}!FA-DY9wXDx}PQHzT03efIFLB>Wb!0tgSa!@&XmwfduHHE#Nfd_J>Qsf)YRxnSyJlfTnMPs6b{UrPa~W2GN)iRTT4BV?7k?s# zKl(LMA%lMXbxc`3YD6oEqD(!BqE4OOU56`pmt-KR0|t>zEt@Q9vv- zl!QX}I2YKSm)B@s^;l)loS6FH63Vgtyz=$iXA;bjz_#K0om^hJb)IaZbr}{O#$*SJ~&zx~)cQR^c{Ws80#_PUXI+&9ZJS=v>74LA_6_3D% z7b%#iRsXI+$#+SYldvWh+^Q5|&E|n{F6}V-b$m897OExzj0+=YADLdCu?lUIfeBf6 zC0rtw`c$(*5BRL|@|%t&@|=EEu-Wz@GMPi}q$(`q6!Hk)#NhvoA_^ zApx;y$2K8gV)&H~m1#WS4b8r)&3SUI=-5lM0+sC*8}Wf94sy{%$S>HUFnK>yaq->M z4BitB*q~s_vR%jOVzu9iB05I?7f}IAvx9>=MrL~want;i@7TOF>(L=g+!`amp^PYD z+&l9;@lBT7aO%rvA!onfn>U!7iHa4>H!Ohwe+6;W97DT1AzD4j|5Dcar-$elU0lk0D%zZfiI@EID8y^Vjbiuw|%yhcoW&bmgM|@4-Mn1{ODt0@445t2pAW z7NGIq{za^l7LTLP!1`h(9rjo3RF)jf-X+$nT(ECTB(T5rJGSq0IxkHzhPJWNYojD; zBE0K-P1qUGpK;b1FBD{-e83iqSxSIGB+Sh4H zp`nU&zM*}zC5AWzp+W$w1qGk%F{Rlix7>H85RBG$n7(W!7Ox$}1U|6-Y&5M%5xSP! zAQ3t%FhZz%w7FX={;t$&98ESZze&q$JMH};6O%|9PD+Aq;Mf~a9H@M7ALUVbh*|t4 zbWr*l9*uIzc5!OqCc#ji5ecaPlvoE zxjj_nH72Gv*&L^h%i=2BVz{?$!B-rRSEblvXTgkYnwP!#%)M>cY=>syJ!06w90e+=BmTKxbb|s@V zi1(~eLFZ;bnBYW}6G29hlQ}OqHK@W|5gZEiW-bh~8POEsa0C*yki)*9l8zs^{wh_V zAbJ8M`bHy?;^^hq7g$h$Bb+_&F4LQ=d}$*_$c8=qgbK>GdP-4a2`qT51PE2 zA2bxy!(wJ5Z0PrmTo&R2zrQBZOeuDM&;axDh%gh_hQ+WFRTt)Ui>=OQgZF>I0dQ_9 z)cJmiau+LGN{lWI^1}w}3rBW{nNQrlkXIwMrNfktU6|b9vvphT!sS?eI74Z(U3kmj z$44%c6X&ts^Fo)U)&3FI)D_YhM?(Zl0_8!olJ!G$1VOVN+aS(h

    0obrd-ivW=z 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

    |gM`A?ORKvXK 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 z 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 z zce&P@3bzEE>2C7}-Gp(LH55+6SVMA@c1`zQ=YIta74qu!ASd~{Xi2^@LXy8wHLXBL zfQ~#?NJb!JyJ9+={6Nri_ zDn`{Pal`1@_01rkU!!x2&aSHKZXtX@zcUh>G)yTmq>J-Xr4+{(=p#5!mUwhPK6Uthm0bp3m!+D($}OX9F=s!RX4L-dGf()3|*)65OUvl~TAm&BBl*XIuQ8vU8zkg_e z_lQ;f-82KBzF+)Ten2|ckqG^TsTjs499??&|4q@xy+1fm2c7;)Trt!6on{4O{VUf1 zX~FIL{3{B@=VQftM%s!e>L({@-6t_3k`|&PYpx~}w-5HrC%(lm8E%*5FST9zjL~m; z?Q8bupNl+eB{RT)DLbIS+BpP8wtq0p-prY#oPP#(V(Zz7c0+aov9uNJgv22wETgV< zsXNJX9D_TnR6RH2060jpRcoO3x~7Ey)@ehm6Q%AJ=Bd{0sDyoDaD_CU?SsA2!_>rI z!M3Y67A$uiuL@ikbjLDi4f{Pu4y@ZQux|8h>C79`Ii?IG=yFwt8`$;tr$k%YW8&p8c6dfj-0cSw6Q-P$hxaSrlX9H^a4ZU+`}1qr(5L z({XwqCFnaxqkD8f5RDkjr?VPOlLA@EmRN;pMt&IlK?T>sCtZvI04)|Ue_O-+tzCsh zt9P_Z**Zx$^!s|8PF|=3G3JTs34)$QJ;6?~h8C3LR~CeN6fK+SDGu&ez-5E`6>wjV%zYVT3B0d>@pvwbhk@D$d&Yw` z`(72}w?JVBti(1d>~O8hMhZJQ=wGMPG3cT|7qz|I(Au?8v;u6oHi{e}P3;7VAmGQI z2S3JDL4`*(${F&)9}3EtJqcN&2r5lmNXbk| z+CpISc0*%y8U|JBB7r<|%m`vlOkS`GzEs8MHL|9_k*$*>vrtx>l8Rxcvzm&+3GN1A zbd{D0BfR=OAC^o(OJ^2RfErw6(1eRDNx}f^*p9MemXn1zGJnfcH$@paLdwA^vS=L& z5tv+^irq!1pn~551siZ)+a23jZL71X*q*4h4it637H*y`+(h^zG5@q1SunW|r=JZI z^nJi!K0^jmDE+L@>q9B2`BffFn*nyC+CN_H^@TF|gS}BYZYdA;wx)pfQ>g#mj=IJA z?-A60-$?y;sDH)|_1~AT|4wcrbPGKEC^;JC_21j=ZZ|gSzjqMwLH&2A|GvHT-|Yq8 z;r@sFAMStn_ss5pCO}y)|JwUM4&%0Q{=;@RhWj7x|1G}%*`Jjf2YtSCZh?_g=1){b z3k{LyHZgnzFXw~#!tgvgdjJ0X_~`274Qfu+BPVD7=YP}5C-Q)r;x0a&ouTbOzXS67 z`254i_a_u2Bv0NRoxUeOBii8T?D*t8ebOaQ-dvo2{78PsRGD=B+tK?^Czsb(=htUP zA5JcD2}YqvYQJjzB*yjAOYyvep7YTuj|23WjZt|TqNie{%JT?4r{h%~c5!1qYUO#1 zp0lyDr+@!p<0qrW!>1F`#;*+iRjVy~lY@WN>L`!-;9s=@<*6L}t5&EyXM=y$iqytp z@UL23Rv$GP+G zQ)y_uzaPwg@Snr!{e6mLL;gFR{A-ae($UfUk3n-`uU;D>Qqe2%6%M=a}X z{v{n45X8Ii?Rao|na-U;D8=NtXC0Y4`yr;zerq%50CeK)7oRu#X-r#wLz%UN)F#ay z7w62|t5a?g4ywOmv&L_=-4}L9vJTjktsnz`Db@wOH_(GO(UuA|vmlK$d6E?*W^)CJ z_r+ z(HVRLAdYvt;&>gsJL@Mdsnr(EJzU-uS5VnYtRL(>NdhEt$zJKr@Vl<_<$#3=Cyge|!98cp!F{G|a=NZF zH{kVC+N(dM2D~Q`RC!(xVyJ-Y6X@4ZKnM54I-T|Pz&@=5eGZ*={UppDe5PSxgIy<5 z?82Bzr+zBMKD_osgB*AdFz=!U(wO(w=4AYRBYnTYqHpyVhVd z%CMn+Fh;^6!h^guzZ(!l&e2WLnom&+rR%&kos54R?v+_YC)3&eV2r!xPwof9yKIu4 z8g|j+=>*-s>8Re!D7$1fJJF+NY~&r1x-jdtX;F+Rr`eUolc0@4L=`fy zWOYz^K3#__AU*Y50^UYik+d4K%}bg5DX{thqtDEpR;7LskB-Vi|AV{0%Y8}rlA*_)rx zV8_Irmva0mI$dni)KwLIVvK0u|MS0CS1((*gc4D2u}<-8HYEBHtn2(~I2|wUCqJ=9 zq%W+ZZsAjnh#2|e9J8k{$-?6y;b^yPl+N(MoS>(zpUyAj_{zl!qJQeoO_Ab_c=8pk zN3IJPsrbEMdd*xf#fs*AHZ>hExi0vPt?@_x()7ptHAOQ{HD5}huDU_cHg8P}4A&49 zK!n!C0wgsu1+Y7=-LQa(cb2aSor3%ZrDoz;yrIixZ_E{7-wwzMaO^OWNJh+y= z1!Cx(8>iaz>`*pG@rf;*sDeX?*f`ab);Md1Glu@DGCE;;aDS*DDxn|+-wFLo<>aO4 zbfJE&l=N^g8-^pea=KIi)V^0rvx+VXn)|Og7kOaFBox{P>YLqA-_(@U6cn?%qGren zYPP-VX&U7;+jZ46yPMOqSQbMoui1Uv>YBj{wphFOs<*k%wmJ6M=1$w#=eTa4JDo?` zXt^ZXW}CLOO@HrpxPsm03{3S#cX)-|j#q`;j#(kMQ(GaolRVO1lLgVY`H@TJMyn`= zESb^joenyDr+Yp-9R%i1_v&^!2phy7SSO?HQV>4wE_GM1OR7XrvrP%xrXcd#rk=4) zsGGWN>Oazf6pNy7cmHv>J6?gY#4g=!r1Vj;0&LZ!%zqkwkTZm#7e^SHEMZv76NcRe z9^@s{(GV?sWR|YCT0~Pt{f%H+e|4cGx~#k{VHsAv$F>A!SoJSYCorQ~B+tedK3rii z&m$~-w8~hdjW2$8`O%kX=Qc9@c!k5?{Ng9e$0Td2%1y*2cX}%rw5~c`->|{yWz7L# z)G%EZ0)LkK#em7E2Fz1eg@8$hSobd(rxPVEEH~zd*<=Jf3uEDQK-(3OrRbSL0o^@P zC|u=Cifpl)ht8Ya+ePP%x#ofYn#(@R);$sHF55O<`^0nYleMfp)d;#SJApD*JoI02 z(p}kN_rzk8W{uam=ef?kwXCyH$@w{ArRHi!jel301S^WA=G~R8c3-SEZ&|tE_`;)b zYgsj^B6D3f0cosT*B?tomnCZ#wZ+1}Y`Zk06f45_lv#EJLJ@KqT3=`WPY}pF*x19i&ES349Qp7zojGLkQ8$ zGJkQLs?g^f%c=C?91dyeW2vxIDFk{)riw1y<06ZFED@l(%+?gUaF2{E_OVQ~t^dXp zdN8IEG!MF6YFn&YD(~3a@~sIS;%}GS9Gm9KKbp8~gCduJ;-$98s>^bnkgXGAM&0@u z*?L)ON=s~!r4M65vBQLxJVjFDT{7uSif_kXuRAHhy*qU-fJ*$1scLg zT2LVF1rJa=*yDe>RT>sG;dsU-TSYjt*suhW$&YMsG!y!ZP%mpN5(pLR6VpWLFCvW| zX=!|lFf^O`i*8|XW+ENi2x;Q=7qO*89#2AJ1B)qGQUFtOx!B+m3fI%;S4@LaoPTAH zLiTmYLi&*+V4rPxTZgoy{je07=GfBFfvu@FEZ_`pI%bt>GsT4k6j!RYpu06V^I*^V z*!dvPDu-Hd=4aknNdd!>bD>5{HaQr)dbb&9XgeXLWPs}LDIuVLyR86~FV$$@`|MchJ>BCq=UKvFf8mO`t7dq%WCi|LrmAzx9gs-+D%SKM4uU9-|I+G0{Wqxp3G^T8f5N{fL;pYi{ck^?p1nPHJsK7DzdLcqsQ(>=-8g~z zpPTrH6Znh<^TE%*QCF2$t$*OKvv-Ch`B!T3%AXMu?1?56N%k(O%JEg}Z=!hU#~<^% z>4YRz7|gRT(|9?>mtR83i6jtKl@9T=ZiGgJGz`MX2Z03AM;t7{&GLak&yb!AL*B0@QS`k z^WiKbMMht>uI{qD^)MKIMJs|cI?M;zgeFpw#aXr06dL%?%5wbvqTzrJ4qGkR!x8#0 z`VuX=D*IryNeATZ;eT{?`^y2gAfdA`!f93cz*yEMG_FI@<=)@ZAAd||qg+EIR_(Nf zX5SiTUuL+*-^cM_jbC^>gcJ{BSqW z+MZ74>12f0X)(%DB-i7ntOJC+T3fW4x3j_heKxsmy&p_&7h`zYQFDgE=&!)BeYkg# z{%euV()?%o@Kx)7y$%kOm+C(Md_2X2od1k<39nl7^!prP#q8^7`p4uEE?VIKfd2#j z5BR^P{NLsIr;Fp0%ftEi`KtK8APG$QKMJw_5BR_J|Fg{RSOI?!|MO(-e~ABq`ybu^ z)EQ)WbGxeZUwQvWhW|HuA9UgV-^9Pqh_fT<34h<44{oo&EV41K1x|k83@YgF_8uI4 zxc@=^-!=I^?6<>ikbwNZv-iKdJCV=*A0(#zzmr6r81DZ~{JWV=?^|TI=hNvpZ)Nv5 zG>SgKRZl0QYZiZ!l<)2RwUtc|rw{3bWRXi#9BG52x*lj;R89-P=Zgo@2Mde|4if~( zeuS;*Wc)({3+oq9)rHv7e3DWxLh{E_-u0fnx$$+PiC{}EPwq|b~}HW&eER_T7$fGL$jOVFI}r& zdfmDq6&P@?IdodCS$=2qe4iei{XObOO*0pfV&D755!-)B-Z9YEN;mYyfP}cc-meI8 zV_hgCd%f=$gU5O@Cwe`kK}y%yt?>1}7Nk@H^4fern=NMJ*ZU14(ooL?W+3_;g6yn#eS^sPw~EcWIugtAOXw`tQkf{@*RS zRHN3HAFcfdbZ$tRzwvnguyvWH?^CMqT=_#Er5J|MPP10C+R_2mjz7 T{DTdDKL7rI9=m8^0Dutyo}UV6 delta 38682 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 diff --git a/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz b/web/api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz index 1cbe7905540aee7e7029009c6856659f4e593f70..6a8bc206aaf1bdbb2e9e00de83e1c186864e6da6 100644 GIT binary patch delta 3680 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 delta 3721 zcmV;44tDY49E%+XABzYGr4+)E2Pc31bK1DFKl`uHw9_%%fWtQ}Grad`APv(57$A3Z zuaof@BZx1?_E~ZhdUOB#(}!&&<0Q~)bG=vX4~e~!R=X?hu4Gm+PK=ZPOsxBJXf0v* z;+sH)!$bcom1ceSO!3ufgP_3ui)VlcVJjrve{CWEAy93C$XRyVtwyWfe4~HW-hd5u zHn3Z5*XnQIHtUtDQE#`~jb=l8q2T`yY<~&u8))Cc(6j=_L^oClm*tkxd}~xrzMlQJ zo6VhxRNGC-{XY%MKpm47DEIlk8cHKVGHtPSjd z{JEi}HTfjbX3=J2h1(98v2xIx3<1K?0^n@`5eO{%)>=cb@c|AFdBdVGSViY{~NS% z`D%L60q>$Un8LshF)AHO^29+Xf=DJb^P|v)htt3zgeMydVe)(j7biq%S|1`ibOOvY zHotLp3bB0;TaJgAR@Tsip@n1ham{gkg9XHQ5PDz}x!4I@2!IA+_$Vmzx^N(yMb-uM zmVxhh7#S%w?E~E2`Qd*ONkdQA3!N1PL>S;Yi;xVpHgPRX#&fe9fK%5($XPis?0`P} z1YJMaKo5f%ws3?%rvvJs40>*GV@Y*~9wwYnv7@B}JsZ-hF#$5qq`G8m)}gi8INlmu zSl&9KS|rscTYTet^r?1a{kk@Vzei38QJEt<;9uRE(JqO@Rz83ADU-N7C!^8<7~W$t z$l>kMzw`d_LGim-|23NX>%WFt|NR~7zgoKftG=z*8tn@8@M@X#gRcLkgI@n~U~HD( za{X7SRa@EhU$a_K>%ZrKueBoiIx7)nkLvr6`u?N7|0wuA;(vbW$rqT$QQ!Y-^;WZ@ zzW=NJpYs2I-~N9u_5W)Br~JR{|4q~J9Bi6Ku>BhQze=N)@&8R4F4X?-8Nf6x*ELPh z1s`?HHhFpp=wu^E)Izpo`n58Cwf|G`U&Vg~D*h+&KMfCuEdZX1|IKFh`){j8s;l__ z4Dd>O1$q(ocVBxKTQKLp_@4Tkz;R&+3KgSD{6JBowflc#FdEOnpg)|0tI@?^HUqzo zuRw1)0JAIdXMWzBGYtJ<82pmF(enOyIC{^B0H6J|iU7i)l(2hdne1b`Xm2+>R0NR=sBhVJ{CI(+oB^7`A4jrkA|0@2g_^;ys zQSqOzn}?;#STl;d-?Ju`+pAIfDC^O^r8(*tc3SLNHjw;-7MNGgylPH z3l_fbLdye=N6;(B#V|A^I`78wbJA^vy^PP!$a9rHkH}+9W6$Q#y^E{C%$$$SQSWjv zBPD;NNrRORcuA?eETyO_K@nO4rbaQN81ZLJmC>R^IeCdHC)oj&9SugZ1F9lsL=d%< zYOH^_K^XcWO-GPb*YC}H?|QRAD(-MRm`=yjROI)A(O}xUU~<#<-qrlv><>o6L7(92 z40ko6*zt7uUxsY(ij$#vIhbTAoD=jPdXO6n49 z`#}9BtY>r_3y{%sEQWxs6RaSNP|_eJqDX&2QIj?j1QUu{q(~4?C~1=tNyuY{Je5)& z_C!i~=o2Z^@n=|3$_}UtC6bgK&=4DmQg%R7C=#XYfR<1qOW6VK>@1wXaN{7FD4lP? z^BbBH?YjOQHQFGp4Up~AJp;UV5H`5pX1~tngG-t|3=tISB0$11SiAnhasl>P+D?BM zc^;)GrdgB7^(nD@GVeW7(Lunr5b>e3jSlaLi<(%WwLv=Z6VP1(QUPuPyp36yOvnAJ z)A>U@*uJ-N))7q(B1L85U6;h8B|TDESr>q&n#UcuzJH5=>)b-d!%i1mU7b|de*mY| z05Qp7pXgZB`Hl8p{a2 zE#YVL@syfy^^Js%Nmt(56Af@e`QZ5B4e3MfccUgU#wZi^8o`9ZjgK>Xyy5rGY zcD7gm*20f4n~-#>E{KVMg++|WTgriR{2`4aOzzS9bpD<7h`Z=h1)h#aqrvGsoq@B{ z^TA~=bv7m=QqL_KjnnY5D~h{mis<2JI5(3())GsuChw=cKJi|RpCiJ}_(LYp#9e1* z?w*{Pkh3psCi3jdoQXfO_JqZC3KC{S@e_7bArZYCv!(_$2h%iL4hb{w{EVvTHhA-07 za_ahr#K=-YT>VS90?ALw1q9tyX-BF%@;wrrVoLE_N{GJ~cQJytHqw8_WE?X*B~@a2 zq=Lj_N+IchQb@T@nGK<}j2U(420~!KS#gwMt}KL2vTde(kRa;$7*KA3<6lVU%I3u=1=88N?N4d$*OdED|m6sGZSO zL6%soTGj@1yL=mHkXSI0iBnh*Dz|$roh5Bo-LJk+c37|NSIeDUJFZEa47(Jdi(oz} z-V)t9L4oxyw}tMG<=P=E&*oF&5+31tH{qpmBkQa}(THb<9^rpg`Hfzr^vT}U_{0zR znKUkbtXwl@m`FJ^zKQ7dLqxwA&gPE|mte|dSDja{U_wf=UIHd^KEsMUA1}!IJ8wEB zK)2nd)1Z56VySCbh>7I$#KH}9;ima~Ly@l!!wLejoGEZ_ijNFcqJ;se{WS+VuifKq zGG0PvDC>}TY3hHp??MMB9?;+S+P>f|p_l8m4|l!(&Z!%`-+l5iD+hStN8KCo`-o*@GR+ZrmXG>?;5Aut1UCSrSb;vf6o3!9oVpA0sxd?~x z8h5W6`4@z|NNho|`%NxbqTXxNeI4rHH7MM|ZHclGW#$?ip?XX@8NlkM7_mzaKGh{PB32_MR209uC-G2noJCD~mAro!6j&F0PXb=Wp8*P0$t_Cm zl2_$_i>wX*R8Xj8y_4X!^sD*V8(nV3zatdtvW0(3rk}CpgvyykOsFVRt7}Cq`Sqe8 zsHCVZp$i0r`UP0!e}2RK4|V?UyUG7h=l@jx$5Z(q23)Vpba{4ixM>`B{;yeYsQdp^ z{-5&yD*sQxckKTUyC3lQ`~O;vy1M`GS>V9?|H@?a%j*6=b^o8b|4-fjr`CUJ{ii^! z|NMUwo*z3i@IC(W^#T1 zdbOeM|9u`%w*6y+ivKG9tN5?t|Ch!8!#00LPsaaR=KhCzrP5Zv|NM>PuPk20|7VQ< zD*x}hiT^79PsRUT{O8|Yy#0Tr-mJCN{{Kgh|A*f5_=WMm*2;eWuT|O&75|?F*foEJ zY?!g{yXjTM3_~yJ@RnwruZT456>xmR51_}+CPPT`_GnsXz>_9vCZk2#MggM{r`gqv zHfb2Y)Pe80+g$_O@&G;b&u=8-o%5>`9Y5iPc1Zhv5BbGRuJ5hEa+jY6;;R-R;K>a1 zqC=?NIrxS(E$C^EgI)sc(6QsKyYYXq3P|%o^N70zkesD&=sha;?u2Zw_W|qGHc^5vQn5f z&u26#m98%3Gd|Fyn?>+yQRsH{1G7#EW!A<{6EsoxBRzyVW&|xaO+$SJ&i#Kou(5Cy zvQz(TYQ!_<-}F+_4beC|*39mn6WReG$yCHP2=*PsGO|gK6Mm^2>HlPg>7_eq4gHBE z)pnU_rXP+hz3X2c*mj+#PvPy&hM;j^)3NQ--B!o|7`1=qqt1UCFf+?rva6=AxnV zgUNAL?lNv8OWDP@q780tE^bC{Uo_TfzSTR+Rpc0H6Q>_I#2& diff --git a/web/client/codechecker_client/cmd/store.py b/web/client/codechecker_client/cmd/store.py index 58e7f307a9..6889b6bdd1 100644 --- a/web/client/codechecker_client/cmd/store.py +++ b/web/client/codechecker_client/cmd/store.py @@ -32,7 +32,6 @@ from typing import Dict, Iterable, List, Set, Tuple from codechecker_api.codeCheckerDBAccess_v6.ttypes import StoreLimitKind -from codechecker_api_shared.ttypes import RequestFailed, ErrorCode from codechecker_report_converter import twodim from codechecker_report_converter.report import Report, report_file, \ @@ -58,7 +57,7 @@ def assemble_blame_info(_, __) -> int: from codechecker_common.compatibility.multiprocessing import Pool from codechecker_common.source_code_comment_handler import \ SourceCodeCommentHandler -from codechecker_common.util import load_json +from codechecker_common.util import format_size, load_json from codechecker_web.shared import webserver_context, host_check from codechecker_web.shared.env import get_default_workspace @@ -66,7 +65,7 @@ def assemble_blame_info(_, __) -> int: LOG = logger.get_logger('system') -MAX_UPLOAD_SIZE = 1 * 1024 * 1024 * 1024 # 1GiB +MAX_UPLOAD_SIZE = 1024 ** 3 # 1024^3 = 1 GiB. AnalyzerResultFileReports = Dict[str, List[Report]] @@ -87,7 +86,7 @@ def assemble_blame_info(_, __) -> int: """Contains information about the report file after parsing. -store_it: True if every information is availabe and the +store_it: True if every information is available and the report can be stored main_report_positions: list of ReportLineInfo containing the main report positions @@ -135,19 +134,6 @@ def _write_summary(self, out=sys.stdout): out.write("\n----=================----\n") -def sizeof_fmt(num, suffix='B'): - """ - Pretty print storage units. - Source: https://stackoverflow.com/questions/1094841/ - reusable-library-to-get-human-readable-version-of-file-size - """ - for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: - if abs(num) < 1024.0: - return f"{num:3.1f}{unit}{suffix}" - num /= 1024.0 - return f"{num:.1f}Yi{suffix}" - - def get_file_content_hash(file_path): """ Return the file content hash for a file. @@ -649,7 +635,7 @@ def assemble_zip(inputs, compressed_zip_size = os.stat(zip_file).st_size LOG.info("Compressing report zip file done (%s / %s).", - sizeof_fmt(zip_size), sizeof_fmt(compressed_zip_size)) + format_size(zip_size), format_size(compressed_zip_size)) # We are responsible for deleting these. shutil.rmtree(temp_dir) @@ -698,7 +684,7 @@ def get_analysis_statistics(inputs, limits): if os.stat(compilation_db).st_size > compilation_db_size: LOG.debug("Compilation database is too big (max: %s).", - sizeof_fmt(compilation_db_size)) + format_size(compilation_db_size)) else: LOG.debug("Copying file '%s' to analyzer statistics " "ZIP...", compilation_db) @@ -721,7 +707,7 @@ def get_analysis_statistics(inputs, limits): if failed_files_size > failure_zip_limit: LOG.debug("We reached the limit of maximum uploadable " "failure zip size (max: %s).", - sizeof_fmt(failure_zip_limit)) + format_size(failure_zip_limit)) break else: LOG.debug("Copying failure zip file '%s' to analyzer " @@ -932,7 +918,7 @@ def main(args): zip_size = os.stat(zip_file).st_size if zip_size > MAX_UPLOAD_SIZE: LOG.error("The result list to upload is too big (max: %s): %s.", - sizeof_fmt(MAX_UPLOAD_SIZE), sizeof_fmt(zip_size)) + format_size(MAX_UPLOAD_SIZE), format_size(zip_size)) sys.exit(1) b64zip = "" @@ -1005,15 +991,6 @@ def main(args): storing_analysis_statistics(client, args.input, args.name) LOG.info("Storage finished successfully.") - except RequestFailed as reqfail: - if reqfail.errorCode == ErrorCode.SOURCE_FILE: - header = ['File', 'Line', 'Checker name'] - table = twodim.to_str( - 'table', header, [c.split('|') for c in reqfail.extraInfo]) - LOG.warning("Setting the review statuses for some reports failed " - "because of non valid source code comments: " - "%s\n %s", reqfail.message, table) - sys.exit(1) except Exception as ex: import traceback traceback.print_exc() diff --git a/web/client/codechecker_client/thrift_call.py b/web/client/codechecker_client/thrift_call.py index 32e5b3dc18..d41f7d7187 100644 --- a/web/client/codechecker_client/thrift_call.py +++ b/web/client/codechecker_client/thrift_call.py @@ -81,7 +81,11 @@ def wrapper(self, *args, **kwargs): LOG.error( 'Client/server API mismatch\n %s', str(reqfailure.message)) else: - LOG.error('API call error: %s\n%s', func_name, str(reqfailure)) + LOG.error("Error during API call: %s", func_name) + LOG.debug("%s", str(reqfailure)) + LOG.error("%s", str(reqfailure.message)) + if reqfailure.extraInfo: + LOG.error("%s", '\n'.join(reqfailure.extraInfo)) sys.exit(1) except TApplicationException as ex: LOG.error("Internal server error: %s", str(ex.message)) diff --git a/web/server/codechecker_server/api/mass_store_run.py b/web/server/codechecker_server/api/mass_store_run.py index 87ab4e2a52..743550f397 100644 --- a/web/server/codechecker_server/api/mass_store_run.py +++ b/web/server/codechecker_server/api/mass_store_run.py @@ -5,31 +5,36 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # # ------------------------------------------------------------------------- +""" +Implementation of the ``massStoreRunAsynchronous()`` API function that store +run data to a product's report database. +Called via `report_server`, but factored out here for readability. +""" import base64 +from collections import defaultdict +from datetime import datetime, timedelta +from hashlib import sha256 import json import os +from pathlib import Path import sqlalchemy import tempfile import time +from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast import zipfile import zlib -from collections import defaultdict -from datetime import datetime, timedelta -from hashlib import sha256 -from tempfile import TemporaryDirectory -from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast - -import codechecker_api_shared +from codechecker_api_shared.ttypes import DBStatus, ErrorCode, RequestFailed from codechecker_api.codeCheckerDBAccess_v6 import ttypes from codechecker_common import skiplist_handler from codechecker_common.logger import get_logger from codechecker_common.review_status_handler import ReviewStatusHandler, \ SourceReviewStatus -from codechecker_common.util import load_json, path_for_fake_root +from codechecker_common.util import format_size, load_json, path_for_fake_root +from codechecker_report_converter import twodim from codechecker_report_converter.util import trim_path_prefixes from codechecker_report_converter.report import \ FakeChecker, Report, UnknownChecker, report_file @@ -45,10 +50,12 @@ ExtendedReportData, \ File, FileContent, \ Report as DBReport, ReportAnnotations, ReviewStatus as ReviewStatusRule, \ - Run, RunLock, RunHistory + Run, RunLock as DBRunLock, RunHistory from ..metadata import checker_is_unavailable, MetadataInfoParser - -from .report_server import ThriftRequestHandler +from ..product import Product as ServerProduct +from ..session_manager import SessionManager +from ..task_executors.abstract_task import AbstractTask +from ..task_executors.task_manager import TaskManager from .thrift_enum_helper import report_extended_data_type_str @@ -56,32 +63,37 @@ STORE_TIME_LOG = get_logger('store_time') -class LogTask: +class StepLog: + """ + Simple context manager that logs an arbitrary step's comment and time + taken annotated with a run name. + """ + def __init__(self, run_name: str, message: str): - self.__run_name = run_name - self.__msg = message - self.__start_time = time.time() + self._run_name = run_name + self._msg = message + self._start_time = time.time() - def __enter__(self, *args): - LOG.info("[%s] %s...", self.__run_name, self.__msg) + def __enter__(self, *_args): + LOG.info("[%s] %s...", self._run_name, self._msg) - def __exit__(self, *args): - LOG.info("[%s] %s. Done. (Duration: %s sec)", self.__run_name, - self.__msg, round(time.time() - self.__start_time, 2)) + def __exit__(self, *_args): + LOG.info("[%s] %s. Done. (Duration: %.2f sec)", + self._run_name, self._msg, time.time() - self._start_time) -class RunLocking: +class RunLock: def __init__(self, session: DBSession, run_name: str): self.__session = session self.__run_name = run_name self.__run_lock = None - def __enter__(self, *args): + def __enter__(self, *_args): # Load the lock record for "FOR UPDATE" so that the transaction that # handles the run's store operations has a lock on the database row # itself. - self.__run_lock = self.__session.query(RunLock) \ - .filter(RunLock.name == self.__run_name) \ + self.__run_lock = self.__session.query(DBRunLock) \ + .filter(DBRunLock.name == self.__run_name) \ .with_for_update(nowait=True) \ .one() @@ -98,39 +110,161 @@ def __enter__(self, *args): self.__run_name, self.__run_lock.locked_at) return self - def __exit__(self, *args): + def __exit__(self, *_args): self.__run_lock = None self.__session = None + def store_run_lock_in_db(self, associated_user: str): + """ + Stores a `DBRunLock` record for the given run name into the database. + """ + try: + # If the run can be stored, we need to lock it first. If there is + # already a lock in the database for the given run name which is + # expired and multiple processes are trying to get this entry from + # the database for update we may get the following exception: + # could not obtain lock on row in relation "run_locks" + # This is the reason why we have to wrap this query to a try/except + # block. + run_lock: Optional[DBRunLock] = self.__session.query(DBRunLock) \ + .filter(DBRunLock.name == self.__run_name) \ + .with_for_update(nowait=True) \ + .one_or_none() + except (sqlalchemy.exc.OperationalError, + sqlalchemy.exc.ProgrammingError) as ex: + LOG.error("Failed to get run lock for '%s': %s", + self.__run_name, ex) + raise RequestFailed( + ErrorCode.DATABASE, + "Someone is already storing to the same run. Please wait " + "while the other storage is finished and try it again.") \ + from ex -def unzip(b64zip: str, output_dir: str) -> int: + if not run_lock: + # If there is no lock record for the given run name, the run + # is not locked -> create a new lock. + self.__session.add(DBRunLock(self.__run_name, associated_user)) + LOG.debug("Acquiring 'run_lock' for '%s' on run '%s' ...", + associated_user, self.__run_name) + elif run_lock.has_expired( + db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE): + # There can be a lock in the database, which has already + # expired. In this case, we assume that the previous operation + # has failed, and thus, we can re-use the already present lock. + run_lock.touch() + run_lock.username = associated_user + LOG.debug("Reusing existing, stale 'run_lock' record on " + "run '%s' ...", + self.__run_name) + else: + # In case the lock exists and it has not expired, we must + # consider the run a locked one. + when = run_lock.when_expires( + db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE) + username = run_lock.username or "another user" + LOG.info("Refusing to store into run '%s' as it is locked by " + "%s. Lock will expire at '%s'.", + self.__run_name, username, when) + raise RequestFailed( + ErrorCode.DATABASE, + f"The run named '{self.__run_name}' is being stored into by " + f"{username}. If the other store operation has failed, this " + f"lock will expire at '{when}'.") + + # At any rate, if the lock has been created or updated, commit it + # into the database. + try: + self.__session.commit() + except (sqlalchemy.exc.IntegrityError, + sqlalchemy.orm.exc.StaleDataError) as ex: + # The commit of this lock can fail. + # + # In case two store ops attempt to lock the same run name at the + # same time, committing the lock in the transaction that commits + # later will result in an IntegrityError due to the primary key + # constraint. + # + # In case two store ops attempt to lock the same run name with + # reuse and one of the operation hangs long enough before COMMIT + # so that the other operation commits and thus removes the lock + # record, StaleDataError is raised. In this case, also consider + # the run locked, as the data changed while the transaction was + # waiting, as another run wholly completed. + + LOG.info("Run '%s' got locked while current transaction " + "tried to acquire a lock. Considering run as locked.", + self.__run_name) + raise RequestFailed( + ErrorCode.DATABASE, + f"The run named '{self.__run_name}' is being stored into by " + "another user.") from ex + + LOG.debug("Successfully acquired 'run_lock' for '%s' on run '%s'.", + associated_user, self.__run_name) + + def drop_run_lock_from_db(self): + """Remove the run_lock row from the database for the current run.""" + # Using with_for_update() here so the database (in case it supports + # this operation) locks the lock record's row from any other access. + LOG.debug("Releasing 'run_lock' from run '%s' ...") + run_lock: Optional[DBRunLock] = self.__session.query(DBRunLock) \ + .filter(DBRunLock.name == self.__run_name) \ + .with_for_update(nowait=True).one() + if not run_lock: + raise KeyError( + f"No 'run_lock' in database for run '{self.__run_name}'") + locked_at = run_lock.locked_at + username = run_lock.username + + self.__session.delete(run_lock) + self.__session.commit() + + LOG.debug("Released 'run_lock' (originally acquired by '%s' on '%s') " + "from run '%s'.", + username, str(locked_at), self.__run_name) + + +def unzip(run_name: str, b64zip: str, output_dir: Path) -> int: """ - This function unzips the base64 encoded zip file. This zip is extracted - to a temporary directory and the ZIP is then deleted. The function returns - the size of the extracted decompressed zip file. + This function unzips a Base64 encoded and ZLib-compressed ZIP file. + This ZIP is extracted to a temporary directory and the ZIP is then deleted. + The function returns the size of the extracted decompressed ZIP file. """ - if len(b64zip) == 0: + if not b64zip: return 0 - with tempfile.NamedTemporaryFile(suffix='.zip') as zip_file: - LOG.debug("Unzipping mass storage ZIP '%s' to '%s'...", - zip_file.name, output_dir) - + with tempfile.NamedTemporaryFile( + suffix=".zip", dir=output_dir) as zip_file: + LOG.debug("Decompressing input massStoreRun() ZIP to '%s' ...", + zip_file.name) + start_time = time.time() zip_file.write(zlib.decompress(base64.b64decode(b64zip))) - with zipfile.ZipFile(zip_file, 'r', allowZip64=True) as zipf: + zip_file.flush() + end_time = time.time() + + size = os.stat(zip_file.name).st_size + LOG.debug("Decompressed input massStoreRun() ZIP '%s' -> '%s' " + "(compression ratio: %.2f%%) in '%s'.", + format_size(len(b64zip)), format_size(size), + (size / len(b64zip)), + timedelta(seconds=end_time - start_time)) + + with StepLog(run_name, "Extract massStoreRun() ZIP contents"), \ + zipfile.ZipFile(zip_file, 'r', allowZip64=True) as zip_handle: + LOG.debug("Extracting massStoreRun() ZIP '%s' to '%s' ...", + zip_file.name, output_dir) try: - zipf.extractall(output_dir) - return os.stat(zip_file.name).st_size + zip_handle.extractall(output_dir) + return size except Exception: LOG.error("Failed to extract received ZIP.") import traceback traceback.print_exc() raise - return 0 def get_file_content(file_path: str) -> bytes: - """Return the file content for the given filepath. """ + """Return the file content for the given `file_path`.""" with open(file_path, 'rb') as f: return f.read() @@ -202,7 +336,7 @@ def add_file_record( def get_blame_file_data( - blame_file: str + blame_file: Path ) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Get blame information from the given file. @@ -214,7 +348,7 @@ def get_blame_file_data( remote_url = None tracking_branch = None - if os.path.isfile(blame_file): + if blame_file.is_file(): data = load_json(blame_file) if data: remote_url = data.get("remote_url") @@ -234,199 +368,303 @@ def checker_name_for_report(report: Report) -> Tuple[str, str]: report.checker_name or UnknownChecker[1]) -class MassStoreRun: - def __init__( - self, - report_server: ThriftRequestHandler, - name: str, - tag: Optional[str], - version: Optional[str], - b64zip: str, - force: bool, - trim_path_prefix_list: Optional[List[str]], - description: Optional[str] - ): - """ Initialize object. """ - self.__report_server = report_server +class MassStoreRunInputHandler: + """Prepares a `MassStoreRunTask` from an API input.""" + + # Note: The implementation of this class is executed in the "foreground", + # in the context of an API handler process! + # **DO NOT** put complex logic here that would take too much time to + # validate. + # Long-running actions of a storage process should be in + # MassStoreRunImplementation instead! + + def __init__(self, + session_manager: SessionManager, + config_db_sessionmaker, + product_db_sessionmaker, + task_manager: TaskManager, + package_context, + product_id: int, + run_name: str, + run_description: Optional[str], + store_tag: Optional[str], + client_version: str, + force_overwrite_of_run: bool, + path_prefixes_to_trim: Optional[List[str]], + zipfile_contents_base64: str, + user_name: str): + self._input_handling_start_time = time.time() + self._session_manager = session_manager + self._config_db = config_db_sessionmaker + self._product_db = product_db_sessionmaker + self._tm = task_manager + self._package_context = package_context + self._input_zip_blob = zipfile_contents_base64 + self.client_version = client_version + self.force_overwrite_of_run = force_overwrite_of_run + self.path_prefixes_to_trim = path_prefixes_to_trim + self.run_name = run_name + self.run_description = run_description + self.store_tag = store_tag + self.user_name = user_name + + with DBSession(self._config_db) as session: + product: Optional[Product] = session.query(Product) \ + .get(product_id) + if not product: + raise KeyError(f"No product with ID '{product_id}'") + + self._product = product + + def check_store_input_validity_at_face_value(self): + """ + Performs semantic checks of a ``massStoreRunAsynchronous()`` Thrift + call that can be done with trivial amounts of work (i.e., without + actually parsing the full input ZIP). + """ + self._check_run_limit() + self._store_run_lock() # Fails if the run can not be stored into. - self.__name = name - self.__tag = tag - self.__version = version - self.__b64zip = b64zip - self.__force = force - self.__trim_path_prefixes = trim_path_prefix_list - self.__description = description + def create_mass_store_task(self, + is_actually_asynchronous=False) \ + -> "MassStoreRunTask": + """ + Constructs the `MassStoreRunTask` for the handled and verified input. - self.__mips: Dict[str, MetadataInfoParser] = {} - self.__analysis_info: Dict[str, AnalysisInfo] = {} - self.__checker_row_cache: Dict[Tuple[str, str], Checker] = {} - 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.__new_report_hashes: Dict[str, Tuple] = {} - self.__all_report_checkers: Set[str] = set() - self.__added_reports: List[Tuple[DBReport, Report]] = [] - self.__reports_with_fake_checkers: Dict[ - # Either a DBReport *without* an ID, or the ID of a committed - # DBReport. - str, Tuple[Report, Union[DBReport, int]]] = {} + Calling this function results in observable changes outside the + process's memory, as it records the task into the database and + extracts things to the server's storage area. + """ + token = self._tm.allocate_task_record( + "report_server::massStoreRunAsynchronous()" + if is_actually_asynchronous + else "report_server::massStoreRun()", + ("Legacy s" if not is_actually_asynchronous else "S") + + f"tore of results to '{self._product.endpoint}' - " + f"'{self.run_name}'", + self.user_name, + self._product) + temp_dir = self._tm.create_task_data(token) + extract_dir = temp_dir / "store_zip" + os.makedirs(extract_dir, exist_ok=True) - self.__get_report_limit_for_product() + try: + with StepLog(self.run_name, + "Save massStoreRun() ZIP data to server storage"): + zip_size = unzip(self.run_name, + self._input_zip_blob, + extract_dir) + + if not zip_size: + raise RequestFailed(ErrorCode.GENERAL, + "The uploaded ZIP file is empty!") + except Exception: + LOG.error("Failed to extract massStoreRunAsynchronous() ZIP!") + import traceback + traceback.print_exc() + raise - @property - def __manager(self): - return self.__report_server._manager + self._input_handling_end_time = time.time() - @property - def __config_database(self): - return self.__report_server._config_database + try: + with open(temp_dir / "store_configuration.json", 'w', + encoding="utf-8") as cfg_f: + json.dump({ + "client_version": self.client_version, + "force_overwrite": self.force_overwrite_of_run, + "path_prefixes_to_trim": self.path_prefixes_to_trim, + "run_name": self.run_name, + "run_description": self.run_description, + "store_tag": self.store_tag, + "user_name": self.user_name, + }, cfg_f) + except Exception: + LOG.error("Failed to write massStoreRunAsynchronous() " + "configuration!") + import traceback + traceback.print_exc() + raise - @property - def __product(self): - return self.__report_server._product + task = MassStoreRunTask(token, temp_dir, + self._package_context, + self._product.id, + zip_size, + self._input_handling_end_time - + self._input_handling_start_time) - @property - def __context(self): - return self.__report_server._context + if not is_actually_asynchronous: + self._tm.add_comment( + task, + "WARNING!\nExecuting a legacy 'massStoreRun()' API call!", + "SYSTEM") - @property - def user_name(self): - return self.__report_server._get_username() + return task - def __check_run_limit(self): + def _check_run_limit(self): """ - Checks the maximum allowed of uploadable runs for the current product. + Checks the maximum allowed number of uploadable runs for the current + product. """ - max_run_count = self.__manager.get_max_run_count() - - with DBSession(self.__config_database) as session: - product = session.query(Product).get(self.__product.id) - if product.run_limit: - max_run_count = product.run_limit - - # Session that handles constraints on the run. - with DBSession(self.__report_server._Session) as session: - if not max_run_count: - return - - LOG.debug("Check the maximum number of allowed runs which is %d", - max_run_count) - - run = session.query(Run) \ - .filter(Run.name == self.__name) \ + run_limit: Optional[int] = self._session_manager.get_max_run_count() + if self._product.run_limit: + run_limit = self._product.run_limit + + if not run_limit: + # Allowing the user to upload an unlimited number of runs. + return + LOG.debug("Checking the maximum number of allowed runs in '%s', " + "which is %d.", + self._product.endpoint, run_limit) + + with DBSession(self._product_db) as session: + existing_run: Optional[Run] = session.query(Run) \ + .filter(Run.name == self.run_name) \ .one_or_none() - - # If max_run_count is not set in the config file, it will allow - # the user to upload unlimited runs. - run_count = session.query(Run.id).count() - # If we are not updating a run or the run count is reached the - # limit it will throw an exception. - if not run and run_count >= max_run_count: - remove_run_count = run_count - max_run_count + 1 - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, - f"You reached the maximum number of allowed runs " - f"({run_count}/{max_run_count})! Please remove at least " - f"{remove_run_count} run(s) before you try it again.") - - def __store_run_lock(self, session: DBSession): + if not existing_run and run_count >= run_limit: + raise RequestFailed( + ErrorCode.GENERAL, + "You reached the maximum number of allowed runs " + f"({run_count}/{run_limit})! " + f"Please remove at least {run_count - run_limit + 1} " + "run(s) before you try again!") + + def _store_run_lock(self): + """Commits a `DBRunLock` for the to-be-stored `Run`, if available.""" + with DBSession(self._product_db) as session: + RunLock(session, self.run_name) \ + .store_run_lock_in_db(self.user_name) + + +class MassStoreRunTask(AbstractTask): + """Executes `MassStoreRun` as a background job.""" + + def __init__(self, token: str, data_path: Path, + package_context, + product_id: int, + input_zip_size: int, + preparation_time_elapsed: float): """ - Store a RunLock record for the given run name into the database. + Creates the `AbstractTask` implementation for + ``massStoreRunAsynchronous()``. + + `preparation_time_elapsed` records how much time was spent by the + input handling that prepared the task. + This time will be added to the total time spent processing the results + in the background. + (The time spent in waiting between task enschedulement and task + execution is not part of the total time.) """ - try: - # If the run can be stored, we need to lock it first. If there is - # already a lock in the database for the given run name which is - # expired and multiple processes are trying to get this entry from - # the database for update we may get the following exception: - # could not obtain lock on row in relation "run_locks" - # This is the reason why we have to wrap this query to a try/except - # block. - run_lock = session.query(RunLock) \ - .filter(RunLock.name == self.__name) \ - .with_for_update(nowait=True).one_or_none() - except (sqlalchemy.exc.OperationalError, - sqlalchemy.exc.ProgrammingError) as ex: - LOG.error("Failed to get run lock for '%s': %s", self.__name, ex) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - "Someone is already storing to the same run. Please wait " - "while the other storage is finished and try it again.") + super().__init__(token, data_path) + self._package_context = package_context + self._product_id = product_id + self.input_zip_size = input_zip_size + self.time_spent_on_task_preparation = preparation_time_elapsed - if not run_lock: - # If there is no lock record for the given run name, the run - # is not locked -- create a new lock. - run_lock = RunLock(self.__name, self.user_name) - session.add(run_lock) - elif run_lock.has_expired( - db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE): - # There can be a lock in the database, which has already - # expired. In this case, we assume that the previous operation - # has failed, and thus, we can re-use the already present lock. - run_lock.touch() - run_lock.username = self.user_name - else: - # In case the lock exists and it has not expired, we must - # consider the run a locked one. - when = run_lock.when_expires( - db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE) + def _implementation(self, tm: TaskManager): + try: + with open(self.data_path / "store_configuration.json", 'r', + encoding="utf-8") as cfg_f: + self.store_configuration = json.load(cfg_f) + except Exception: + LOG.error("Invalid or unusable massStoreRunAsynchronous() " + "configuration!") + raise - username = run_lock.username if run_lock.username is not None \ - else "another user" + with DBSession(tm.configuration_database_session_factory) as session: + db_product: Optional[Product] = session.query(Product) \ + .get(self._product_id) + if not db_product: + raise KeyError(f"No product with ID '{self._product_id}'") + + self._product = ServerProduct(db_product.id, + db_product.endpoint, + db_product.display_name, + db_product.connection, + self._package_context, + tm.environment) + + self._product.connect() + if self._product.db_status != DBStatus.OK: + raise EnvironmentError("Database for product " + f"'{self._product.endpoint}' is in " + "a bad shape!") + + m = MassStoreRun(self.data_path / "store_zip", + self._package_context, + tm.configuration_database_session_factory, + self._product, + self.store_configuration["run_name"], + self.store_configuration["store_tag"], + self.store_configuration["client_version"], + self.store_configuration["force_overwrite"], + self.store_configuration["path_prefixes_to_trim"], + self.store_configuration["run_description"], + self.store_configuration["user_name"], + ) + m.store(self.input_zip_size, self.time_spent_on_task_preparation) - LOG.info("Refusing to store into run '%s' as it is locked by " - "%s. Lock will expire at '%s'.", self.__name, username, - when) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - f"The run named '{self.__name}' is being stored into by " - f"{username}. If the other store operation has failed, this " - f"lock will expire at '{when}'.") - # At any rate, if the lock has been created or updated, commit it - # into the database. - try: - session.commit() - except (sqlalchemy.exc.IntegrityError, - sqlalchemy.orm.exc.StaleDataError) as ex: - # The commit of this lock can fail. - # - # In case two store ops attempt to lock the same run name at the - # same time, committing the lock in the transaction that commits - # later will result in an IntegrityError due to the primary key - # constraint. - # - # In case two store ops attempt to lock the same run name with - # reuse and one of the operation hangs long enough before COMMIT - # so that the other operation commits and thus removes the lock - # record, StaleDataError is raised. In this case, also consider - # the run locked, as the data changed while the transaction was - # waiting, as another run wholly completed. +class MassStoreRun: + """Implementation for ``massStoreRunAsynchronous()``.""" + + # Note: The implementation of this class is called from MassStoreRunTask + # and it is executed in the background, in the context of a Task worker + # process. + # This is the place where complex implementation logic must go, but be + # careful, there is no way to communicate with the user's client anymore! + + # TODO: Poll the task manager at regular points for a cancel signal! + + def __init__(self, + zip_dir: Path, + package_context, + config_db, + product: ServerProduct, + name: str, + tag: Optional[str], + version: Optional[str], + force: bool, + trim_path_prefix_list: Optional[List[str]], + description: Optional[str], + user_name: str, + ): + self._zip_dir = zip_dir + self._name = name + self._tag = tag + self._version = version + self._force = force + self._trim_path_prefixes = trim_path_prefix_list + self._description = description + self._user_name = user_name + self.__config_db = config_db + self.__package_context = package_context + self.__product = product - LOG.info("Run '%s' got locked while current transaction " - "tried to acquire a lock. Considering run as locked.", - self.__name) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - f"The run named '{self.__name}' is being stored into by " - "another user.") from ex + self.__mips: Dict[str, MetadataInfoParser] = {} + self.__analysis_info: Dict[str, AnalysisInfo] = {} + self.__checker_row_cache: Dict[Tuple[str, str], Checker] = {} + 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.__new_report_hashes: Dict[str, Tuple] = {} + self.__all_report_checkers: Set[str] = set() + self.__added_reports: List[Tuple[DBReport, Report]] = [] + self.__reports_with_fake_checkers: Dict[ + # Either a DBReport *without* an ID, or the ID of a committed + # DBReport. + str, Tuple[Report, Union[DBReport, int]]] = {} - def __free_run_lock(self, session: DBSession): - """ Remove the lock from the database for the given run name. """ - # Using with_for_update() here so the database (in case it supports - # this operation) locks the lock record's row from any other access. - run_lock = session.query(RunLock) \ - .filter(RunLock.name == self.__name) \ - .with_for_update(nowait=True).one() - session.delete(run_lock) - session.commit() + with DBSession(config_db) as session: + product = session.query(Product).get(self.__product.id) + self.__report_limit = product.report_limit def __store_source_files( self, - source_root: str, + source_root: Path, filename_to_hash: Dict[str, str] ) -> Dict[str, int]: """ Storing file contents from plist. """ @@ -434,10 +672,10 @@ def __store_source_files( file_path_to_id = {} for file_name, file_hash in filename_to_hash.items(): - source_file_path = path_for_fake_root(file_name, source_root) + source_file_path = path_for_fake_root(file_name, str(source_root)) LOG.debug("Storing source file: %s", source_file_path) trimmed_file_path = trim_path_prefixes( - file_name, self.__trim_path_prefixes) + file_name, self._trim_path_prefixes) if not os.path.isfile(source_file_path): # The file was not in the ZIP file, because we already @@ -445,7 +683,7 @@ def __store_source_files( # record in the database or we need to add one. LOG.debug('%s not found or already stored.', trimmed_file_path) - with DBSession(self.__report_server._Session) as session: + with DBSession(self.__product.session_factory) as session: fid = add_file_record( session, trimmed_file_path, file_hash) @@ -458,7 +696,7 @@ def __store_source_files( source_file_path, file_hash) continue - with DBSession(self.__report_server._Session) as session: + with DBSession(self.__product.session_factory) as session: self.__add_file_content(session, source_file_path, file_hash) file_path_to_id[trimmed_file_path] = add_file_record( @@ -468,7 +706,7 @@ def __store_source_files( def __add_blame_info( self, - blame_root: str, + blame_root: Path, filename_to_hash: Dict[str, str] ): """ @@ -480,11 +718,11 @@ def __add_blame_info( .zip file. This function stores blame info even if the corresponding source file is not in the .zip file. """ - with DBSession(self.__report_server._Session) as session: + with DBSession(self.__product.session_factory) as session: for subdir, _, files in os.walk(blame_root): for f in files: - blame_file = os.path.join(subdir, f) - file_path = blame_file[len(blame_root.rstrip("/")):] + blame_file = Path(subdir) / f + file_path = f"/{str(blame_file.relative_to(blame_root))}" blame_info, remote_url, tracking_branch = \ get_blame_file_data(blame_file) @@ -599,8 +837,8 @@ def __store_checker_identifiers(self, checkers: Set[Tuple[str, str]]): while tries < max_tries: tries += 1 try: - LOG.debug("[%s] Begin attempt %d...", self.__name, tries) - with DBSession(self.__report_server._Session) as session: + LOG.debug("[%s] Begin attempt %d...", self._name, tries) + with DBSession(self.__product.session_factory) as session: known_checkers = {(r.analyzer_name, r.checker_name) for r in session .query(Checker.analyzer_name, @@ -608,7 +846,8 @@ def __store_checker_identifiers(self, checkers: Set[Tuple[str, str]]): .all()} for analyzer, checker in \ sorted(all_checkers - known_checkers): - s = self.__context.checker_labels.severity(checker) + s = self.__package_context.checker_labels \ + .severity(checker) s = ttypes.Severity._NAMES_TO_VALUES[s] session.add(Checker(analyzer, checker, s)) LOG.debug("Acquiring ID for checker '%s/%s' " @@ -620,7 +859,7 @@ def __store_checker_identifiers(self, checkers: Set[Tuple[str, str]]): sqlalchemy.exc.ProgrammingError) as ex: LOG.error("Storing checkers of run '%s' failed: %s.\n" "Waiting %d before trying again...", - self.__name, ex, wait_time) + self._name, ex, wait_time) time.sleep(wait_time.total_seconds()) wait_time *= 2 except Exception as ex: @@ -630,10 +869,9 @@ def __store_checker_identifiers(self, checkers: Set[Tuple[str, str]]): traceback.print_exc() raise - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - "Storing the names of the checkers in the run failed due to " - "excessive contention!") + raise ConnectionRefusedError("Storing the names of the checkers in " + "the run failed due to excessive " + "contention!") def __store_analysis_statistics( self, @@ -661,7 +899,7 @@ def __store_analysis_statistics( stats[analyzer_type]["versions"].add(res["version"]) if "failed_sources" in res: - if self.__version == '6.9.0': + if self._version == '6.9.0': stats[analyzer_type]["failed_sources"].add( 'Unavailable in CodeChecker 6.9.0!') else: @@ -757,89 +995,82 @@ def __add_or_update_run( By default updates the results if name already exists. Using the force flag removes existing analysis results for a run. """ - try: - LOG.debug("Adding run '%s'...", self.__name) + LOG.debug("Adding run '%s'...", self._name) + + run = session.query(Run) \ + .filter(Run.name == self._name) \ + .one_or_none() + + update_run = True + if run and self._force: + # Clean already collected results. + if not run.can_delete: + # Deletion is already in progress. + msg = f"Can't delete {run.id}" + LOG.debug(msg) + raise EnvironmentError(msg) + + LOG.info('Removing previous analysis results...') + session.delete(run) + # Not flushing after delete leads to a constraint violation + # error later, when adding run entity with the same name as + # the old one. + session.flush() - run = session.query(Run) \ - .filter(Run.name == self.__name) \ - .one_or_none() + checker_run = Run(self._name, self._version) + session.add(checker_run) + session.flush() + run_id = checker_run.id - update_run = True - if run and self.__force: - # Clean already collected results. - if not run.can_delete: - # Deletion is already in progress. - msg = f"Can't delete {run.id}" - LOG.debug(msg) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - msg) - - LOG.info('Removing previous analysis results...') - session.delete(run) - # Not flushing after delete leads to a constraint violation - # error later, when adding run entity with the same name as - # the old one. - session.flush() - - checker_run = Run(self.__name, self.__version) - session.add(checker_run) - session.flush() - run_id = checker_run.id - - elif run: - # There is already a run, update the results. - run.date = datetime.now() - run.duration = -1 - session.flush() - run_id = run.id - else: - # There is no run create new. - checker_run = Run(self.__name, self.__version) - session.add(checker_run) - session.flush() - run_id = checker_run.id - update_run = False - - # Add run to the history. - LOG.debug("Adding run history.") - - if self.__tag is not None: - run_history = session.query(RunHistory) \ - .filter(RunHistory.run_id == run_id, - RunHistory.version_tag == self.__tag) \ - .one_or_none() - - if run_history: - run_history.version_tag = None - session.add(run_history) - - cc_versions = set() - for mip in self.__mips.values(): - if mip.cc_version: - cc_versions.add(mip.cc_version) - - cc_version = '; '.join(cc_versions) if cc_versions else None - run_history = RunHistory( - run_id, self.__tag, self.user_name, run_history_time, - cc_version, self.__description) - - session.add(run_history) + elif run: + # There is already a run, update the results. + run.date = datetime.now() + run.duration = -1 session.flush() + run_id = run.id + else: + # There is no run create new. + checker_run = Run(self._name, self._version) + session.add(checker_run) + session.flush() + run_id = checker_run.id + update_run = False + + # Add run to the history. + LOG.debug("Adding run history.") - LOG.debug("Adding run done.") + if self._tag is not None: + run_history = session.query(RunHistory) \ + .filter(RunHistory.run_id == run_id, + RunHistory.version_tag == self._tag) \ + .one_or_none() - self.__store_analysis_statistics(session, run_history.id) - self.__store_analysis_info(session, run_history) + if run_history: + run_history.version_tag = None + session.add(run_history) - session.flush() - LOG.debug("Storing analysis statistics done.") + cc_versions = set() + for mip in self.__mips.values(): + if mip.cc_version: + cc_versions.add(mip.cc_version) - return run_id, update_run - except Exception as ex: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, - str(ex)) + cc_version = '; '.join(cc_versions) if cc_versions else None + run_history = RunHistory( + run_id, self._tag, self._user_name, run_history_time, + cc_version, self._description) + + session.add(run_history) + session.flush() + + LOG.debug("Adding run done.") + + self.__store_analysis_statistics(session, run_history.id) + self.__store_analysis_info(session, run_history) + + session.flush() + LOG.debug("Storing analysis statistics done.") + + return run_id, update_run def __get_checker(self, session: DBSession, @@ -879,49 +1110,43 @@ def __add_report( fixed_at: Optional[datetime] = None ) -> int: """ Add report to the database. """ - try: - checker = self.__checker_for_report(session, report) + checker = self.__checker_for_report(session, report) + if not checker: + # It would be too easy to create a 'Checker' instance with the + # observed data right here, but __add_report() is called in + # the context of the *BIG* TRANSACTION which has all the + # reports of the entire store pending. Losing all that + # information on a potential UNIQUE CONSTRAINT violation due + # to multiple concurrent massStoreRun()s trying to store the + # same checker ID which was never seen in a 'metadata.json' is + # not worth it. + checker = self.__get_checker(session, + FakeChecker[0], FakeChecker[1]) if not checker: - # It would be too easy to create a 'Checker' instance with the - # observed data right here, but __add_report() is called in - # the context of the *BIG* TRANSACTION which has all the - # reports of the entire store pending. Losing all that - # information on a potential UNIQUE CONSTRAINT violation due - # to multiple concurrent massStoreRun()s trying to store the - # same checker ID which was never seen in a 'metadata.json' is - # not worth it. - checker = self.__get_checker(session, - FakeChecker[0], FakeChecker[1]) - if not checker: - LOG.fatal("Psuedo-checker '%s/%s' has no " - "identity in the database, even though " - "__store_checker_identifiers() should have " - "always preemptively created it!", - FakeChecker[0], FakeChecker[1]) - raise KeyError(FakeChecker[1]) - - db_report = DBReport( - file_path_to_id[report.file.path], run_id, report.report_hash, - checker, report.line, report.column, - len(report.bug_path_events), report.message, detection_status, - review_status.status, review_status.author, - review_status.message, run_history_time, - review_status.in_source, detection_time, fixed_at) - if analysis_info: - db_report.analysis_info.append(analysis_info) - - session.add(db_report) - self.__added_reports.append((db_report, report)) - if db_report.checker.checker_name == FakeChecker[1]: - self.__reports_with_fake_checkers[report_path_hash] = \ - (report, db_report) - - return db_report.id - - except Exception as ex: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, - str(ex)) + LOG.fatal("Psuedo-checker '%s/%s' has no " + "identity in the database, even though " + "__store_checker_identifiers() should have " + "always preemptively created it!", + FakeChecker[0], FakeChecker[1]) + raise KeyError(FakeChecker[1]) + + db_report = DBReport( + file_path_to_id[report.file.path], run_id, report.report_hash, + checker, report.line, report.column, + len(report.bug_path_events), report.message, detection_status, + review_status.status, review_status.author, + review_status.message, run_history_time, + review_status.in_source, detection_time, fixed_at) + if analysis_info: + db_report.analysis_info.append(analysis_info) + + session.add(db_report) + self.__added_reports.append((db_report, report)) + if db_report.checker.checker_name == FakeChecker[1]: + self.__reports_with_fake_checkers[report_path_hash] = \ + (report, db_report) + + return db_report.id def __get_faked_checkers(self) \ -> Set[Tuple[str, str]]: @@ -966,78 +1191,67 @@ def __realise_fake_checkers(self, session): so all it does is upgrade the 'checker_id' FOREIGN KEY field to point at the real checker. """ - try: - grouped_by_checker: Dict[Tuple[str, str], List[int]] = \ - defaultdict(list) - for _, (report, db_id) in \ - self.__reports_with_fake_checkers.items(): - checker: Tuple[str, str] = checker_name_for_report(report) - grouped_by_checker[checker].append(cast(int, db_id)) - - for chk, report_ids in grouped_by_checker.items(): - analyzer_name, checker_name = chk - chk_obj = cast(Checker, self.__get_checker(session, - analyzer_name, - checker_name)) - session.query(DBReport) \ - .filter(DBReport.id.in_(report_ids)) \ - .update({"checker_id": chk_obj.id}, - synchronize_session=False) - except Exception as ex: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - str(ex)) + grouped_by_checker: Dict[Tuple[str, str], List[int]] = \ + defaultdict(list) + for _, (report, db_id) in \ + self.__reports_with_fake_checkers.items(): + checker: Tuple[str, str] = checker_name_for_report(report) + grouped_by_checker[checker].append(cast(int, db_id)) + + for chk, report_ids in grouped_by_checker.items(): + analyzer_name, checker_name = chk + chk_obj = cast(Checker, self.__get_checker(session, + analyzer_name, + checker_name)) + session.query(DBReport) \ + .filter(DBReport.id.in_(report_ids)) \ + .update({"checker_id": chk_obj.id}, + synchronize_session=False) def __add_report_context(self, session, file_path_to_id): - try: - for db_report, report in self.__added_reports: - LOG.debug("Storing bug path positions.") - for i, p in enumerate(report.bug_path_positions): - session.add(BugReportPoint( - p.range.start_line, p.range.start_col, - p.range.end_line, p.range.end_col, - i, file_path_to_id[p.file.path], db_report.id)) - - LOG.debug("Storing bug path events.") - for i, event in enumerate(report.bug_path_events): - session.add(BugPathEvent( - event.range.start_line, event.range.start_col, - event.range.end_line, event.range.end_col, - i, event.message, file_path_to_id[event.file.path], - db_report.id)) - - LOG.debug("Storing notes.") - for note in report.notes: - data_type = report_extended_data_type_str( - ttypes.ExtendedReportDataType.NOTE) - - session.add(ExtendedReportData( - note.range.start_line, note.range.start_col, - note.range.end_line, note.range.end_col, - note.message, file_path_to_id[note.file.path], - db_report.id, data_type)) - - LOG.debug("Storing macro expansions.") - for macro in report.macro_expansions: - data_type = report_extended_data_type_str( - ttypes.ExtendedReportDataType.MACRO) - - session.add(ExtendedReportData( - macro.range.start_line, macro.range.start_col, - macro.range.end_line, macro.range.end_col, - macro.message, file_path_to_id[macro.file.path], - db_report.id, data_type)) - - if report.annotations: - self.__validate_and_add_report_annotations( - session, db_report.id, report.annotations) + for db_report, report in self.__added_reports: + LOG.debug("Storing bug path positions.") + for i, p in enumerate(report.bug_path_positions): + session.add(BugReportPoint( + p.range.start_line, p.range.start_col, + p.range.end_line, p.range.end_col, + i, file_path_to_id[p.file.path], db_report.id)) + + LOG.debug("Storing bug path events.") + for i, event in enumerate(report.bug_path_events): + session.add(BugPathEvent( + event.range.start_line, event.range.start_col, + event.range.end_line, event.range.end_col, + i, event.message, file_path_to_id[event.file.path], + db_report.id)) + + LOG.debug("Storing notes.") + for note in report.notes: + data_type = report_extended_data_type_str( + ttypes.ExtendedReportDataType.NOTE) + + session.add(ExtendedReportData( + note.range.start_line, note.range.start_col, + note.range.end_line, note.range.end_col, + note.message, file_path_to_id[note.file.path], + db_report.id, data_type)) + + LOG.debug("Storing macro expansions.") + for macro in report.macro_expansions: + data_type = report_extended_data_type_str( + ttypes.ExtendedReportDataType.MACRO) + + session.add(ExtendedReportData( + macro.range.start_line, macro.range.start_col, + macro.range.end_line, macro.range.end_col, + macro.message, file_path_to_id[macro.file.path], + db_report.id, data_type)) + + if report.annotations: + self.__validate_and_add_report_annotations( + session, db_report.id, report.annotations) - session.flush() - - except Exception as ex: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, - str(ex)) + session.flush() def __process_report_file( self, @@ -1074,7 +1288,7 @@ def get_missing_file_ids(report: Report) -> List[str]: for report in reports: self.__report_count += 1 - report.trim_path_prefixes(self.__trim_path_prefixes) + report.trim_path_prefixes(self._trim_path_prefixes) missing_ids_for_files = get_missing_file_ids(report) if missing_ids_for_files: @@ -1117,7 +1331,7 @@ def get_missing_file_ids(report: Report) -> List[str]: except ValueError as err: self.__wrong_src_code_comments.append(str(err)) - review_status.author = self.user_name + review_status.author = self._user_name review_status.date = run_history_time # False positive and intentional reports are considered as closed @@ -1181,24 +1395,17 @@ def __validate_and_add_report_annotations( try: allowed_annotations[key]["func"](value) session.add(ReportAnnotations(report_id, key, value)) - except KeyError: - # pylint: disable=raise-missing-from - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.REPORT_FORMAT, - f"'{key}' is not an allowed report annotation.", - allowed_annotations.keys()) - except ValueError: - # pylint: disable=raise-missing-from - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.REPORT_FORMAT, - f"'{value}' has wrong format. '{key}' annotations must be " - 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 + except KeyError as ke: + raise TypeError(f"'{key}' is not an allowed report " + "annotation. " + "The allowed annotations are: " + f"{allowed_annotations.keys()}") \ + from ke + except ValueError as ve: + raise ValueError(f"'{value}' is in a wrong format! " + f"'{key}' annotations must be " + f"'{allowed_annotations[key]['display']}'.") \ + from ve def __check_report_count(self): """ @@ -1210,13 +1417,7 @@ def __check_report_count(self): 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, + raise OverflowError( "**Report Limit Exceeded** " + "This report folder cannot be stored because the number of " + "reports in the result folder is too high. Usually noisy " + @@ -1226,14 +1427,13 @@ def __check_report_count(self): "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) + f"Limit: {self.__report_limit}") def __store_reports( self, session: DBSession, - report_dir: str, - source_root: str, + report_dir: Path, + source_root: Path, run_id: int, file_path_to_id: Dict[str, int], run_history_time: datetime @@ -1241,11 +1441,11 @@ def __store_reports( """ Parse up and store the plist report files. """ def get_skip_handler( - report_dir: str + report_dir: Path ) -> skiplist_handler.SkipListHandler: """ Get a skip list handler based on the given report directory.""" - skip_file_path = os.path.join(report_dir, 'skip_file') - if not os.path.exists(skip_file_path): + skip_file_path = report_dir / "skip_file" + if not skip_file_path.exists(): return skiplist_handler.SkipListHandler() LOG.debug("Pocessing skip file %s", skip_file_path) @@ -1282,9 +1482,8 @@ def get_skip_handler( for root_dir_path, _, report_file_paths in os.walk(report_dir): LOG.debug("Get reports from '%s' directory", root_dir_path) - skip_handler = get_skip_handler(root_dir_path) - - review_status_handler = ReviewStatusHandler(source_root) + skip_handler = get_skip_handler(Path(root_dir_path)) + review_status_handler = ReviewStatusHandler(str(source_root)) review_status_cfg = \ os.path.join(root_dir_path, 'review_status.yaml') @@ -1346,7 +1545,7 @@ def get_skip_handler( session.flush() - LOG.info("[%s] Processed %d analyzer result file(s).", self.__name, + LOG.info("[%s] Processed %d analyzer result file(s).", self._name, processed_result_file_count) # If a checker was found in a plist file it can not be disabled so we @@ -1377,8 +1576,8 @@ def get_skip_handler( report.fixed_at = run_history_time if reports_to_delete: - self.__report_server._removeReports( - session, list(reports_to_delete)) + from .report_server import remove_reports + remove_reports(session, reports_to_delete) def finish_checker_run( self, @@ -1401,153 +1600,126 @@ def finish_checker_run( return False - def store(self) -> int: - """ Store run results to the server. """ + def store(self, + original_zip_size: int, + time_spent_on_task_preparation: float): + """Store run results to the server.""" start_time = time.time() - # Check constraints of the run. - self.__check_run_limit() - - with DBSession(self.__report_server._Session) as session: - self.__store_run_lock(session) - try: - with TemporaryDirectory( - dir=self.__context.codechecker_workspace - ) as zip_dir: - with LogTask(run_name=self.__name, - message="Unzip storage file"): - zip_size = unzip(self.__b64zip, zip_dir) - - if zip_size == 0: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes. - ErrorCode.GENERAL, - "The received zip file content is empty!") - - LOG.debug("Using unzipped folder '%s'", zip_dir) - - source_root = os.path.join(zip_dir, 'root') - blame_root = os.path.join(zip_dir, 'blame') - report_dir = os.path.join(zip_dir, 'reports') - content_hash_file = os.path.join( - zip_dir, 'content_hashes.json') - - filename_to_hash = load_json(content_hash_file, {}) - - with LogTask(run_name=self.__name, - message="Store source files"): - LOG.info("[%s] Storing %d source file(s).", self.__name, - len(filename_to_hash.keys())) - file_path_to_id = self.__store_source_files( - source_root, filename_to_hash) - self.__add_blame_info(blame_root, filename_to_hash) - - run_history_time = datetime.now() - - # Parse all metadata information from the report directory. - with LogTask(run_name=self.__name, - message="Parse 'metadata.json's"): - for root_dir_path, _, _ in os.walk(report_dir): - metadata_file_path = os.path.join( - root_dir_path, 'metadata.json') - - self.__mips[root_dir_path] = \ - MetadataInfoParser(metadata_file_path) - - with LogTask(run_name=self.__name, - message="Store look-up ID for checkers in " - "'metadata.json'"): - checkers_in_metadata = { - (analyzer, checker) - for metadata in self.__mips.values() - for analyzer in metadata.analyzers - for checker - in metadata.checkers.get(analyzer, {}).keys()} - self.__store_checker_identifiers(checkers_in_metadata) - - try: - # This session's transaction buffer stores the actual - # run data into the database. - with DBSession(self.__report_server._Session) as session, \ - RunLocking(session, self.__name): - # Actual store operation begins here. - run_id, update_run = self.__add_or_update_run( - session, run_history_time) - - with LogTask(run_name=self.__name, - message="Store reports"): - self.__store_reports( - session, report_dir, source_root, run_id, - file_path_to_id, run_history_time) - - session.commit() - self.__load_report_ids_for_reports_with_fake_checkers( - session) + LOG.debug("Using unzipped folder '%s'", self._zip_dir) + + source_root = self._zip_dir / "root" + blame_root = self._zip_dir / "blame" + report_dir = self._zip_dir / "reports" + filename_to_hash = load_json( + self._zip_dir / "content_hashes.json", {}) + + # Store information that is "global" on the product database level. + with StepLog(self._name, "Store source files"): + LOG.info("[%s] Storing %d source file(s).", self._name, + len(filename_to_hash.keys())) + file_path_to_id = self.__store_source_files( + source_root, filename_to_hash) + self.__add_blame_info(blame_root, filename_to_hash) + + run_history_time = datetime.now() + + with StepLog(self._name, "Parse 'metadata.json's"): + for root_dir_path, _, _ in os.walk(report_dir): + metadata_file_path = os.path.join( + root_dir_path, 'metadata.json') + + self.__mips[root_dir_path] = \ + MetadataInfoParser(metadata_file_path) + + with StepLog(self._name, + "Store look-up ID for checkers in 'metadata.json'"): + checkers_in_metadata = { + (analyzer, checker) + for metadata in self.__mips.values() + for analyzer in metadata.analyzers + for checker + in metadata.checkers.get(analyzer, {}).keys()} + self.__store_checker_identifiers(checkers_in_metadata) + + try: + # This session's transaction buffer stores the actual run data + # into the database. + with DBSession(self.__product.session_factory) as session, \ + RunLock(session, self._name): + run_id, update_run = self.__add_or_update_run( + session, run_history_time) + + with StepLog(self._name, "Store 'reports'"): + self.__store_reports( + session, report_dir, source_root, run_id, + file_path_to_id, run_history_time) + session.commit() + self.__load_report_ids_for_reports_with_fake_checkers( + session) + + if self.__reports_with_fake_checkers: + with StepLog( + self._name, + "Get look-up IDs for checkers not present in " + "'metadata.json'"): + additional_checkers = self.__get_faked_checkers() + # __store_checker_identifiers() has its own + # TRANSACTION! + self.__store_checker_identifiers( + additional_checkers) + + with DBSession(self.__product.session_factory) as session, \ + RunLock(session, self._name): + # The data of the run has been successfully committed + # into the database. Deal with post-processing issues + # that could only be done after-the-fact. if self.__reports_with_fake_checkers: - with LogTask(run_name=self.__name, - message="Get look-up ID for checkers " - "not present in 'metadata.json'"): - additional_checkers = self.__get_faked_checkers() - # __store_checker_identifiers() has its own - # TRANSACTION! - self.__store_checker_identifiers( - additional_checkers) - - with DBSession(self.__report_server._Session) as session, \ - RunLocking(session, self.__name): - # The data of the run has been successfully committed - # into the database. Deal with post-processing issues - # that could only be done after-the-fact. - if self.__reports_with_fake_checkers: - with LogTask(run_name=self.__name, - message="Fix-up report-to-checker " - "associations"): - self.__realise_fake_checkers(session) - - self.finish_checker_run(session, run_id) - session.commit() - - # If it's a run update, do not increment the number - # of runs of the current product. - inc_num_of_runs = 1 if not update_run else None - - self.__report_server._set_run_data_for_curr_product( - inc_num_of_runs, run_history_time) - - runtime = round(time.time() - start_time, 2) - zip_size_kb = round(zip_size / 1024) - - tag_desc = "" - if self.__tag: - tag_desc = f", under tag '{self.__tag}'" - - LOG.info("'%s' stored results (%s KB " - "/decompressed/) to run '%s' (id: %d) %s in " - "%s seconds.", self.user_name, - zip_size_kb, self.__name, run_id, tag_desc, - runtime) - - iso_start_time = datetime.fromtimestamp( - start_time).isoformat() - - log_msg = f"{iso_start_time}, " +\ - f"{runtime}s, " +\ - f'"{self.__product.name}", ' +\ - f'"{self.__name}", ' +\ - f"{zip_size_kb}KB, " +\ - f"{self.__report_count}, " +\ - f"{run_id}" - - STORE_TIME_LOG.info(log_msg) - - return run_id - except (sqlalchemy.exc.OperationalError, - sqlalchemy.exc.ProgrammingError) as ex: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.DATABASE, - f"Storing reports to the database failed: {ex}") + with StepLog(self._name, + "Fix-up report-to-checker associations"): + self.__realise_fake_checkers(session) + + self.finish_checker_run(session, run_id) + session.commit() + + end_time = time.time() + + # If the current store() updated an existing run, do not + # increment the number of runs saved for the product. + self.__product.set_cached_run_data( + self.__config_db, + number_of_runs_change=(0 if update_run else 1), + last_store_date=run_history_time) + + run_time: float = (end_time - start_time) + \ + time_spent_on_task_preparation + zip_size_kib: float = original_zip_size / 1024 + + LOG.info("'%s' stored results (decompressed size: %.2f KiB) " + "to run '%s' (ID: %d%s) in %.2f seconds.", + self._user_name, zip_size_kib, self._name, run_id, + f", under tag '{self._tag}'" if self._tag else "", + run_time) + + iso_start_time = datetime.fromtimestamp(start_time) \ + .isoformat() + + log_msg = f"{iso_start_time}, " \ + f"{round(run_time, 2)}s, " \ + f'"{self.__product.name}", ' \ + f'"{self._name}", ' \ + f"{round(zip_size_kib)}KiB, " \ + f"{self.__report_count}, " \ + f"{run_id}" + + STORE_TIME_LOG.info(log_msg) + except (sqlalchemy.exc.OperationalError, + sqlalchemy.exc.ProgrammingError) as ex: + LOG.error("Database error! Storing reports to the " + "database failed: %s", ex) + raise except Exception as ex: LOG.error("Failed to store results: %s", ex) import traceback @@ -1560,10 +1732,17 @@ def store(self) -> int: # (If the failure is undetectable, the coded grace period expiry # of the lock will allow further store operations to the given # run name.) - with DBSession(self.__report_server._Session) as session: - self.__free_run_lock(session) + with DBSession(self.__product.session_factory) as session: + RunLock(session, self._name).drop_run_lock_from_db() if self.__wrong_src_code_comments: - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.SOURCE_FILE, - self.__wrong_src_code_comments) + wrong_files_as_table = twodim.to_str( + "table", + ["File", "Line", "Checker name"], + [wrong_comment.split('|', 3) + for wrong_comment in self.__wrong_src_code_comments]) + + raise ValueError("One or more source files contained invalid " + "source code comments! " + "Failed to set review statuses.\n\n" + f"{wrong_files_as_table}") diff --git a/web/server/codechecker_server/api/report_server.py b/web/server/codechecker_server/api/report_server.py index 2348708b81..9c8bd6f07a 100644 --- a/web/server/codechecker_server/api/report_server.py +++ b/web/server/codechecker_server/api/report_server.py @@ -22,7 +22,7 @@ from copy import deepcopy from collections import OrderedDict, defaultdict, namedtuple from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Collection, Dict, List, Optional, Set, Tuple import sqlalchemy from sqlalchemy.sql.expression import or_, and_, not_, func, \ @@ -1340,13 +1340,26 @@ def get_is_opened_case(subquery): ) +def remove_reports(session: DBSession, + report_ids: Collection, + chunk_size: int = SQLITE_MAX_VARIABLE_NUMBER): + """ + Removes `Report`s in chunks. + """ + for r_ids in util.chunks(iter(report_ids), chunk_size): + session.query(Report) \ + .filter(Report.id.in_(r_ids)) \ + .delete(synchronize_session=False) + + class ThriftRequestHandler: """ Connect to database and handle thrift client requests. """ def __init__(self, - manager, + session_manager, + task_manager, Session, product, auth_session, @@ -1359,7 +1372,8 @@ def __init__(self, raise ValueError("Cannot initialize request handler without " "a product to serve.") - self._manager = manager + self._manager = session_manager + self._task_manager = task_manager self._product = product self._auth_session = auth_session self._config_database = config_database @@ -1377,34 +1391,6 @@ def _get_username(self): """ return self._auth_session.user if self._auth_session else "Anonymous" - def _set_run_data_for_curr_product( - self, - inc_num_of_runs: Optional[int], - latest_storage_date: Optional[datetime] = None - ): - """ - Increment the number of runs related to the current product with the - given value and set the latest storage date. - """ - values = {} - - if inc_num_of_runs is not None: - values["num_of_runs"] = Product.num_of_runs + inc_num_of_runs - # FIXME: This log is likely overkill. - LOG.info("Run counter in the config database was %s by %i.", - 'increased' if inc_num_of_runs >= 0 else 'decreased', - abs(inc_num_of_runs)) - - if latest_storage_date is not None: - values["latest_storage_date"] = latest_storage_date - - with DBSession(self._config_database) as session: - session.query(Product) \ - .filter(Product.id == self._product.id) \ - .update(values) - - session.commit() - def __require_permission(self, required): """ Helper method to raise an UNAUTHORIZED exception if the user does not @@ -3595,16 +3581,6 @@ def removeRunResults(self, run_ids): failed = True return not failed - def _removeReports(self, session, report_ids, - chunk_size=SQLITE_MAX_VARIABLE_NUMBER): - """ - Removing reports in chunks. - """ - for r_ids in util.chunks(iter(report_ids), chunk_size): - session.query(Report) \ - .filter(Report.id.in_(r_ids)) \ - .delete(synchronize_session=False) - @exc_to_thrift_reqfail @timeit def removeRunReports(self, run_ids, report_filter, cmp_data): @@ -3634,7 +3610,7 @@ def removeRunReports(self, run_ids, report_filter, cmp_data): reports_to_delete = [r[0] for r in q] if reports_to_delete: - self._removeReports(session, reports_to_delete) + remove_reports(session, reports_to_delete) session.commit() session.close() @@ -3706,9 +3682,9 @@ def removeRun(self, run_id, run_filter): LOG.info("Runs '%s' were removed by '%s'.", "', '".join(runs), self._get_username()) - # Decrement the number of runs but do not update the latest storage - # date. - self._set_run_data_for_curr_product(-1 * deleted_run_cnt) + self._product.set_cached_run_data( + self._config_database, + number_of_runs_change=-1 * deleted_run_cnt) # Remove unused comments and unused analysis info from the database. # Originally db_cleanup.remove_unused_data() was used here which @@ -3893,14 +3869,69 @@ def getMissingContentHashesForBlameInfo(self, file_hashes): @exc_to_thrift_reqfail @timeit - def massStoreRun(self, name, tag, version, b64zip, force, - trim_path_prefixes, description): + def massStoreRun(self, + name: str, + tag: Optional[str], + version: str, + b64zip: str, + force: bool, + trim_path_prefixes: Optional[List[str]], + description: Optional[str]) -> int: self.__require_store() - - from codechecker_server.api.mass_store_run import MassStoreRun - m = MassStoreRun(self, name, tag, version, b64zip, force, - trim_path_prefixes, description) - return m.store() + if not name: + raise ValueError("A run name is needed to know where to store!") + + from .mass_store_run import MassStoreRunInputHandler, MassStoreRunTask + ih = MassStoreRunInputHandler(self._manager, + self._config_database, + self._Session, + self._task_manager, + self._context, + self._product.id, + name, + description, + tag, + version, + force, + trim_path_prefixes, + b64zip, + self._get_username()) + ih.check_store_input_validity_at_face_value() + m: MassStoreRunTask = ih.create_mass_store_task(False) + self._task_manager.push_task(m) + + LOG.info("massStoreRun(): Running as '%s' ...", m.token) + + # To be compatible with older (<= 6.24, API <= 6.58) clients which + # may keep using the old API endpoint, simulate awaiting the + # background task in the API handler. + while True: + time.sleep(5) + t = self._task_manager.get_task_record(m.token) + if t.is_in_terminated_state: + if t.status == "failed": + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes.ErrorCode.GENERAL, + "massStoreRun()'s processing failed. Here follow " + f"the details:\n\n{t.comments}") + if t.status == "cancelled": + raise codechecker_api_shared.ttypes.RequestFailed( + codechecker_api_shared.ttypes.ErrorCode.GENERAL, + "Server administrators cancelled the processing of " + "the massStoreRun() request!") + break + + # Prior to CodeChecker 6.25.0 (API v6.59), massStoreRun() was + # completely synchronous and blocking, and the implementation of the + # storage logic returned the ID of the run that was stored by the + # call. + # massStoreRun() was implemented in + # commit 2b29d787599da0318cd23dbe816377b9bce7236c (September 2017), + # replacing the previously used (and then completely removed!) + # addCheckerRun() function, which also returned the run's ID. + # The official client implementation stopped using this returned value + # from the moment of massStoreRun()'s implementation. + return -1 @exc_to_thrift_reqfail @timeit diff --git a/web/server/codechecker_server/product.py b/web/server/codechecker_server/product.py new file mode 100644 index 0000000000..3c18de4339 --- /dev/null +++ b/web/server/codechecker_server/product.py @@ -0,0 +1,236 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- +""" +The in-memory representation and access methods for querying and mutating a +"Product": a separate and self-contained database and entity containing +analysis results and associated information, which a CodeChecker server can +connect to. +""" +from datetime import datetime +from typing import Optional + +from sqlalchemy.orm import sessionmaker + +from codechecker_api_shared.ttypes import DBStatus + +from codechecker_common.logger import get_logger + +from .database import database, db_cleanup +from .database.config_db_model import Product as DBProduct +from .database.database import DBSession +from .database.run_db_model import \ + IDENTIFIER as RUN_META, \ + Run, RunLock + + +LOG = get_logger("server") + + +class Product: + """ + Represents a product, which is a distinct storage of analysis reports in + a separate database (and database connection) with its own access control. + """ + + # The amount of SECONDS that need to pass after the last unsuccessful + # connect() call so the next could be made. + CONNECT_RETRY_TIMEOUT = 300 + + def __init__(self, id_: int, endpoint: str, display_name: str, + connection_string: str, context, check_env): + """ + Set up a new managed product object for the configuration given. + """ + self.__id = id_ + self.__endpoint = endpoint + self.__display_name = display_name + self.__connection_string = connection_string + self.__driver_name = None + self.__context = context + self.__check_env = check_env + self.__engine = None + self.__session = None + self.__db_status = DBStatus.MISSING + + self.__last_connect_attempt = None + + @property + def id(self): + return self.__id + + @property + def endpoint(self): + """ + Returns the accessible URL endpoint of the product. + """ + return self.__endpoint + + @property + def name(self): + """ + Returns the display name of the product. + """ + return self.__display_name + + @property + def session_factory(self): + """ + Returns the session maker on this product's database engine which + can be used to initiate transactional connections. + """ + return self.__session + + @property + def driver_name(self): + """ + Returns the name of the sql driver (sqlite, postgres). + """ + return self.__driver_name + + @property + def db_status(self): + """ + Returns the status of the database which belongs to this product. + Call connect to update it. + """ + return self.__db_status + + @property + def last_connection_failure(self): + """ + Returns the reason behind the last executed connection attempt's + failure. + """ + return self.__last_connect_attempt[1] if self.__last_connect_attempt \ + else None + + def connect(self, init_db=False): + """ + Initiates the actual connection to the database configured for the + product. + + Each time the connect is called the db_status is updated. + """ + LOG.debug("Checking '%s' database.", self.endpoint) + + sql_server = database.SQLServer.from_connection_string( + self.__connection_string, + self.__endpoint, + RUN_META, + self.__context.run_migration_root, + interactive=False, + env=self.__check_env) + + if isinstance(sql_server, database.PostgreSQLServer): + self.__driver_name = 'postgresql' + elif isinstance(sql_server, database.SQLiteDatabase): + self.__driver_name = 'sqlite' + + try: + LOG.debug("Trying to connect to the database") + + # Create the SQLAlchemy engine. + self.__engine = sql_server.create_engine() + LOG.debug(self.__engine) + + self.__session = sessionmaker(bind=self.__engine) + + self.__engine.execute('SELECT 1') + self.__db_status = sql_server.check_schema() + self.__last_connect_attempt = None + + if self.__db_status == DBStatus.SCHEMA_MISSING and init_db: + LOG.debug("Initializing new database schema.") + self.__db_status = sql_server.connect(init_db) + + except Exception as ex: + LOG.exception("The database for product '%s' cannot be" + " connected to.", self.endpoint) + self.__db_status = DBStatus.FAILED_TO_CONNECT + self.__last_connect_attempt = (datetime.now(), str(ex)) + + def get_details(self): + """ + Get details for a product from the database. + + It may throw different error messages depending on the used SQL driver + adapter in case of connection error. + """ + with DBSession(self.session_factory) as run_db_session: + run_locks = run_db_session.query(RunLock.name) \ + .filter(RunLock.locked_at.isnot(None)) \ + .all() + + runs_in_progress = set(run_lock[0] for run_lock in run_locks) + + num_of_runs = run_db_session.query(Run).count() + + latest_store_to_product = "" + if num_of_runs: + last_updated_run = run_db_session.query(Run) \ + .order_by(Run.date.desc()) \ + .limit(1) \ + .one_or_none() + + latest_store_to_product = last_updated_run.date + + return num_of_runs, runs_in_progress, latest_store_to_product + + def teardown(self): + """ + Disposes the database connection to the product's backend. + """ + if self.__db_status == DBStatus.FAILED_TO_CONNECT: + return + + self.__engine.dispose() + + self.__session = None + self.__engine = None + + def cleanup_run_db(self): + """ + Cleanup the run database which belongs to this product. + """ + LOG.info("[%s] Garbage collection started...", self.endpoint) + + db_cleanup.remove_expired_data(self) + db_cleanup.remove_unused_data(self) + db_cleanup.update_contextual_data(self, self.__context) + + LOG.info("[%s] Garbage collection finished.", self.endpoint) + return True + + def set_cached_run_data(self, + config_db_session_factory, + number_of_runs_change: Optional[int] = None, + last_store_date: Optional[datetime] = None): + """ + Update the configuration database row for the current `Product` + for the keys that contain cached summaries of what would otherwise + be queriable from the product's database. + """ + updates = {} + + if number_of_runs_change: + updates["num_of_runs"] = DBProduct.num_of_runs \ + + number_of_runs_change + LOG.info("%s: Changing 'num_of_runs' in CONFIG database by %s%i.", + self.__endpoint, + '+' if number_of_runs_change > 0 else '-', + abs(number_of_runs_change)) + + if last_store_date: + updates["latest_storage_date"] = last_store_date + + if updates: + with DBSession(config_db_session_factory) as session: + session.query(DBProduct) \ + .filter(DBProduct.id == self.__id) \ + .update(updates) + session.commit() diff --git a/web/server/codechecker_server/server.py b/web/server/codechecker_server/server.py index f10f28291e..183a390f8a 100644 --- a/web/server/codechecker_server/server.py +++ b/web/server/codechecker_server/server.py @@ -13,11 +13,11 @@ import atexit from collections import Counter -import datetime from functools import partial from hashlib import sha256 from http.server import HTTPServer, SimpleHTTPRequestHandler import os +import pathlib import posixpath from random import sample import shutil @@ -68,14 +68,14 @@ from .api.server_info_handler import \ ThriftServerInfoHandler as ServerInfoHandler_v6 from .api.tasks import ThriftTaskHandler as TaskHandler_v6 -from .database import database, db_cleanup from .database.config_db_model import Product as ORMProduct, \ Configuration as ORMConfiguration from .database.database import DBSession -from .database.run_db_model import IDENTIFIER as RUN_META, Run, RunLock +from .database.run_db_model import Run +from .product import Product from .task_executors.main import executor as background_task_executor from .task_executors.task_manager import \ - TaskManager as BackgroundTaskManager, drop_all_incomplete_tasks + TaskManager as BackgroundTaskManager LOG = get_logger('server') @@ -421,6 +421,7 @@ def do_POST(self): acc_handler = ReportHandler_v6( self.server.manager, + self.server.task_manager, product.session_factory, product, self.auth_session, @@ -450,7 +451,7 @@ def do_POST(self): self.send_response(200) self.send_header("content-type", "application/x-thrift") - self.send_header("Content-Length", len(result)) + self.send_header("Content-Length", str(len(result))) self.end_headers() self.wfile.write(result) return @@ -497,182 +498,6 @@ def translate_path(self, path): return path -class Product: - """ - Represents a product, which is a distinct storage of analysis reports in - a separate database (and database connection) with its own access control. - """ - - # The amount of SECONDS that need to pass after the last unsuccessful - # connect() call so the next could be made. - CONNECT_RETRY_TIMEOUT = 300 - - def __init__(self, id_: int, endpoint: str, display_name: str, - connection_string: str, context, check_env): - """ - Set up a new managed product object for the configuration given. - """ - self.__id = id_ - self.__endpoint = endpoint - self.__display_name = display_name - self.__connection_string = connection_string - self.__driver_name = None - self.__context = context - self.__check_env = check_env - self.__engine = None - self.__session = None - self.__db_status = DBStatus.MISSING - - self.__last_connect_attempt = None - - @property - def id(self): - return self.__id - - @property - def endpoint(self): - """ - Returns the accessible URL endpoint of the product. - """ - return self.__endpoint - - @property - def name(self): - """ - Returns the display name of the product. - """ - return self.__display_name - - @property - def session_factory(self): - """ - Returns the session maker on this product's database engine which - can be used to initiate transactional connections. - """ - return self.__session - - @property - def driver_name(self): - """ - Returns the name of the sql driver (sqlite, postgres). - """ - return self.__driver_name - - @property - def db_status(self): - """ - Returns the status of the database which belongs to this product. - Call connect to update it. - """ - return self.__db_status - - @property - def last_connection_failure(self): - """ - Returns the reason behind the last executed connection attempt's - failure. - """ - return self.__last_connect_attempt[1] if self.__last_connect_attempt \ - else None - - def connect(self, init_db=False): - """ - Initiates the actual connection to the database configured for the - product. - - Each time the connect is called the db_status is updated. - """ - LOG.debug("Checking '%s' database.", self.endpoint) - - sql_server = database.SQLServer.from_connection_string( - self.__connection_string, - self.__endpoint, - RUN_META, - self.__context.run_migration_root, - interactive=False, - env=self.__check_env) - - if isinstance(sql_server, database.PostgreSQLServer): - self.__driver_name = 'postgresql' - elif isinstance(sql_server, database.SQLiteDatabase): - self.__driver_name = 'sqlite' - - try: - LOG.debug("Trying to connect to the database") - - # Create the SQLAlchemy engine. - self.__engine = sql_server.create_engine() - LOG.debug(self.__engine) - - self.__session = sessionmaker(bind=self.__engine) - - self.__engine.execute('SELECT 1') - self.__db_status = sql_server.check_schema() - self.__last_connect_attempt = None - - if self.__db_status == DBStatus.SCHEMA_MISSING and init_db: - LOG.debug("Initializing new database schema.") - self.__db_status = sql_server.connect(init_db) - - except Exception as ex: - LOG.exception("The database for product '%s' cannot be" - " connected to.", self.endpoint) - self.__db_status = DBStatus.FAILED_TO_CONNECT - self.__last_connect_attempt = (datetime.datetime.now(), str(ex)) - - def get_details(self): - """ - Get details for a product from the database. - - It may throw different error messages depending on the used SQL driver - adapter in case of connection error. - """ - with DBSession(self.session_factory) as run_db_session: - run_locks = run_db_session.query(RunLock.name) \ - .filter(RunLock.locked_at.isnot(None)) \ - .all() - - runs_in_progress = set(run_lock[0] for run_lock in run_locks) - - num_of_runs = run_db_session.query(Run).count() - - latest_store_to_product = "" - if num_of_runs: - last_updated_run = run_db_session.query(Run) \ - .order_by(Run.date.desc()) \ - .limit(1) \ - .one_or_none() - - latest_store_to_product = last_updated_run.date - - return num_of_runs, runs_in_progress, latest_store_to_product - - def teardown(self): - """ - Disposes the database connection to the product's backend. - """ - if self.__db_status == DBStatus.FAILED_TO_CONNECT: - return - - self.__engine.dispose() - - self.__session = None - self.__engine = None - - def cleanup_run_db(self): - """ - Cleanup the run database which belongs to this product. - """ - LOG.info("[%s] Garbage collection started...", self.endpoint) - - db_cleanup.remove_expired_data(self) - db_cleanup.remove_unused_data(self) - db_cleanup.update_contextual_data(self, self.__context) - - LOG.info("[%s] Garbage collection finished.", self.endpoint) - return True - - def _do_db_cleanup(context, check_env, id_: int, endpoint: str, display_name: str, connection_str: str) -> Tuple[Optional[bool], str]: @@ -782,10 +607,10 @@ def __init__(self, self.manager.set_database_connection(self.config_session) self.__task_queue = task_queue - self.task_manager = BackgroundTaskManager(task_queue, - self.config_session, - server_shutdown_flag, - machine_id) + self.task_manager = BackgroundTaskManager( + task_queue, self.config_session, self.check_env, + server_shutdown_flag, machine_id, + pathlib.Path(self.context.codechecker_workspace)) # Load the initial list of products and set up the server. cfg_sess = self.config_session() @@ -1163,23 +988,6 @@ def start_server(config_directory: str, package_data, port: int, else: LOG.debug("Skipping db_cleanup, as requested.") - def _cleanup_incomplete_tasks(action: str) -> int: - config_session_factory = config_sql_server.create_engine() - try: - return drop_all_incomplete_tasks( - sessionmaker(bind=config_session_factory), - machine_id, action) - finally: - config_session_factory.dispose() - - dropped_tasks = _cleanup_incomplete_tasks( - "New server started with the same machine_id, assuming the old " - "server is dead and won't be able to finish the task.") - if dropped_tasks: - LOG.info("At server startup, dropped %d background tasks left behind " - "by a previous server instance matching machine ID '%s'.", - dropped_tasks, machine_id) - api_processes: Dict[int, Process] = {} requested_api_threads = cast(int, manager.worker_processes) \ or cpu_count() @@ -1194,6 +1002,34 @@ def _cleanup_incomplete_tasks(action: str) -> int: bg_task_queue: Queue = Queue() is_server_shutting_down = Value('B', False) + def _cleanup_incomplete_tasks(action: str) -> int: + config_db = config_sql_server.create_engine() + config_session_factory = sessionmaker(bind=config_db) + tm = BackgroundTaskManager( + bg_task_queue, config_session_factory, check_env, + is_server_shutting_down, machine_id, + pathlib.Path(context.codechecker_workspace)) + + try: + tm.destroy_all_temporary_data() + except OSError: + LOG.warning("Clearing task-temporary storage space failed!") + import traceback + traceback.print_exc() + + try: + return tm.drop_all_incomplete_tasks(action) + finally: + config_db.dispose() + + dropped_tasks = _cleanup_incomplete_tasks( + "New server started with the same machine_id, assuming the old " + "server is dead and won't be able to finish the task.") + if dropped_tasks: + LOG.info("At server startup, dropped %d background tasks left behind " + "by a previous server instance matching machine ID '%s'.", + dropped_tasks, machine_id) + server_clazz = CCSimpleHttpServer if ':' in listen_address: # IPv6 address specified for listening. @@ -1295,6 +1131,7 @@ def spawn_bg_process(): target=background_task_executor, args=(bg_task_queue, config_sql_server, + check_env, is_server_shutting_down, machine_id, ), diff --git a/web/server/codechecker_server/task_executors/abstract_task.py b/web/server/codechecker_server/task_executors/abstract_task.py index f38830ad33..bb665f034e 100644 --- a/web/server/codechecker_server/task_executors/abstract_task.py +++ b/web/server/codechecker_server/task_executors/abstract_task.py @@ -9,6 +9,7 @@ Contains the base class to be inherited and implemented by all background task types. """ +import logging import os import pathlib import shutil @@ -82,6 +83,8 @@ def destroy_data(self): try: shutil.rmtree(self._data_path) + LOG.debug("Wiping temporary data of task '%s' at '%s' ...", + self._token, self._data_path) except Exception as ex: LOG.warning("Failed to remove background task's data_dir at " "'%s':\n%s", self.data_path, str(ex)) @@ -94,7 +97,7 @@ def _implementation(self, _task_manager: "TaskManager") -> None: context of the executed subprocess, to query and mutate service-level information about the current task. """ - raise NotImplementedError() + raise NotImplementedError(f"No implementation for task class {self}!") def execute(self, task_manager: "TaskManager") -> None: """ @@ -183,6 +186,10 @@ def _log_exception_and_fail(db_task: DBTask): db_task.add_comment( f"FAILED!\nException during execution:\n{str(ex)}", "SYSTEM[AbstractTask::execute()]") + if LOG.isEnabledFor(logging.DEBUG): + db_task.add_comment("Debug exception information:\n" + f"{traceback.format_exc()}", + "SYSTEM[AbstractTask::execute()]") db_task.set_finished(successfully=False) task_manager._mutate_task_record(self, _log_exception_and_fail) diff --git a/web/server/codechecker_server/task_executors/main.py b/web/server/codechecker_server/task_executors/main.py index 580a27c4b9..320de737c5 100644 --- a/web/server/codechecker_server/task_executors/main.py +++ b/web/server/codechecker_server/task_executors/main.py @@ -31,6 +31,7 @@ def executor(queue: Queue, config_db_sql_server, + server_environment, server_shutdown_flag: "Value", machine_id: str): """ @@ -66,8 +67,8 @@ def executor_hangup_handler(signum: int, _frame): signal.signal(signal.SIGHUP, executor_hangup_handler) config_db_engine = config_db_sql_server.create_engine() - tm = TaskManager(queue, sessionmaker(bind=config_db_engine), kill_flag, - machine_id) + tm = TaskManager(queue, sessionmaker(bind=config_db_engine), + server_environment, kill_flag, machine_id) while not kill_flag.value: try: diff --git a/web/server/codechecker_server/task_executors/task_manager.py b/web/server/codechecker_server/task_executors/task_manager.py index ddd3b31053..6b6929109a 100644 --- a/web/server/codechecker_server/task_executors/task_manager.py +++ b/web/server/codechecker_server/task_executors/task_manager.py @@ -11,6 +11,8 @@ """ import os from pathlib import Path +import re +import shutil import tempfile from typing import Callable, Optional @@ -24,6 +26,7 @@ from ..database.database import DBSession MAX_TOKEN_RANDOM_RETRIES = 10 +CHARS_INVALID_IN_PATH = re.compile(r"[\'\"<>:\\/\|\*\?\. ]") LOG = get_logger("server") @@ -47,11 +50,33 @@ class TaskManager: """ def __init__(self, q: Queue, config_db_session_factory, - executor_kill_flag: Value, machine_id: str): + server_environment, + executor_kill_flag: Value, + machine_id: str, + temp_dir: Optional[Path] = None): self._queue = q self._database_factory = config_db_session_factory + self._server_environment = server_environment self._is_shutting_down = executor_kill_flag self._machine_id = machine_id + self._temp_dir_root = (temp_dir or Path(tempfile.gettempdir())) \ + / "codechecker_tasks" \ + / CHARS_INVALID_IN_PATH.sub('_', machine_id) + + os.makedirs(self._temp_dir_root, exist_ok=True) + + @property + def configuration_database_session_factory(self): + """ + Returns a `sqlalchemy.orm.sessionmaker` instance for the server + configuration database. + """ + return self._database_factory + + @property + def environment(self): + """Returns the ``check_env`` injected into the task manager.""" + return self._server_environment @property def machine_id(self) -> str: @@ -95,31 +120,78 @@ def allocate_task_record(self, kind: str, summary: str, def create_task_data(self, token: str) -> Path: """ Creates a temporary directory which is **NOT** cleaned up - automatically, and suitable for putting arbitrary files underneath - to communicate large inputs (that should not be put in the `Queue`) - to the `execute` method of an `AbstractTask`. + automatically by the current context, and which is suitable for + putting arbitrary files underneath to communicate large inputs + (that should not be put in the `Queue`) to the `execute` method of + an `AbstractTask`. + + The larger business logic of the Server implementation may still clean + up the temporary directories, e.g., if the pending tasks are being + dropped during a shutdown, making retention of this "temporary data" + useless. + See `destroy_temporary_data`. + """ + task_temp_dir = tempfile.mkdtemp(prefix=f"{token}-", + dir=self._temp_dir_root) + return Path(task_temp_dir) + + def destroy_all_temporary_data(self): """ - task_tmp_root = Path(tempfile.gettempdir()) / "codechecker_tasks" \ - / self.machine_id - os.makedirs(task_tmp_root, exist_ok=True) + Removes the contents of task-temporary directories under the + `TaskManager`'s initial `temp_dir` and current "machine ID". + """ + try: + shutil.rmtree(self._temp_dir_root) + except Exception as ex: + LOG.warning("Failed to remove background tasks' data_dirs at " + "'%s':\n%s", self._temp_dir_root, str(ex)) - task_tmp_dir = tempfile.mkdtemp(prefix=f"{token}-") - return Path(task_tmp_dir) + def drop_all_incomplete_tasks(self, action: str) -> int: + """ + Sets all tasks in the database that were associated with the given + `machine_id` to ``"dropped"`` status, indicating that the status was + changed during the `action`. - def _get_task_record(self, task_obj: "AbstractTask") -> DBTask: + Returns the number of `DBTask`s actually changed. + """ + count: int = 0 + with DBSession(self._database_factory) as session: + for t in session.query(DBTask) \ + .filter(DBTask.machine_id == self.machine_id, + DBTask.status.in_(["allocated", + "enqueued", + "running"])) \ + .all(): + count += 1 + t.add_comment(f"DROPPED!\n{action}", + "SYSTEM") + t.set_abandoned(force_dropped_status=True) + + session.commit() + return count + + def get_task_record(self, token: str) -> DBTask: """ Retrieves the `DBTask` for the task identified by `task_obj`. This class should not be mutated, only the fields queried. """ with DBSession(self._database_factory) as session: - try: - db_task = session.query(DBTask).get(task_obj.token) - session.expunge(db_task) - return db_task - except sqlalchemy.exc.SQLAlchemyError as sql_err: - raise KeyError(f"No task record for token '{task_obj.token}' " - "in the database") from sql_err + db_task: Optional[DBTask] = \ + session.query(DBTask).get(token) + if not db_task: + raise KeyError(f"No task record for token '{token}' " + "in the database") + session.expunge(db_task) + return db_task + + def _get_task_record(self, task_obj: "AbstractTask") -> DBTask: + """ + Retrieves the `DBTask` for the task identified by `task_obj`. + + This class should not be mutated, only the fields queried. + """ + return self.get_task_record(task_obj.token) def _mutate_task_record(self, task_obj: "AbstractTask", mutator: Callable[[DBTask], None]): @@ -128,14 +200,14 @@ def _mutate_task_record(self, task_obj: "AbstractTask", corresponding to the `task_obj` description available in memory. """ with DBSession(self._database_factory) as session: - try: - db_record = session.query(DBTask).get(task_obj.token) - except sqlalchemy.exc.SQLAlchemyError as sql_err: + db_task: Optional[DBTask] = \ + session.query(DBTask).get(task_obj.token) + if not db_task: raise KeyError(f"No task record for token '{task_obj.token}' " - "in the database") from sql_err + "in the database") try: - mutator(db_record) + mutator(db_task) except Exception: session.rollback() @@ -195,35 +267,18 @@ def should_cancel(self, task_obj: "AbstractTask") -> bool: (db_task.status in ["enqueued", "running"] and db_task.cancel_flag) + def add_comment(self, task_obj: "AbstractTask", comment: str, + actor: Optional[str] = None): + """ + Adds `comment` in the name of `actor` to the task record corresponding + to `task_obj`. + """ + self._mutate_task_record(task_obj, + lambda dbt: dbt.add_comment(comment, actor)) + def heartbeat(self, task_obj: "AbstractTask"): """ Triggers ``heartbeat()`` timestamp update in the database for `task_obj`. """ self._mutate_task_record(task_obj, lambda dbt: dbt.heartbeat()) - - -def drop_all_incomplete_tasks(config_db_session_factory, machine_id: str, - action: str) -> int: - """ - Sets all tasks in the database (reachable via `config_db_session_factory`) - that were associated with the given `machine_id` to ``"dropped"`` status, - indicating that the status was changed during the `action`. - - Returns the number of `DBTask`s actually changed. - """ - count: int = 0 - with DBSession(config_db_session_factory) as session: - for t in session.query(DBTask) \ - .filter(DBTask.machine_id == machine_id, - DBTask.status.in_(["allocated", - "enqueued", - "running"])) \ - .all(): - count += 1 - t.add_comment(f"DROPPED!\n{action}", - "SYSTEM") - t.set_abandoned(force_dropped_status=True) - - session.commit() - return count diff --git a/web/server/vue-cli/package-lock.json b/web/server/vue-cli/package-lock.json index d0943fd772..07b56dce13 100644 --- a/web/server/vue-cli/package-lock.json +++ b/web/server/vue-cli/package-lock.json @@ -5115,7 +5115,7 @@ "node_modules/codechecker-api": { "version": "6.59.0", "resolved": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", - "integrity": "sha512-BZCBDRjVFS5UerrXsoPNioQppTfrCdDgToHqfFfaQtk6FPVrER42LchfU+cZl254PgWh58H5bLfqdLyFfqntCg==", + "integrity": "sha512-DN1vQkV3P/5jwI62Sd+JzvNALe/i7km2iDd8GKfK6vQYdYPnHg3ZpwK1vyRcF0dsegZhjfgoMzOAclH+nwk+Yg==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "thrift": "0.13.0-hotfix.1" @@ -21146,7 +21146,7 @@ }, "codechecker-api": { "version": "file:../../api/js/codechecker-api-node/dist/codechecker-api-6.59.0.tgz", - "integrity": "sha512-BZCBDRjVFS5UerrXsoPNioQppTfrCdDgToHqfFfaQtk6FPVrER42LchfU+cZl254PgWh58H5bLfqdLyFfqntCg==", + "integrity": "sha512-DN1vQkV3P/5jwI62Sd+JzvNALe/i7km2iDd8GKfK6vQYdYPnHg3ZpwK1vyRcF0dsegZhjfgoMzOAclH+nwk+Yg==", "requires": { "thrift": "0.13.0-hotfix.1" } From e30fcc2aaa4056211238a090ab4fa6c0dacf2acd Mon Sep 17 00:00:00 2001 From: Whisperity Date: Sat, 24 Aug 2024 22:18:02 +0200 Subject: [PATCH 4/4] feat: `massStoreRunAsynchronous()` Even though commit d91547313e8eeaeea0af91744f280a2cc0c45294 introduced a socket-level TCP keepalive support into the server's implementation, this was observed multiple times to not be enough to **deterministically** fix the issues with the `CodeChecker store` client hanging indefinitely when the server takes a long time processing the to-be-stored data. The underlying reasons are not entirely clear and the issue only pops up sporadically, but we did observe a few similar scenarios (such as multi-million report storage from analysing LLVM and then storing between datacentres) where it almost reliably reproduces. The symptoms (even with a configure `kepalive`) generally include the server not becoming notified about the client's disconnect, while the client process is hung on a low-level system call `read(4, ...)`, trying to get the Thrift response of `massStoreRun()` from the HTTP socket. Even if the server finishes the storage processing "in time" and sent the Thrift reply, it never reaches the client, which means it never exits from the waiting, which means it keeps either the terminal or, worse, a CI script occupied, blocking execution. This is the "more proper solution" foreshadowed in commit 15af7d8a68fe649ed14e20ab407838412b828967. Implemented the server-side logic to spawn a `MassStoreRun` task and return its token, giving the `massStoreRunAsynchronous()` API call full force. Implemented the client-side logic to use the new `task_client` module and the same logic as `CodeChecker cmd serverside-tasks --await --token TOKEN...` to poll the server for the task's completion and status. --- docs/web/user_guide.md | 13 ++ web/api/codechecker_api_shared.thrift | 4 +- web/client/codechecker_client/cmd/store.py | 129 +++++++++--------- .../codechecker_client/helpers/results.py | 10 +- web/client/codechecker_client/task_client.py | 5 +- .../codechecker_server/api/mass_store_run.py | 32 ++++- .../codechecker_server/api/report_server.py | 65 +++++---- .../task_executors/abstract_task.py | 5 + 8 files changed, 160 insertions(+), 103 deletions(-) diff --git a/docs/web/user_guide.md b/docs/web/user_guide.md index bda19706a2..1f9f364353 100644 --- a/docs/web/user_guide.md +++ b/docs/web/user_guide.md @@ -480,6 +480,19 @@ optional arguments: is given, the longest match will be removed. You may also use Unix shell-like wildcards (e.g. '/*/jsmith/'). + --detach Runs `store` in fire-and-forget mode: exit immediately + once the server accepted the analysis reports for + storing, without waiting for the server-side data + processing to conclude. Doing this is generally not + recommended, as the client will never be notified of + potential processing failures, and there is no easy way + to wait for the successfully stored results to become + available server-side for potential further processing + (e.g., `CodeChecker cmd diff`). However, using + '--detach' can significantly speed up large-scale + monitoring analyses where access to the results by a + tool is not a goal, such as in the case of non-gating + CI systems. --config CONFIG_FILE Allow the configuration from an explicit configuration file. The values configured in the config file will overwrite the values set in the command line. diff --git a/web/api/codechecker_api_shared.thrift b/web/api/codechecker_api_shared.thrift index 4a41889bbc..6b7bef2f87 100644 --- a/web/api/codechecker_api_shared.thrift +++ b/web/api/codechecker_api_shared.thrift @@ -34,13 +34,13 @@ enum ErrorCode { API_MISMATCH = 5, // REMOVED IN API v6.59 (CodeChecker v6.25.0)! - // Previously sent by report_server.thrif/codeCheckerDBAccess::massStoreRun() + // Previously sent by report_server.thrift/codeCheckerDBAccess::massStoreRun() // when the client uploaded a source file which contained errors, such as // review status source-code-comment errors. /* SOURCE_FILE = 6, */ // Never reuse the value of the enum constant! // REMOVED IN API v6.59 (CodeChecker v6.25.0)! - // Previously sent by report_server.thrif/codeCheckerDBAccess::massStoreRun() + // Previously sent by report_server.thrift/codeCheckerDBAccess::massStoreRun() // when the client uploaded a report with annotations that had invalid types. /* REPORT_FORMAT = 7, */ // Never reuse the value of the enum constant! } diff --git a/web/client/codechecker_client/cmd/store.py b/web/client/codechecker_client/cmd/store.py index 6889b6bdd1..c6f5b3ea86 100644 --- a/web/client/codechecker_client/cmd/store.py +++ b/web/client/codechecker_client/cmd/store.py @@ -31,7 +31,8 @@ from threading import Timer from typing import Dict, Iterable, List, Set, Tuple -from codechecker_api.codeCheckerDBAccess_v6.ttypes import StoreLimitKind +from codechecker_api.codeCheckerDBAccess_v6.ttypes import \ + StoreLimitKind, SubmittedRunOptions from codechecker_report_converter import twodim from codechecker_report_converter.report import Report, report_file, \ @@ -50,8 +51,8 @@ def assemble_blame_info(_, __) -> int: """ raise NotImplementedError() -from codechecker_client import client as libclient -from codechecker_client import product +from codechecker_client import client as libclient, product +from codechecker_client.task_client import await_task_termination from codechecker_common import arg, logger, cmd_config from codechecker_common.checker_labels import CheckerLabels from codechecker_common.compatibility.multiprocessing import Pool @@ -256,6 +257,24 @@ def add_arguments_to_parser(parser): "match will be removed. You may also use Unix " "shell-like wildcards (e.g. '/*/jsmith/').") + parser.add_argument("--detach", + dest="detach", + default=argparse.SUPPRESS, + action="store_true", + required=False, + help=""" +Runs `store` in fire-and-forget mode: exit immediately once the server accepted +the analysis reports for storing, without waiting for the server-side data +processing to conclude. +Doing this is generally not recommended, as the client will never be notified +of potential processing failures, and there is no easy way to wait for the +successfully stored results to become available server-side for potential +further processing (e.g., `CodeChecker cmd diff`). +However, using '--detach' can significantly speed up large-scale monitoring +analyses where access to the results by a tool is not a goal, such as in the +case of non-gating CI systems. +""") + cmd_config.add_option(parser) parser.add_argument('-f', '--force', @@ -718,7 +737,7 @@ def get_analysis_statistics(inputs, limits): return statistics_files if has_failed_zip else [] -def storing_analysis_statistics(client, inputs, run_name): +def store_analysis_statistics(client, inputs, run_name): """ Collects and stores analysis statistics information on the server. """ @@ -933,68 +952,56 @@ def main(args): description = args.description if 'description' in args else None - LOG.info("Storing results to the server...") - - try: - with _timeout_watchdog(timedelta(hours=1), - signal.SIGUSR1): - client.massStoreRun(args.name, - args.tag if 'tag' in args else None, - str(context.version), - b64zip, - 'force' in args, - trim_path_prefixes, - description) - except WatchdogError as we: - LOG.warning("%s", str(we)) - - # Showing parts of the exception stack is important here. - # We **WANT** to see that the timeout happened during a wait on - # Thrift reading from the TCP connection (something deep in the - # Python library code at "sock.recv_into"). - import traceback - _, _, tb = sys.exc_info() - frames = traceback.extract_tb(tb) - first, last = frames[0], frames[-2] - formatted_frames = traceback.format_list([first, last]) - fmt_first, fmt_last = formatted_frames[0], formatted_frames[1] - LOG.info("Timeout was triggered during:\n%s", fmt_first) - LOG.info("Timeout interrupted this low-level operation:\n%s", - fmt_last) - - LOG.error("Timeout!" - "\n\tThe server's reply did not arrive after " - "%d seconds (%s) elapsed since the server-side " - "processing began." - "\n\n\tThis does *NOT* mean that there was an issue " - "with the run you were storing!" - "\n\tThe server might still be processing the results..." - "\n\tHowever, it is more likely that the " - "server had already finished, but the client did not " - "receive a response." - "\n\tUsually, this is caused by the underlying TCP " - "connection failing to signal a low-level disconnect." - "\n\tClients potentially hanging indefinitely in these " - "scenarios is an unfortunate and known issue." - "\n\t\tSee http://github.com/Ericsson/codechecker/" - "issues/3672 for details!" - "\n\n\tThis error here is a temporary measure to ensure " - "an infinite hang is replaced with a well-explained " - "timeout." - "\n\tA more proper solution will be implemented in a " - "subsequent version of CodeChecker.", - we.timeout.total_seconds(), str(we.timeout)) - sys.exit(1) + LOG.info("Storing results to the server ...") + task_token: str = client.massStoreRunAsynchronous( + b64zip, + SubmittedRunOptions( + runName=args.name, + tag=args.tag if "tag" in args else None, + version=str(context.version), + force="force" in args, + trimPathPrefixes=trim_path_prefixes, + description=description) + ) + LOG.info("Reports submitted to the server for processing.") - # Storing analysis statistics if the server allows them. if client.allowsStoringAnalysisStatistics(): - storing_analysis_statistics(client, args.input, args.name) - - LOG.info("Storage finished successfully.") + store_analysis_statistics(client, args.input, args.name) + + if "detach" in args: + LOG.warning("Exiting the 'store' subcommand as '--detach' was " + "specified: not waiting for the result of the store " + "operation.\n" + "The server might not have finished processing " + "everything at this point, so do NOT rely on querying " + "the results just yet!\n" + "To await the completion of the processing later, " + "you can execute:\n\n" + "\tCodeChecker cmd serverside-tasks --token %s " + "--await", + task_token) + # Print the token to stdout as well, so scripts can use "--detach" + # meaningfully. + print(task_token) + return + + task_client = libclient.setup_task_client(protocol, host, port) + task_status: str = await_task_termination(LOG, task_token, + task_api_client=task_client) + + if task_status == "COMPLETED": + LOG.info("Storing the reports finished successfully.") + else: + LOG.error("Storing the reports failed! " + "The job terminated in status '%s'. " + "The comments associated with the failure are:\n\n%s", + task_status, + task_client.getTaskInfo(task_token).comments) + sys.exit(1) except Exception as ex: import traceback traceback.print_exc() - LOG.info("Storage failed: %s", str(ex)) + LOG.error("Storing the reports failed: %s", str(ex)) sys.exit(1) finally: os.close(zip_file_handle) diff --git a/web/client/codechecker_client/helpers/results.py b/web/client/codechecker_client/helpers/results.py index 399623019e..dd6978ee9c 100644 --- a/web/client/codechecker_client/helpers/results.py +++ b/web/client/codechecker_client/helpers/results.py @@ -182,10 +182,12 @@ def massStoreRun(self, name, tag, version, zipdir, force, pass @thrift_client_call - def massStoreRunAsynchronous(self, zipfile_blob: str, - store_opts: ttypes.SubmittedRunOptions) \ - -> str: - pass + def massStoreRunAsynchronous( + self, + zipfile_blob: str, + store_opts: ttypes.SubmittedRunOptions + ) -> str: + raise NotImplementedError("Should have called Thrift code!") @thrift_client_call def allowsStoringAnalysisStatistics(self): diff --git a/web/client/codechecker_client/task_client.py b/web/client/codechecker_client/task_client.py index f6666923ef..d83f562491 100644 --- a/web/client/codechecker_client/task_client.py +++ b/web/client/codechecker_client/task_client.py @@ -363,6 +363,7 @@ def _transform_product_ids_to_endpoints( product.id: product.endpoint for product in get_product_api().getProducts(None, None)} + ti["productEndpoint"] = product_id_to_endpoint[ti["productId"]] del ti["productId"] @@ -445,7 +446,7 @@ def get_product_api() -> ThriftProductHelper: ti["taskKind"], ti["summary"], ti["status"], - ti["productEndpoint"] or "", + ti.get("productEndpoint", ""), ti["actorUsername"] or "", ti["enqueuedAt"] or "", ti["startedAt"] or "", @@ -529,7 +530,7 @@ def get_product_api() -> ThriftProductHelper: ti = task_info_for_print[0] product_line = \ f" - Product: {ti['productEndpoint']}\n" \ - if ti["productEndpoint"] else "" + if "productEndpoint" in ti else "" user_line = f" - User: {ti['actorUsername']}\n" \ if ti["actorUsername"] else "" cancel_line = " - Cancelled by administrators!\n" \ diff --git a/web/server/codechecker_server/api/mass_store_run.py b/web/server/codechecker_server/api/mass_store_run.py index 743550f397..e1d2870285 100644 --- a/web/server/codechecker_server/api/mass_store_run.py +++ b/web/server/codechecker_server/api/mass_store_run.py @@ -21,7 +21,8 @@ import sqlalchemy import tempfile import time -from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, \ + cast import zipfile import zlib @@ -54,7 +55,7 @@ from ..metadata import checker_is_unavailable, MetadataInfoParser from ..product import Product as ServerProduct from ..session_manager import SessionManager -from ..task_executors.abstract_task import AbstractTask +from ..task_executors.abstract_task import AbstractTask, TaskCancelHonoured from ..task_executors.task_manager import TaskManager from .thrift_enum_helper import report_extended_data_type_str @@ -591,7 +592,13 @@ def _implementation(self, tm: TaskManager): f"'{self._product.endpoint}' is in " "a bad shape!") - m = MassStoreRun(self.data_path / "store_zip", + def __cancel_if_needed(): + tm.heartbeat(self) + if tm.should_cancel(self): + raise TaskCancelHonoured(self) + + m = MassStoreRun(__cancel_if_needed, + self.data_path / "store_zip", self._package_context, tm.configuration_database_session_factory, self._product, @@ -615,9 +622,8 @@ class MassStoreRun: # This is the place where complex implementation logic must go, but be # careful, there is no way to communicate with the user's client anymore! - # TODO: Poll the task manager at regular points for a cancel signal! - def __init__(self, + graceful_cancel: Callable[[], None], zip_dir: Path, package_context, config_db, @@ -641,6 +647,7 @@ def __init__(self, self.__config_db = config_db self.__package_context = package_context self.__product = product + self.__graceful_cancel_if_requested = graceful_cancel self.__mips: Dict[str, MetadataInfoParser] = {} self.__analysis_info: Dict[str, AnalysisInfo] = {} @@ -668,10 +675,10 @@ def __store_source_files( filename_to_hash: Dict[str, str] ) -> Dict[str, int]: """ Storing file contents from plist. """ - file_path_to_id = {} for file_name, file_hash in filename_to_hash.items(): + self.__graceful_cancel_if_requested() source_file_path = path_for_fake_root(file_name, str(source_root)) LOG.debug("Storing source file: %s", source_file_path) trimmed_file_path = trim_path_prefixes( @@ -721,6 +728,7 @@ def __add_blame_info( with DBSession(self.__product.session_factory) as session: for subdir, _, files in os.walk(blame_root): for f in files: + self.__graceful_cancel_if_requested() blame_file = Path(subdir) / f file_path = f"/{str(blame_file.relative_to(blame_root))}" blame_info, remote_url, tracking_branch = \ @@ -1502,6 +1510,7 @@ def get_skip_handler( LOG.debug("Parsing input file '%s'", f) report_file_path = os.path.join(root_dir_path, f) + self.__graceful_cancel_if_requested() self.__process_report_file( report_file_path, session, run_id, file_path_to_id, run_history_time, @@ -1604,6 +1613,7 @@ def store(self, original_zip_size: int, time_spent_on_task_preparation: float): """Store run results to the server.""" + self.__graceful_cancel_if_requested() start_time = time.time() try: @@ -1627,12 +1637,14 @@ def store(self, with StepLog(self._name, "Parse 'metadata.json's"): for root_dir_path, _, _ in os.walk(report_dir): + self.__graceful_cancel_if_requested() metadata_file_path = os.path.join( root_dir_path, 'metadata.json') self.__mips[root_dir_path] = \ MetadataInfoParser(metadata_file_path) + self.__graceful_cancel_if_requested() with StepLog(self._name, "Store look-up ID for checkers in 'metadata.json'"): checkers_in_metadata = { @@ -1656,10 +1668,16 @@ def store(self, session, report_dir, source_root, run_id, file_path_to_id, run_history_time) + self.__graceful_cancel_if_requested() session.commit() self.__load_report_ids_for_reports_with_fake_checkers( session) + # The task should not be cancelled after this point, as the + # "main" bulk of the modifications to the database had already + # been committed, and the user would be left with potentially + # a bunch of "fake checkers" visible in the database. + if self.__reports_with_fake_checkers: with StepLog( self._name, @@ -1720,6 +1738,8 @@ def store(self, LOG.error("Database error! Storing reports to the " "database failed: %s", ex) raise + except TaskCancelHonoured: + raise except Exception as ex: LOG.error("Failed to store results: %s", ex) import traceback diff --git a/web/server/codechecker_server/api/report_server.py b/web/server/codechecker_server/api/report_server.py index 9c8bd6f07a..24f2b39280 100644 --- a/web/server/codechecker_server/api/report_server.py +++ b/web/server/codechecker_server/api/report_server.py @@ -3867,18 +3867,10 @@ def getMissingContentHashesForBlameInfo(self, file_hashes): return list(set(file_hashes) - set(fc.content_hash for fc in q)) - @exc_to_thrift_reqfail - @timeit - def massStoreRun(self, - name: str, - tag: Optional[str], - version: str, - b64zip: str, - force: bool, - trim_path_prefixes: Optional[List[str]], - description: Optional[str]) -> int: + def __massStoreRun_common(self, is_async: bool, zipfile_blob: str, + store_opts: SubmittedRunOptions) -> str: self.__require_store() - if not name: + if not store_opts.runName: raise ValueError("A run name is needed to know where to store!") from .mass_store_run import MassStoreRunInputHandler, MassStoreRunTask @@ -3888,26 +3880,48 @@ def massStoreRun(self, self._task_manager, self._context, self._product.id, - name, - description, - tag, - version, - force, - trim_path_prefixes, - b64zip, + store_opts.runName, + store_opts.description, + store_opts.tag, + store_opts.version, + store_opts.force, + store_opts.trimPathPrefixes, + zipfile_blob, self._get_username()) ih.check_store_input_validity_at_face_value() - m: MassStoreRunTask = ih.create_mass_store_task(False) + m: MassStoreRunTask = ih.create_mass_store_task(is_async) self._task_manager.push_task(m) - LOG.info("massStoreRun(): Running as '%s' ...", m.token) + return m.token + + @exc_to_thrift_reqfail + @timeit + def massStoreRun(self, + name: str, + tag: Optional[str], + version: str, + b64zip: str, + force: bool, + trim_path_prefixes: Optional[List[str]], + description: Optional[str]) -> int: + store_opts = SubmittedRunOptions(runName=name, + tag=tag, + version=version, + force=force, + trimPathPrefixes=trim_path_prefixes, + description=description, + ) + token = self.__massStoreRun_common(False, b64zip, store_opts) + + LOG.info("massStoreRun(): Blocking until task '%s' terminates ...", + token) # To be compatible with older (<= 6.24, API <= 6.58) clients which # may keep using the old API endpoint, simulate awaiting the # background task in the API handler. while True: time.sleep(5) - t = self._task_manager.get_task_record(m.token) + t = self._task_manager.get_task_record(token) if t.is_in_terminated_state: if t.status == "failed": raise codechecker_api_shared.ttypes.RequestFailed( @@ -3937,13 +3951,8 @@ def massStoreRun(self, @timeit def massStoreRunAsynchronous(self, zipfile_blob: str, store_opts: SubmittedRunOptions) -> str: - import pprint - LOG.info("massStoreRunAsynchronous() called with:\n\t - %d bytes " - "input\n\t - Options:\n\n%s", len(zipfile_blob), - pprint.pformat(store_opts.__dict__, indent=2, depth=8)) - raise codechecker_api_shared.ttypes.RequestFailed( - codechecker_api_shared.ttypes.ErrorCode.GENERAL, - "massStoreRunAsynchronous() not implemented in this server build!") + token = self.__massStoreRun_common(True, zipfile_blob, store_opts) + return token @exc_to_thrift_reqfail @timeit diff --git a/web/server/codechecker_server/task_executors/abstract_task.py b/web/server/codechecker_server/task_executors/abstract_task.py index bb665f034e..1f1508ae17 100644 --- a/web/server/codechecker_server/task_executors/abstract_task.py +++ b/web/server/codechecker_server/task_executors/abstract_task.py @@ -174,6 +174,11 @@ def _log_drop_and_abandon(db_task: DBTask): task_manager._mutate_task_record(self, _log_cancel_and_abandon) else: task_manager._mutate_task_record(self, _log_drop_and_abandon) + + import traceback + LOG.debug("Task '%s' honoured the administrator's cancel request " + "at:\n%s", + self.token, traceback.format_exc()) except Exception as ex: LOG.error("Failed to execute task '%s' on machine '%s' " "executor #%d: %s",