Skip to content

math poller: queue admission caps, a stable request key, routing refused without a queue that answers, the login named by its secret (P-084) - #2984

Open
colinmegill wants to merge 9 commits into
edgefrom
queue/admission
Open

colinmegill wants to merge 9 commits into
edgefrom
queue/admission

Conversation

@colinmegill

Copy link
Copy Markdown
Member

Observable changes, all only with MATH_CAPACITY_ROUTING=1 (default 0; nothing changes with routing off):

  1. Routing is refused without a queue that answers. A missing MATH_CAPACITY_QUEUE_DSN, a missing source commit, or a queue whose depth read fails (at start, before the first tick, and on any readiness tick) refuses routing: the small poller computes those conversations itself, exactly as with routing off, and the capacity line says queue_unreachable=1 until a depth read succeeds. Before this, routing went on and routed conversations were silently not enqueued.
  2. Two new keys on the capacity line (math_poller.capacity/1, and the readiness line's capacity object): queue_full and queue_unreachable, 0 or 1. The server's line parser accepts them. The CDK filters of cdk: one worker group per queue class; demand and alarms read from the queue; manifest remnants removed #2974 already read both.
  3. Caps. No new job while 20 jobs of class large are queued or waiting to retry in the env (queue_full=1; the conversation stays routed, no compute, no park); at most 2 new jobs per conversation per day (kept on the record and in the state file).
  4. One binding row per intent. The request key is the digest of the intent (env, scope, labels, input watermark, and the job the poller already holds) and the actor is math-poller:<env>, so asking again for the same unresolved demand reuses one polis_queue_requests row instead of adding one per ask (8,640 a day for six unchanged conversations, per the operating-surface map). A finished job in an answer is followed by one re-ask keyed after that job, so the scope guard decides as before (a fresh job once released, the same one while it holds).
  5. The queue login is named by its secret. MATH_CAPACITY_QUEUE_LOGIN_SECRET names the Secrets Manager secret holding the login's password (JSON with password, the shape the jobs daemon reads); a DSN that carries a password is refused. Compose forwards the name.

No schema change. Nothing is enabled.

What it is

Cost-reduction plan P-084 (admission), PRs 1 and 3 of its list, and the secret-name rule. Code: delphi/polismath/poller/capacity_queue.py (request_key, actor_scope, QUEUE_CAP_LARGE, SCOPE_DAILY_CAP, dsn_with_secret_password, the bounded follow-the-finished-job loop), capacity.py (queue_full, queue_unreachable, the record's admitted_ms, CapacityRouter.routing = configured and not refused), service.py (the start check and the per-tick reachability), server/src/ops/readinessLine.ts, docker-compose.yml, docs.

Where the plan did not survive contact with the code

  • The request key needs a basis. P-084's key is sha256(env, scope, staged_label, target_label, watermark). On the merged SQL a binding wins over the guard for 24 h: with that key alone, a cancelled or dead job's binding would answer every later ask of the same watermark with the finished job, and the cancel and dead paths of the composed chain could never admit a fresh job. The key also carries the job the poller already holds for the conversation (empty at first), and a re-ask after a finished job is keyed after:<job>. Rows stay bounded per job (two or three), not per ask.
  • The per-day cap meets the composed chain proof. The proof admits more than two jobs for its one conversation in a day (cancel, restart, kill, dead x3); it lifts the module constant in-process, as do two walks in the Postgres tests. Production's value is pinned in test_capacity_queue.py.
  • Routing refused at start means "until the queue has answered once", checked in the first start hook, not at construction (the client never connects when it is built).
  • The secret is read through boto3 in the poller. The small poller's container receives no AWS settings, so the region comes from boto3's own resolution or the instance metadata. The small box's role needs secretsmanager:GetSecretValue on the login secret (CDK grants it today only to the worker class roles), and the container needs metadata access. Recorded for the provisioning step; Colin provisions the login.

Gates (mm2)

  • queue-rs (unchanged here; from queue scheduling: one closed table of lanes and budgets in the poller; the daemon's idle poll is 60 s while LISTEN is up (P-082) #2982): fmt, clippy, unit, jobs_integration: pass.
  • Server: npm ci, contract:check, lint: pass. Full jest the CI way: 1707 passed, 1 failed, conversation-stats.test.ts "should accept until parameter", which fails the same way on this box for every branch tried (edge's hosted run is green; recorded as a box finding).
  • Delphi suite the CI way (python-ci stack): 3855 passed, 0 failed. New: TestAdmission (one key per intent, the cap asks nothing and the line says so, two a day survive the state file, an unreachable queue refuses routing until it answers, routing waits for the first answer, a missing commit stays refused, the login named by its secret only) and the Postgres test_one_binding_per_intent_however_often_it_is_asked (five asks, one row; five more with the known job, two; three newer-watermark asks, no row; actor math-poller:<env>).
  • Vote convention test_gate and sign_lint: pass.
  • Composed chain proof (real chain, real daemon, real child): every step held, all nine phases (cancel and dead re-admission included).

Stacked by merge on #2982 (scheduling constants); its commit shows here until it lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

colinmegill and others added 3 commits October 6, 2026 15:39
…; the daemon's idle poll is 60 s while LISTEN is up

The poller's math_rebuild admission takes its lane, attempt budget and class
from one read-only table (math_rebuild -> lane 1, 3 attempts, class large,
cost-reduction plan P-082); no caller can choose another lane or budget, and
a test pins the table and what reaches pd_enqueue. The values are the ones
the producer already sent.

polis-jobs: POLIS_JOBS_IDLE_POLL_SECONDS (default 60, 1..3600) is the fallback
claim poll while the LISTEN connection is up; without it the worker polls
every POLIS_JOBS_POLL_SECONDS as before. The test harnesses set it to 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…sed without a queue that answers, the queue login named by its secret

Cost-reduction plan P-084, day one, all behind MATH_CAPACITY_ROUTING (off):

- The request key is the digest of the intent (env, scope, labels, input
  watermark, and the job the poller already knows), with one actor per env
  (math-poller:<env>): asking again for the same unresolved demand reuses
  one binding row instead of adding one per ask. A finished job in an
  answer is followed by a re-ask keyed after that job, so the scope guard
  decides (a fresh job once released, the same one while it holds).
- No new job while 20 jobs of class large are queued or waiting
  (queue_full=1 on the capacity line; the conversation stays routed); at
  most 2 new jobs per conversation per day (kept on the record and in the
  state file).
- With routing on, a missing queue DSN, a missing source commit, or a
  queue that does not answer (at start, before the first tick, and on any
  readiness tick) refuses routing: the small poller computes those
  conversations itself, as with routing off, and the line says
  queue_unreachable=1 until a depth read succeeds.
- MATH_CAPACITY_QUEUE_LOGIN_SECRET names the Secrets Manager secret that
  holds the queue login's password; a DSN that carries a password is
  refused. Forwarded by compose; the server's line parser learns the two
  new keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…eleased the guard; the poison walks lift the per-day cap

A binding made while the guard still held a finished job (exit unproven)
answered the ask that followed the release with the same finished job, so
no fresh job was admitted. The re-ask's key now says whether this ask
released the guard (after-release:<job> or after:<job>). The Postgres
walks that admit more than two jobs for one conversation in a day lift the
module constant, as the unit walk does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill and others added 6 commits October 6, 2026 17:15
# Conflicts:
#	delphi/polismath/poller/capacity.py
#	delphi/tests/poller/fixtures/ops-poller-lines.txt
…e_full and queue_unreachable)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
The small poller's capacity line (and the readiness line's capacity object)
now carries "rev" (1 here). A line without it predates revisioning and is
revision 1. Python decode_counts and the server's TypeScript decodeCounts
read revisions 1..own+8: an older line carries exactly its own keys (newer
ones decode as null); a newer line must still carry every key the decoder
knows, and the keys its revision declared are checked as counts and dropped,
never relayed. Within a decoder's own revisions the shape stays closed.

The Python depth decoder reads both 000024's polis-queue/3 reply and
000026's polis-queue/4 (with oldest_eligible_at), so a poller keeps its depth
when 000026 is applied under it.

delphi/tests/poller/fixtures/capacity-revisions.txt holds a line per revision
(pre-revision, 1, 2, 3 and a forward 4); the Python test pins this
revision's line to the real emitter and both decoders read every line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Re-sync by merge with ops/capacity-line-revisions (which is edge plus the
revision field). This branch's emitter writes rev 2: revision 2 added
queue_full and queue_unreachable (CAPACITY_REV_KEYS in capacity.py and
readinessLine.ts). Its decoders read revisions 1 (with or without rev) and
3, and the fixture's revision-2 line is pinned to this emitter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…lve revision 2

Production (stable) logs nine counts; edge added large_leased,
large_poisoned and large_parked. Both shapes were logged without a rev, so
both are kept readable: an unrevisioned line is revision 1 or 2, told apart
by its keys. This emitter writes rev 2 explicitly. The fixture now holds
both unrevisioned shapes, revisions 2..4 and a forward 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Production's nine-count line is revision 1 and edge's twelve revision 2
(both unrevisioned, told apart by their keys); queue_full and
queue_unreachable are revision 3, which this emitter writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill added a commit that referenced this pull request Oct 7, 2026
…sion 4

Revision 4 added large_dead, oldest_queued_age_ms, queue_bytes and
sweep_age_ms (CAPACITY_REV_KEYS in capacity.py and readinessLine.ts). The
depth decoder keeps this branch's /3 + /4 reader, which edge now has too, and
its usage reader.

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

Copy link
Copy Markdown
Member Author

Correction merged in (9f7a2e0, 1edea3c; merge, no rebase): the capacity line carries rev; this branch writes revision 3 (queue_full, queue_unreachable added). Its Python and TS decoders read production's nine-key line, edge's twelve, and revisions 2..4 (forward up to 11). Brings in #2987 (edge + the revision field + the /3 and /4 depth reader). Work box: Delphi poller tests 975 passed; server unit 944 passed; lint/contract clean. Cross-revision matrix in #2987's body.

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 570 33 94%
poller/capacity_queue.py 310 25 92%
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 686 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 24441 8177 67%

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