#!/usr/bin/env python3 """Provisional loopback HTTP workload using Gunicorn and disposable PostgreSQL.""" import argparse import concurrent.futures import hashlib import http.cookiejar import json import math import os import platform import signal import socket import subprocess import sys import tempfile import threading import time import urllib.error import urllib.parse import urllib.request import uuid from datetime import UTC, datetime from pathlib import Path def summarize(rows): durations = sorted(row["seconds"] for row in rows) return { "requests": len(rows), "errors": sum(not r["ok"] for r in rows), "p95_seconds": durations[math.ceil(len(durations) * 0.95) - 1] if durations else None, } LEGACY_OPERATIONS = [ "list", "search", "tender-create", "detail", "lot-create", "task-create", "document-upload", "document-download", ] DEPENDENCIES = { "list": None, "search": None, "tender-create": None, "detail": "tender-create", "tender-edit": "tender-create", "lot-create": "tender-create", "lot-edit": "lot-create", "task-create": "tender-create", "task-edit": "task-create", "document-upload": "tender-create", "document-download": "document-upload", } def operation_plan(operations): if not isinstance(operations, list) or not operations: raise ValueError("operations must be a nonempty ordered list") seen = set() for operation in operations: if operation not in DEPENDENCIES: raise ValueError(f"unknown operation: {operation}") dependency = DEPENDENCIES[operation] if dependency and dependency not in seen: raise ValueError(f"{operation} requires earlier {dependency}") if operation in seen and (operation.endswith("-create") or operation == "document-upload"): raise ValueError(f"only one {operation} per cycle; repeat reads/edits to tune mix") seen.add(operation) return list(operations) def phase_plan(config): phases = config.get( "phases", [ { "name": "ordinary", "kind": "load", "duration_seconds": config["duration_seconds"], "think_seconds": config["think_seconds"], } ], ) if not isinstance(phases, list) or not phases: raise ValueError("phases must be a nonempty list") names = set() result = [] for item in phases: item = dict(item) name = item.get("name") if not isinstance(name, str) or not name or name in names or name in ("idle", "startup"): raise ValueError("phase names must be unique and not idle/startup") names.add(name) if item.get("kind") == "restart": if config.get("runtime") != "container": raise ValueError("restart phase requires container runtime") elif item.get("kind") == "load": for key in ("duration_seconds", "think_seconds"): value = item.get(key) if type(value) not in (int, float) or not math.isfinite(value) or value < 0: raise ValueError(f"invalid phase {key}") if not item["duration_seconds"]: raise ValueError("load duration must be positive") item["operations"] = operation_plan( item.get("operations", config.get("operations", LEGACY_OPERATIONS)) ) else: raise ValueError("phase kind must be load or restart") result.append(item) if not any(p["kind"] == "load" for p in result): raise ValueError("at least one load phase required") return result def expected_requests(completed, operations, users): return sum(completed) * len(operations) + users * 2 def wait_ready(base_url, timeout=30): deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: with urllib.request.urlopen(base_url + "/health/", timeout=1) as response: if response.status == 200: return except OSError: pass time.sleep(0.1) raise RuntimeError("application readiness timeout") def container_url(name): binding = subprocess.check_output( ["docker", "port", name, "8000/tcp"], text=True, timeout=10 ).strip() host, port = binding.rsplit(":", 1) if host != "127.0.0.1" or not port.isdigit(): raise ValueError("synthetic runtime must have one loopback HTTP binding") return f"http://127.0.0.1:{port}" def restart_container(name): subprocess.run( ["docker", "restart", "--time", "5", name], check=True, timeout=20, stdout=subprocess.DEVNULL, ) # Docker can allocate a different random published port after restart. endpoint = container_url(name) wait_ready(endpoint) return endpoint def validate(config): for key in ( "users", "iterations", "tenders", "document_bytes", "workers", "timeout_seconds", ): if type(config[key]) is not int or config[key] < 1: raise ValueError(f"{key} must be a positive integer") for key in ( "think_seconds", "ordinary_p95_seconds", "duration_seconds", "idle_seconds", ): if not isinstance(config[key], (int, float)) or not math.isfinite(config[key]): raise ValueError(f"{key} must be finite") if config["think_seconds"] < 0 or config["ordinary_p95_seconds"] <= 0: raise ValueError("invalid timing parameter") if config["duration_seconds"] <= 0 or config["idle_seconds"] < 0: raise ValueError("duration must be positive and idle nonnegative") if config["document_bytes"] < 8: raise ValueError("document_bytes must be at least 8") if config.get("runtime", "host") not in ("host", "container"): raise ValueError("unsupported runtime") if config.get("runtime") == "container": if ( not isinstance(config["app_cpu_limit"], (int, float)) or not 0 < config["app_cpu_limit"] <= 8 ): raise ValueError("app_cpu_limit must be positive and at most 8") if not config.get("app_image") or not config.get("app_memory_limit"): raise ValueError("container image and memory limit required") phase_plan(config) return config def stop_process(server): """Bounded teardown of a process group, including Gunicorn workers.""" if server is None: return try: os.killpg(server.pid, signal.SIGTERM) except ProcessLookupError: pass try: server.wait(timeout=5) except subprocess.TimeoutExpired: try: os.killpg(server.pid, signal.SIGKILL) except ProcessLookupError: pass server.wait(timeout=5) def guarded_sampler(function, errors): try: function() except Exception as exc: errors.append({"sampler": function.__name__, "error_type": type(exc).__name__}) def sampling_gaps(samples, db_samples, errors): gaps = list(errors) if any(s.get("sample_error") for s in samples): gaps.append({"sampler": "application", "reason": "sample failed"}) good = [s for s in samples if "monotonic_seconds" in s] if len(good) < 2 or not db_samples: gaps.append({"sampler": "coverage", "reason": "insufficient samples"}) if any( b["monotonic_seconds"] - a["monotonic_seconds"] > 3 for a, b in zip(good, good[1:]) if a.get("phase") == b.get("phase") ): gaps.append({"sampler": "application", "reason": "sampling interval exceeds 3 seconds"}) return gaps def run(config, root, failure_output=None): # Never consume a deployment database, media tree, session or credential. os.environ["DATABASE_URL"] = os.environ["SYNTHETIC_DATABASE_URL"] os.environ["SECRET_KEY"] = "synthetic-disposable-not-a-deployment-key" os.environ["DJANGO_SETTINGS_MODULE"] = "synthetic_settings" container_mode = config.get("runtime", "host") == "container" media = "/tmp/synthetic-media" if container_mode else str(root / "media") issue_db = "/tmp/synthetic-issues.db" if container_mode else str(root / "issues.db") (root / "synthetic_settings.py").write_text( "from vergabe_teilnahme.settings.base import *\n" 'DEBUG = False\nALLOWED_HOSTS = ["127.0.0.1"]\nNETKINGDOM_ENABLED = False\n' f"MEDIA_ROOT = {media!r}\n" f"ISSUE_FACADE_LOCAL_DB = {issue_db!r}\n" 'ISSUE_FACADE_GITEA = None\nFORCE_SCRIPT_NAME = None\nMEDIA_URL = "/media/"\n' 'EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"\n' "SESSION_COOKIE_SECURE = False\nCSRF_COOKIE_SECURE = False\n" ) sys.path.insert(0, str(root)) import django django.setup() from django.core.management import call_command from django.db import connections from vergabe_teilnahme.apps.accounts.models import Mitarbeiter from vergabe_teilnahme.apps.aufgaben.models import Aufgabe from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung from vergabe_teilnahme.apps.dokumente.models import Dokument from vergabe_teilnahme.apps.lose.models import Los call_command("migrate", verbosity=0) for i in range(config["tenders"]): Ausschreibung.objects.create(titel=f"Synthetic tender {i}", ausschreiber="Synthetic") users = [] password = uuid.uuid4().hex for i in range(config["users"]): user = Mitarbeiter.objects.create_user(username=f"synthetic-{i}", password=password) users.append(user.username) with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] base_url = f"http://127.0.0.1:{port}" env = dict(os.environ, PYTHONPATH=str(root) + os.pathsep + str(Path.cwd())) server_log = (root / "server.log").open("w") server = None app_container = None sampling_done = threading.Event() sampler_threads = [] sampler_errors = [] sampling_lock = threading.Lock() try: startup_started = time.monotonic() if container_mode: app_container = os.environ["SYNTHETIC_CONTAINER"] + "-app" root.chmod(0o755) subprocess.run( [ "docker", "run", "--detach", "--pull=never", "--name", app_container, "--network", os.environ["SYNTHETIC_NETWORK"], "--publish", "127.0.0.1::8000", "--cpus", str(config["app_cpu_limit"]), "--memory", config["app_memory_limit"], "--pids-limit", "256", "--mount", ( f"type=bind,src={root / 'synthetic_settings.py'}," "dst=/synthetic_settings.py,readonly" ), "--env", "PYTHONPATH=/", "--env", "DJANGO_SETTINGS_MODULE=synthetic_settings", "--env", "SECRET_KEY=synthetic-disposable-not-a-deployment-key", "--env", "DATABASE_URL=postgres://synthetic:synthetic-local-only@db:5432/synthetic", config["app_image"], "gunicorn", "vergabe_teilnahme.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", str(config["workers"]), "--timeout", "30", ], check=True, timeout=30, stdout=subprocess.DEVNULL, ) base_url = container_url(app_container) else: server = subprocess.Popen( [ sys.executable, "-m", "gunicorn", "vergabe_teilnahme.wsgi:application", "--bind", f"127.0.0.1:{port}", "--workers", str(config["workers"]), "--timeout", "30", ], start_new_session=True, env=env, stdout=server_log, stderr=server_log, ) samples = [] db_samples = [] phase = {"name": "startup", "kind": "startup"} def sample(): ticks_per_second = os.sysconf("SC_CLK_TCK") while not sampling_done.is_set(): if phase.get("kind") == "restart": samples.append( { "at": datetime.now(UTC).isoformat(), "phase": phase["name"], "expected_unavailable": True, "reason": "planned restart", } ) sampling_done.wait(0.5) continue if container_mode: with sampling_lock: value = subprocess.check_output( [ "docker", "exec", app_container, "sh", "-c", ( "cat /sys/fs/cgroup/cpu.stat; echo MEMORY; " "cat /sys/fs/cgroup/memory.current" ), ], text=True, timeout=5, ) cpu_text, memory_text = value.split("MEMORY\n") counters = { k: int(v) for k, v in (line.split() for line in cpu_text.splitlines()) } samples.append( { "at": datetime.now(UTC).isoformat(), "monotonic_seconds": time.monotonic(), "phase": phase["name"], "cpu_seconds": counters["usage_usec"] / 1e6, "rss_bytes": int(memory_text.strip()), "memory_kind": "cgroup-memory-current", "cpu_stat": counters, "pids": [app_container], } ) sampling_done.wait(0.5) continue pids = [server.pid] try: pids += [ int(x) for x in Path(f"/proc/{server.pid}/task/{server.pid}/children") .read_text() .split() ] cpu, rss = 0, 0 for pid in pids: fields = Path(f"/proc/{pid}/stat").read_text().split(") ", 1)[1].split() cpu += (int(fields[11]) + int(fields[12])) / ticks_per_second rss += int(fields[21]) * os.sysconf("SC_PAGE_SIZE") samples.append( { "at": datetime.now(UTC).isoformat(), "monotonic_seconds": time.monotonic(), "phase": phase["name"], "cpu_seconds": cpu, "rss_bytes": rss, "pids": pids, } ) except (OSError, ValueError): samples.append({"at": datetime.now(UTC).isoformat(), "sample_error": True}) sampling_done.wait(0.5) def sample_db(): while not sampling_done.is_set(): value = subprocess.check_output( [ "docker", "stats", "--no-stream", "--format", "{{json .}}", os.environ["SYNTHETIC_CONTAINER"], ], text=True, timeout=5, ) db_samples.append( { "at": datetime.now(UTC).isoformat(), "phase": phase["name"], "docker_stats": json.loads(value), } ) sampling_done.wait(1) sampler = threading.Thread( target=guarded_sampler, args=(sample, sampler_errors), daemon=True ) db_sampler = threading.Thread( target=guarded_sampler, args=(sample_db, sampler_errors), daemon=True ) sampler_threads.extend([sampler, db_sampler]) sampler.start() db_sampler.start() wait_ready(base_url) startup_seconds = time.monotonic() - startup_started phase.update(name="idle", kind="idle") time.sleep(config["idle_seconds"]) plan = phase_plan(config) active_plan = [] phase_results = [] transport_rebinds = [] phase_completed = {} def worker(i): jar = http.cookiejar.CookieJar() class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(jar), NoRedirect() ) rows = [] def request(operation, method, path, data=None, expected=200, transfer=False): started = time.perf_counter() try: headers = {} body = None if method == "post": token = next((c.value for c in jar if c.name == "csrftoken"), "") fields = dict(data or {}, csrfmiddlewaretoken=token) if transfer: boundary = "synthetic-" + uuid.uuid4().hex body = b"" for name, value in fields.items(): body += ( f"--{boundary}\r\nContent-Disposition: form-data; " f'name="{name}"\r\n\r\n{value}\r\n' ).encode() body += ( ( f"--{boundary}\r\nContent-Disposition: form-data; " 'name="datei"; filename="synthetic.pdf"\r\n' "Content-Type: application/pdf\r\n\r\n" ).encode() + b"%PDF-1.4" + b"x" * (config["document_bytes"] - 8) + f"\r\n--{boundary}--\r\n".encode() ) headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" else: body = urllib.parse.urlencode(fields).encode() headers["Referer"] = base_url + path req = urllib.request.Request( base_url + path, data=body, headers=headers, method=method.upper() ) try: response = opener.open(req, timeout=config["timeout_seconds"]) except urllib.error.HTTPError as exc: response = exc received = len(response.read()) status = response.status ok = status == expected if transfer and method == "get": ok = ok and received == config["document_bytes"] response.close() except Exception: status, ok, received = None, False, 0 rows.append( { "phase": phase["name"], "user": i, "operation": operation, "transfer": transfer, "seconds": time.perf_counter() - started, "status": status, "ok": ok, "response_bytes": received, } ) return ok request("login-form", "get", "/accounts/login/") barrier.wait(timeout=30) request( "login", "post", "/accounts/login/", {"username": users[i], "password": password}, 302, ) for n in range(config["iterations"]): if time.monotonic() >= deadline: break title = f"Workload-{phase['name']}-{i}-{n}" tender = lot = task = document = None base = None for index, operation in enumerate(active_plan): dependency = DEPENDENCIES[operation] entity = { "tender-create": tender, "lot-create": lot, "task-create": task, "document-upload": document, } if dependency and entity[dependency] is None: rows.append( { "phase": phase["name"], "user": i, "operation": operation, "transfer": operation.startswith("document-"), "seconds": 0, "status": None, "ok": False, "response_bytes": 0, "reason": "dependency operation failed", } ) continue if operation == "list": request(operation, "get", "/ausschreibungen/") elif operation == "search": request(operation, "get", "/ausschreibungen/?q=Synthetic") elif operation == "tender-create": if request( operation, "post", "/ausschreibungen/neu/", {"titel": title, "ausschreiber": "Synthetic"}, 302, ): tender = Ausschreibung.objects.filter(titel=title).first() base = f"/ausschreibungen/{tender.pk}/" if tender else None elif operation == "detail": request(operation, "get", base) elif operation == "tender-edit": changed = f"{title} edited-{index}" request( operation, "post", base + "bearbeiten/", {"titel": changed, "ausschreiber": "Synthetic edited"}, 302, ) tender.refresh_from_db() rows[-1]["ok"] &= tender.titel == changed elif operation == "lot-create": request( operation, "post", base + "lose/neu/", {"losnummer": "1", "lostitel": "Synthetic lot"}, 302, ) lot = Los.objects.filter(ausschreibung=tender).first() elif operation == "lot-edit": changed = f"Edited lot-{index}" request( operation, "post", base + f"lose/{lot.pk}/bearbeiten/", {"losnummer": "1", "lostitel": changed}, 302, ) lot.refresh_from_db() rows[-1]["ok"] &= lot.lostitel == changed elif operation == "task-create": request( operation, "post", base + "aufgaben/neu/", {"titel": "Synthetic task", "typ": "fachlich", "prioritaet": 2}, 302, ) task = Aufgabe.objects.filter(ausschreibung=tender).first() elif operation == "task-edit": changed = f"Edited task-{index}" request( operation, "post", base + f"aufgaben/{task.pk}/bearbeiten/", {"titel": changed, "typ": "fachlich", "prioritaet": 1}, 302, ) task.refresh_from_db() rows[-1]["ok"] &= task.titel == changed and task.prioritaet == 1 elif operation == "document-upload": request( operation, "post", base + "dokumente/hochladen/", {"kategorie": "intern", "version": "1.0"}, 302, True, ) document = Dokument.objects.filter(ausschreibung=tender).first() elif operation == "document-download": request(operation, "get", document.datei.url, transfer=True) completed[i] += 1 time.sleep(active_phase["think_seconds"]) connections.close_all() return rows started = datetime.now(UTC).isoformat() before = time.perf_counter() rows = [] for active_phase in plan: phase.update(name=active_phase["name"], kind=active_phase["kind"]) phase_started = time.monotonic() if active_phase["kind"] == "restart": old_url = base_url with sampling_lock: base_url = restart_container(app_container) transport_rebinds.append( {"phase": active_phase["name"], "before": old_url, "after": base_url} ) phase_results.append( { "name": active_phase["name"], "kind": "restart", "seconds": time.monotonic() - phase_started, "ready": True, } ) continue active_plan = active_phase["operations"] deadline = time.monotonic() + active_phase["duration_seconds"] completed = [0] * config["users"] barrier = threading.Barrier(config["users"]) with concurrent.futures.ThreadPoolExecutor(max_workers=config["users"]) as pool: phase_rows = [ r for group in pool.map(worker, range(config["users"])) for r in group ] phase_completed[active_phase["name"]] = completed phase_ordinary = summarize([r for r in phase_rows if not r["transfer"]]) phase_transfers = summarize([r for r in phase_rows if r["transfer"]]) expected = expected_requests(completed, active_plan, config["users"]) phase_results.append( { "name": active_phase["name"], "kind": "load", "seconds": time.monotonic() - phase_started, "operations": active_plan, "expected_requests": expected, "actual_requests": len(phase_rows), "complete": all(completed) and len(phase_rows) == expected, "ordinary": phase_ordinary, "document_transfer": phase_transfers, } ) rows.extend(phase_rows) ordinary = summarize([r for r in rows if not r["transfer"]]) transfers = summarize([r for r in rows if r["transfer"]]) complete = all(p.get("complete", p.get("ready", False)) for p in phase_results) result = { "schema_version": 2, "startup_seconds": startup_seconds, "transport_rebinds": transport_rebinds, "phase_results": phase_results, "started_at": started, "finished_at": datetime.now(UTC).isoformat(), "duration_seconds": time.perf_counter() - before, "config": config, "environment": { "python": platform.python_version(), "django": django.get_version(), "platform": platform.platform(), "database": "disposable PostgreSQL 16 container", "transport": "loopback HTTP Gunicorn", "workers": config["workers"], }, "ordinary": ordinary, "document_transfer": transfers, "complete": complete, "provisional_pass": complete and ordinary["errors"] == 0 and transfers["errors"] == 0 and all( p["ordinary"]["p95_seconds"] <= config["ordinary_p95_seconds"] for p in phase_results if p["kind"] == "load" ), "limitations": [ "No browser, ingress, TLS or SSO", "No CPU/memory allocation recommendation or human acceptance", "No deployed-release hardware or shared-service contention acceptance", ], "operations": rows, "runtime_samples": samples, "database_samples": db_samples, "completed_iterations": phase_completed, } sampling_done.set() for thread in sampler_threads: thread.join(timeout=7) gaps = sampling_gaps(samples, db_samples, sampler_errors) if any(thread.is_alive() for thread in sampler_threads): gaps.append({"sampler": "cleanup", "reason": "thread did not stop"}) result["measurement_gaps"] = gaps result["measurement_complete"] = not gaps if container_mode: result["app_container"] = json.loads( subprocess.check_output( ["docker", "inspect", "--format", "{{json .}}", app_container], text=True, timeout=10, ) ) # Retain only public runtime identifiers/limits, never full container environment. info = result.pop("app_container") result["app_runtime"] = { "image_id": info["Image"], "nano_cpus": info["HostConfig"]["NanoCpus"], "memory_bytes": info["HostConfig"]["Memory"], "memory_kind": "cgroup-memory-current includes page cache", "oom_killed": info["State"]["OOMKilled"], "dockerfile_sha256": hashlib.sha256(Path("Dockerfile").read_bytes()).hexdigest(), } return result except Exception as exc: if failure_output is not None: failure = { "schema_version": 2, "status": "failed", "complete": False, "provisional_pass": False, "measurement_complete": False, "error_type": type(exc).__name__, "phase": locals().get("phase", {"name": "startup"}), "config": config, "phase_results": locals().get("phase_results", []), "operations": locals().get("rows", []), "runtime_samples": locals().get("samples", []), "database_samples": locals().get("db_samples", []), "measurement_gaps": [{"reason": "runtime failure; partial evidence only"}], "driver_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), } failure_output.write_text(json.dumps(failure, indent=2) + "\n") raise finally: sampling_done.set() for thread in sampler_threads: if thread.ident is not None: thread.join(timeout=7) stop_process(server) if app_container is not None: subprocess.run( ["docker", "rm", "--force", "--volumes", app_container], timeout=15, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) server_log.close() connections.close_all() if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() config = validate(json.loads(args.config.read_text())) sys.path.insert(0, str(Path(__file__).resolve().parents[1])) name = "vergabe-synthetic-" + uuid.uuid4().hex[:12] network = name + "-network" network_args = [] try: if config.get("runtime") == "container": subprocess.run( ["docker", "network", "create", network], check=True, timeout=15, stdout=subprocess.DEVNULL, ) os.environ["SYNTHETIC_NETWORK"] = network network_args = ["--network", network, "--network-alias", "db"] # Cached image only, loopback random host port, no existing volume or database. subprocess.run( [ "docker", "run", "--detach", "--pull=never", "--name", name, "--publish", "127.0.0.1::5432", "--env", "POSTGRES_DB=synthetic", "--env", "POSTGRES_USER=synthetic", "--env", "POSTGRES_PASSWORD=synthetic-local-only", *network_args, "postgres:16-alpine", ], check=True, stdout=subprocess.DEVNULL, ) for _ in range(100): ready = subprocess.run( ["docker", "exec", name, "pg_isready", "-U", "synthetic"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if ready.returncode == 0: break time.sleep(0.1) else: raise RuntimeError("isolated PostgreSQL startup failed") port = ( subprocess.check_output(["docker", "port", name, "5432/tcp"], text=True) .strip() .split(":")[-1] ) os.environ["SYNTHETIC_CONTAINER"] = name os.environ["SYNTHETIC_DATABASE_URL"] = ( f"postgres://synthetic:synthetic-local-only@127.0.0.1:{port}/synthetic" ) with tempfile.TemporaryDirectory(prefix="vergabe-synthetic-") as directory: result = run(config, Path(directory), args.output) result["postgres_image_id"] = subprocess.check_output( ["docker", "inspect", "--format", "{{.Image}}", name], text=True ).strip() result["postgres_final_stats"] = json.loads( subprocess.check_output( ["docker", "stats", "--no-stream", "--format", "{{json .}}", name], text=True, ) ) finally: subprocess.run( ["docker", "rm", "--force", "--volumes", name], timeout=15, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if network_args: subprocess.run( ["docker", "network", "rm", network], check=True, timeout=15, stdout=subprocess.DEVNULL, ) result["source_revision"] = subprocess.check_output( ["git", "rev-parse", "HEAD"], text=True ).strip() result["driver_sha256"] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() app_files = sorted(Path("vergabe_teilnahme").rglob("*.py")) + [ Path("uv.lock"), Path("pyproject.toml"), ] result["app_source_sha256"] = hashlib.sha256( b"".join(str(p).encode() + b"\0" + p.read_bytes() for p in app_files) ).hexdigest() result["host"] = { "logical_cpus": os.cpu_count(), "affinity_cpus": len(os.sched_getaffinity(0)), "cpu_model": next( ( x.split(":", 1)[1].strip() for x in Path("/proc/cpuinfo").read_text().splitlines() if x.startswith("model name") ), "unknown", ), "memory_total": next( x for x in Path("/proc/meminfo").read_text().splitlines() if x.startswith("MemTotal:") ), "cgroup_cpu_max": Path("/sys/fs/cgroup/cpu.max").read_text().strip() if Path("/sys/fs/cgroup/cpu.max").exists() else "unavailable", "app_cpu_quota": config.get("app_cpu_limit", "not imposed by harness"), } result["config_sha256"] = hashlib.sha256(args.config.read_bytes()).hexdigest() args.output.write_text(json.dumps(result, indent=2) + "\n") print( json.dumps( { k: result[k] for k in ( "ordinary", "document_transfer", "complete", "provisional_pass", ) } ) ) raise SystemExit(0 if result["provisional_pass"] and result["measurement_complete"] else 1)