vergabe-teilnahme/docs/synthetic-pilot.md
tegwick dca6683a8a
All checks were successful
Application acceptance / application-tests (push) Successful in 1m53s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat: drive isolated remote synthetic replay with namespaced pod restart
Adds runtime: remote to the synthetic pilot driver (external base URL and
disposable database, restart via the driver's namespaced ServiceAccount,
UTC phase windows for external cgroup sampling) and the target replay
profile pinned to the deployed release digest and 3 Gunicorn workers.
Five focused tests added; 22 tests pass, ruff clean.

For railiance-apps RAPPS-WP-0014-T04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 8340@bnt-lap001
Assistant-Session: 2e192b3d-b3e8-4403-94d4-d55a2c8b476c
2026-10-21 08:36:49 +02:00

10 KiB

Provisional synthetic pilot workload

The owner implementation is scripts/synthetic_pilot.py, configured by config/synthetic-pilot.json. It reuses the application routes/forms exercised by existing workflow tests. There was no existing performance driver in this repository or railiance-apps. The driver uses real loopback HTTP, cookie sessions, CSRF, Gunicorn and a fresh PostgreSQL 16 container; it never points to an existing service/database. No live credentials, customer records or publication are needed.

Run from the application repository with its installed locked environment and cached postgres:16-alpine Docker image:

.venv/bin/python scripts/test_synthetic_pilot.py
.venv/bin/python scripts/synthetic_pilot.py \
  --config config/synthetic-pilot.json --output /tmp/vergabe-sizing.json

The container uses a unique name, random loopback database port and disposable volume; cleanup removes only that container and its volume. Gunicorn binds a random loopback port. Database, media and issue storage are isolated; temporary passwords exist only for generated local users. The local settings disable SSO; this measures local password login, not the invited deployment's identity flow. No seed_dev command or existing PostgreSQL volume is reused.

Adjustable assumptions

Defaults are two users, two synchronous Gunicorn workers, 100 initial tenders, 180 seconds of ordinary work after 30 seconds idle, and 0.2 seconds think time per cycle. Each cycle creates a tender, lot and task, and reads list, search and detail pages; it uploads then downloads one 1 MiB synthetic PDF. Thus the corpus grows during the run. This is an explicit provisional operation mix, not observed customer demand. iterations is a per-user safety ceiling; duration normally ends the run first. User/worker count, duration, idle, iteration ceiling, corpus, think time, document size, HTTP timeout and ordinary p95 threshold are tunable. The PDF bytes are a transfer fixture, not a claim of document parser fidelity.

Pass requires all expected operations, ordinary p95 <= 2 seconds and zero failed ordinary/transfer requests. Redirects are not automatically followed: rejected login redirects cannot count as successful data-page responses. Document transfer samples are reported separately. Ordinary samples include the login page/login; fixture creation/migration and readiness are outside the measured workload.

Coverage and interpretation

The result retains individual status/latency/response-size records, phase-tagged 0.5-second Gunicorn process CPU-time/RSS samples, separate interval Docker database stats, driver/config/application source digests, runtime versions, image identity, hardware and UTC boundaries. App RSS sums process resident sets and can double-count shared pages; CPU reflects this host and has no app quota. Database snapshots are separate demand, not allocated to the app by inference. A shared deployment database requires a baseline and attribution before using these observations for capacity.

Historical baseline receipts readiness-checked startup without timing it. Idle and fixed-think-time ordinary phases are measured; restart and a distinct burst phase are not. Ingress/TLS/browser/SSO, production quotas, throttling and shared services are unmeasured. Local success does not establish human acceptance or prove the existing 60m production request. Revisit when user count, corpus, document sizes, workers, image, database, hardware or quota changes, and after an owner-approved target-runtime observation window. The existing VERGABE-WP-0019 / RAPPS-WP-0014 human acceptance remains separate.

Provisional allocation calculation

config/synthetic-sizing-margins.json keeps safety factors tunable. After a complete passing run, derive a candidate envelope without applying it:

.venv/bin/python scripts/summarize_synthetic_sizing.py /tmp/vergabe-sizing.json \
  --margins config/synthetic-sizing-margins.json --output /tmp/vergabe-sizing-envelope.json

CPU request is measured aggregate ordinary p95 times 1.5, rounded up to 25m; CPU limit is observed sample peak times 1.5, rounded up to 100m. Memory request is peak summed RSS times 1.25 and limit times 1.5, each rounded up to 64Mi. These are conservative test candidates for this configuration, not measured production requirements. The normalized per-worker CPU value divides aggregate by configured workers; it is not a claim that worker load is equal. No database capacity is folded into the app recommendation. No resource setting is applied by this tool.

The baseline profiles create rather than edit tender/lot/task records. The phased profile below adds edits and lifecycle/burst coverage; target hardware and shared contention still require owner evidence before full pilot sizing acceptance.

Existing-limit container comparison

Build the existing owner runtime target, then use the separate configurable container profile. Neither command publishes an image or modifies a deployment:

docker build --target runtime -t vergabe-synthetic:local-11f7c8a .
.venv/bin/python scripts/synthetic_pilot.py \
  --config config/synthetic-pilot-container.json \
  --output /tmp/vergabe-container-sizing.json

The profile retains the same workload assumptions and adds runtime: container, app_image, app_cpu_limit: 1.0, and app_memory_limit: 1g. This tests the accepted 1000m CPU limit and 1Gi memory limit; a 60m request is scheduling reservation, not an execution quota. The image is built from the owner Dockerfile, with its locked runtime dependencies. App and database are attached to a uniquely named bridge; only random loopback host ports are published. This is data/process isolation, not an egress firewall. Synthetic settings disable SSO and external issue routing.

Container samples read cpu.stat (including throttling counters) and memory.current inside its cgroup. For raw-schema compatibility the latter uses rss_bytes with memory_kind: cgroup-memory-current; it includes page cache and is not comparable directly with the earlier host summed-RSS measurement. The short sampling exec processes also consume a small amount of the container quota. The database remains separately sampled, outside the application's CPU limit.

All runtime paths use bounded finally cleanup; host Gunicorn runs in its own process group for TERM/KILL teardown. Sampler failures are retained in measurement_gaps, make measurement_complete false, and return a nonzero CLI status even if HTTP latency passes. The envelope calculator refuses both gaps and capped runs: a capped run must be compared directly against its limit, not extrapolated into a recommendation to raise that limit.

Named operation and lifecycle profiles

config/synthetic-pilot-phases.json selects an ordered operation list. Repeat reads or edits to change frequency, or omit an operation with its dependants. Named operations are list, search, tender-create/detail/tender-edit, lot-create/ lot-edit, task-create/task-edit and document-upload/document-download. The parser rejects unknown operations, missing/late dependencies and repeated creates or uploads before starting Docker. Expected counts derive from the selected plan, completed cycles and two login requests per user/phase; eight is no longer a hardcoded cycle count. Edit responses are checked against persisted field values, and downloads must return exactly the configured byte count.

Load phases may override the operation list and set duration/think time. Names must be unique; a restart phase requires container mode. The bounded profile is: measured startup,30s idle,90s ordinary work,30s burst with zero think time, one restart/readiness check, then60s recovery traffic. It keeps two users/workers, 100 initial tenders,1MiB transfers and the existing1000m/1Gi limits.

.venv/bin/python scripts/synthetic_pilot.py \
  --config config/synthetic-pilot-phases.json --output /tmp/vergabe-phases.json
.venv/bin/python scripts/compare_synthetic_limits.py /tmp/vergabe-phases.json \
  --output /tmp/vergabe-phases-comparison.json

Startup timing begins before application launch; resource sampling begins once its loopback binding exists. Planned restart downtime is marked explicitly as unavailable for app resource collection, with readiness timing and recovery measurements retained. It is not silently treated as a sample failure or as continuous CPU evidence. Same-phase sample gaps still refuse measurement; the comparison never subtracts CPU counters across restart resets or phase boundaries. Docker may change a random published port after restart, so the driver rereads and verifies its loopback binding before readiness and subsequent requests.

Each load phase retains its own ordinary/transfer latency and error summaries, request expectations and completion. Partial operations/resource samples are written on runtime failure before cleanup. Successful synthetic restart/recovery is not backup/restore acceptance or customer/human acceptance. Target-release, hardware and shared-contention evidence belongs to receiving task railiance-apps/workplans/RAPPS-WP-0014-vergabe-invited-pilot.md T04, linked to CUST-WP-0071-T02 and RESOURCE-WP-0008-T02. Its routine read-only capture/preparation is authorized; customer datasets must not be used for benchmark writes.

Remote runtime for an isolated target replay

runtime: remote drives an already running isolated synthetic deployment instead of starting Docker. SYNTHETIC_BASE_URL and SYNTHETIC_DATABASE_URL are required and must point at a disposable app/database, never a customer dataset. A restart phase deletes the single ready pod matched by SYNTHETIC_RESTART_SELECTOR in the driver's own namespace (namespaced ServiceAccount) and waits for a different ready replacement. Resource sampling is external in this mode; every phase and the idle window retain UTC start/finish so external cgroup samples can be aligned. config/synthetic-pilot-target-remote.json is the phased profile with the deployed release's image digest and Gunicorn worker count (image CMD: 3). The 2026-10-21 replay on railiance01 and its manifests/evidence are retained in railiance-apps/docs/evidence/2026-10-21-vergabe-target-replay/.