Skip to content

worker boot: the large worker's unit carries its CA, staged label and commit check; rehearsed end to end - #2988

Open
colinmegill wants to merge 2 commits into
edgefrom
cdk/worker-boot-env
Open

colinmegill wants to merge 2 commits into
edgefrom
cdk/worker-boot-env

Conversation

@colinmegill

Copy link
Copy Markdown
Member

Observable changes: only with -c enableLargeClass=true (off in production; the flag-off template is byte-identical). The worker boxes' user data changes: the env document /etc/app-info/polis-jobs.env gains POLIS_JOBS_CA_FILE=/etc/polis-jobs/rds-ca-bundle.pem, POLIS_JOBS_HOST_ALLOWLIST=<RDS instance endpoint> (or context queueHostAllowlist), POLIS_JOBS_REQUIRE_SOURCE_COMMIT, and for the math-large class MATH_ENV=python-large, MATH_CAPACITY_ROUTING=0, MATH_CAPACITY_PROMOTE=0, MATH_CAPACITY_RESTAGE=. The start script fetches AWS's RDS CA bundle (https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem) when the file is missing, mounts it read-only, and refuses to start the daemon (exit 1, systemd retries every 60 s) when the app .env has no MATH_POLLER_SOURCE_COMMIT on a class that needs one. No schema change. No Lambda. Not deployed by this PR.

Why (connections map §5, findings #671 and #722)

  • The daemon defaults to TLS and needs a readable CA file and a host allowlist; the unit mounted neither and the image has no RDS bundle, so on a worker box it exits 2 every minute.
  • The child inherits the shared app .env, whose MATH_ENV is the served label python; the child refuses that (it never writes a served label), and it also refuses any small-poller routing/promotion setting that is on. Nothing gave the worker its staged label: the composed chain proof set MATH_ENV=python-large itself, which hid the gap.
  • A box whose .env lacks the source commit would claim jobs only for every child to refuse them (version-skew guard), burning attempts.

What changes

  • cdk/workerClasses.ts: STAGED_LABEL = 'python-large' (a test pins it to DEFAULT_STAGED_LABEL in delphi/polismath/poller/capacity.py); the math-large row's childEnv and requiresSourceCommit; queueHostAllowlist context.
  • cdk/launchTemplates.ts: the env document, start script and unit are exported functions (workerEnvDocument, workerStartScript, workerUnit); the user data writes the env document from the first. The env document stays AFTER the app .env in docker run, so its keys win.
  • cdk/lib/cdk-stack.ts: passes the RDS endpoint as the allowlist.
  • cdk/scripts/emit-worker-files.ts: writes the three files exactly as the launch template installs them, for rehearsal.
  • delphi/tests/poller/chain_proof.py: no longer sets the child's label. Its daemon env carries the served label (as the app .env does) and --worker-env applies the generated env document.
  • delphi/tests/poller/worker_boot_proof.py (+ worker_boot/Dockerfile.box, worker_boot/aws-standin): the real boot path on a build box.

Proof on the work box

Worker boot proof: the final Delphi image built from edge + this PR + #2944 (so the vote-convention guard is in), the unit/start script/env document from emit-worker-files.ts, installed at their real paths in a stand-in box running systemd and started with systemctl start polis-jobs. The secret store is a stand-in: a local file served by a stand-in aws command, clearly marked, never AWS. The TLS CA is a stand-in local CA in place of the RDS bundle. 24 checks, all ok:

  1. the unit is active. The password was read by the secret's name. The daemon's session is TLSv1.3 as polis_jobs_proof, a plain login that is a member of polis_queue_executor. Inside the container MATH_ENV=python-large (the app .env says python) and MATH_CAPACITY_ROUTING=0 (the app .env says 1). The cgroup limit is 53248 MiB.
  2. the real small-poller code admitted a routed conversation and the child computed it: its log shows memory admission: limit_mb=53248 (cgroup), staged=python-large and the source commit, and it rebuilt under python-large. The bundle is complete under python-large, nothing was written under python, and the receipt binds the result.
  3. with no MATH_POLLER_SOURCE_COMMIT in .env, the start script refuses and no daemon starts. The proof found a real bug here: under set -o pipefail a missing line exited before the refusal message was printed. Fixed (|| true).
  4. a frame staged for python-other is refused by the child (stage_failed:2).
  5. a database declaring agree = +1 is refused by the child through the same unit ("this database declares vote convention version 1 (agree = +1) … built for agree = -1").

Proof on edge + this PR alone: steps 1–4 hold (22 checks) and step 5 is skipped because the tree has no 000025. Composed chain proof (edge + this PR, real daemon + child + promotion, with the generated env document and no label override): 41 checks ok, done: every step held. cdk jest 165 passed; cdk synth -c enableLargeClass=true ok; Delphi poller tests 865 passed. Every container, the box image and the VM paths were removed afterwards.

Not covered: the CA fetch from AWS's URL was not exercised (the proof places the stand-in CA first). Drain and shutdown (#718), and the Delphi class's own child env, are out of scope. The unit still runs the hook-built mutable image tag.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

… check; rehearsed end to end

The large worker's env document (/etc/app-info/polis-jobs.env, written by
its launch template and passed to docker run after the shared app .env, so
it wins) now carries:
- POLIS_JOBS_CA_FILE and POLIS_JOBS_HOST_ALLOWLIST (the RDS instance
  endpoint, or context queueHostAllowlist): the start script fetches AWS's
  RDS CA bundle when the file is missing and mounts it read-only (#671);
- the large child's MATH_ENV=python-large (STAGED_LABEL, pinned to the small
  poller's default staged label) with MATH_CAPACITY_ROUTING/PROMOTE off and
  RESTAGE empty, which the child otherwise refuses (#722);
- POLIS_JOBS_REQUIRE_SOURCE_COMMIT=1: the start script refuses to start the
  daemon without MATH_POLLER_SOURCE_COMMIT in the app .env, so no job is
  claimed only for its child to refuse it.

The env document, start script and unit are exported functions
(workerEnvDocument, workerStartScript, workerUnit); cdk/scripts/
emit-worker-files.ts writes them for local rehearsal. The chain proof no
longer sets the child's label: its base env carries the served label and
--worker-env applies the generated env document.

delphi/tests/poller/worker_boot_proof.py starts the final image through the
real unit under systemd in a stand-in box, with the login read by name from
a stand-in secret store, TLS against a stand-in CA, and checks the child's
success and its refusals (no commit, another staged label, and with 000025 a
vote convention it was not built for).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
postgres:17 declares a data volume; docker rm -f without -v left one behind
per run on the build box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
init.py 2 0 100%
benchmarks/bench_pca.py 128 107 16%
benchmarks/bench_repness.py 81 65 20%
benchmarks/bench_update_votes.py 38 28 26%
benchmarks/benchmark_utils.py 34 0 100%
components/init.py 1 0 100%
components/config.py 165 70 58%
conversation/init.py 2 0 100%
conversation/conversation.py 1291 264 80%
conversation/group_vote_counts.py 38 2 95%
conversation/manager.py 131 42 68%
database/init.py 1 0 100%
database/dynamodb.py 395 178 55%
database/postgres.py 414 95 77%
delphi_storage/init.py 1 0 100%
delphi_storage/codec.py 295 23 92%
delphi_storage/golden_corpus.py 60 9 85%
empty_output.py 29 2 93%
engine_adapter.py 317 265 16%
job_child/init.py 325 49 85%
job_child/census.py 101 13 87%
pca_kmeans_rep/init.py 5 0 100%
pca_kmeans_rep/clusters.py 257 38 85%
pca_kmeans_rep/corr.py 98 17 83%
pca_kmeans_rep/group_k_smoother.py 28 0 100%
pca_kmeans_rep/legacy_kmeans.py 197 8 96%
pca_kmeans_rep/pca.py 162 19 88%
pca_kmeans_rep/repness.py 226 5 98%
poller/init.py 4 0 100%
poller/admission.py 318 15 95%
poller/backfill.py 936 32 97%
poller/capacity.py 485 27 94%
poller/capacity_queue.py 248 13 95%
poller/coordinator_bridge.py 317 317 0%
poller/math_writer.py 113 3 97%
poller/promotion.py 112 4 96%
poller/readiness.py 368 13 96%
poller/rebuild_child.py 133 6 95%
poller/service.py 665 52 92%
poller/worker_pool.py 140 0 100%
queue/executor.py 223 111 50%
regression/init.py 4 0 100%
regression/clojure_comparer.py 188 20 89%
regression/comparer.py 891 640 28%
regression/datasets.py 135 27 80%
regression/recorder.py 36 27 25%
regression/utils.py 130 85 35%
replay/init.py 2 0 100%
replay/attribution_capture.py 49 0 100%
replay/axis_continuity.py 347 24 93%
replay/certify.py 999 59 94%
replay/crosslang.py 128 2 98%
replay/decision_ties.py 55 8 85%
replay/diagnostics.py 50 7 86%
replay/driver.py 73 1 99%
replay/dsn_admission.py 36 1 97%
replay/equiv_query.py 10 0 100%
replay/event_ingress.py 95 1 99%
replay/fixture_bundle.py 815 62 92%
replay/fixture_config.py 266 11 96%
replay/fixture_extract.py 285 12 96%
replay/fixture_generate.py 103 1 99%
replay/fixture_samples.py 157 14 91%
replay/fixture_selection.py 119 0 100%
replay/fixture_survey.py 147 11 93%
replay/legacy_pca.py 29 17 41%
replay/polarity.py 319 25 92%
replay/poller_equiv.py 776 99 87%
replay/prodclone.py 193 2 99%
replay/real_data.py 146 10 93%
replay/schedule.py 205 10 95%
replay/served_math.py 159 4 97%
replay/shard_bench.py 189 65 66%
replay/stagecompare.py 890 161 82%
replay/stages.py 256 40 84%
replay/stepcompare.py 86 1 99%
replay/store.py 120 11 91%
replay/tie_capture.py 107 19 82%
replay/types.py 71 6 92%
run_math_pipeline.py 284 93 67%
tools/engine_rebuild.py 230 11 95%
types.py 22 0 100%
umap_narrative/501_calculate_comment_extremity.py 113 38 66%
umap_narrative/502_calculate_priorities.py 135 135 0%
umap_narrative/700_datamapplot_for_layer.py 502 502 0%
umap_narrative/701_static_datamapplot_for_layer.py 310 310 0%
umap_narrative/702_consensus_divisive_datamapplot.py 434 275 37%
umap_narrative/801_narrative_report_batch.py 837 620 26%
umap_narrative/803_check_batch_status.py 267 148 45%
umap_narrative/llm_factory_constructor/init.py 2 2 0%
umap_narrative/llm_factory_constructor/model_provider.py 234 234 0%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 270 270 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 160 9 94%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 3 0 100%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 331 289 13%
umap_narrative/polismath_commentgraph/utils/storage.py 580 466 20%
umap_narrative/reset_conversation.py 159 50 69%
umap_narrative/run_pipeline.py 379 229 40%
umap_narrative/topic_naming.py 148 148 0%
utils/cli_flags.py 15 0 100%
utils/clj_hash.py 44 0 100%
utils/env_flags.py 13 0 100%
utils/general.py 63 39 38%
utils/output_profile.py 66 2 97%
utils/serialization.py 10 5 50%
utils/vote_convention.py 159 5 97%
Total 24273 8159 66%

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant