Repository navigation
Conversation
Every emitter service delivers every pending run in the host's shared spool, oldest update first. A run whose delivery failed with an unexpected error ended the pass and backed off delivery for every run, and a registration row that could not be decoded failed the listing itself, so one bad run stopped every later run on the host. Each run now keeps its own retry delay (1 s doubling to 60 s, cleared when a batch is delivered), and the pass moves on. The token exchange, which every run shares, has its own delay and charges no run. The listing reads only run keys; a run's registration is read, and can fail, only for that run. A 401 now charges the run, so a refused run cannot cost a request every tick. A batch the collector accepted but lock contention kept in the spool is removed before its run sends anything else, so it is never sent twice. Errors stay one line per type and scope, without a traceback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…' into fix/telemetry-per-run-delivery
…f load checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1168: the base branch is
fix/telemetry-spool-lock-startup. I'll retarget tomainonce #1168 merges. This is the #1168 follow-up "Isolate delivery failures per run".Problem
Every emitter service on a host delivers every pending run in the host-wide shared spool, oldest
updated_atfirst. On #1168,CollectorDelivery.flush_oncewraps the whole pass:updated_at, while live builds' runs move to the back with each event. So runs after it were never delivered, by any service.pending_runs()itself. That query decodes every row'sregistration_json, so no run at all was delivered.Persistent per-run failures I checked on this branch:
--staging-run-idis free text,TelemetryRundoes not validaterun_id, and urllib raiseshttp.client.InvalidURLfor an events URL whose run id contains a space.InvalidURLis anHTTPException, not anOSError, so on Keep the telemetry emitter service alive through spool lock contention #1168 every pass raised at that run.registration_jsonthat is not a JSON object makesJsonObjectTextraiseValueErroron read. Another checkout sharing the spool could write one.The task description also listed a collector response that makes
int(response["expires_in"])raise as a per-run state. It isn't one: the token exchange sends{}with the Hugging Face credential, and neither its request nor its response depends on the run. This PR treats it as shared (item 2 below).Change
Each run keeps its own retry delay (
RetryDelay: 1 s, doubling to 60 s, kept in memory per service).The token exchange has one shared delay. Its failures (transport error, a status other than 200/401/403, a 200 without a usable token, or an exception parsing it) defer further exchanges and charge no run. Runs that need a token skip the pass and are attempted again next pass. A 401/403 from the exchange still makes that run local-only, as before.
One run's stored registration can fail only that run.
EventSpool.pending_run_keys()lists(run_id, producer_id)from the key columns only: same filter and order aspending_runs(), with ties broken by key.EventSpool.registration(run_id, producer_id)reads one row. Delivery calls it only when the run needs registering, inside that run's error handling.pending_runs()now shares the query builder, usingEXISTSinstead ofJOIN … DISTINCT. A differential test checks that it lists the same runs.A 401 counts as a failure of that run. The token is still cleared. The next run that needs a token exchanges once, so a refused run costs one exchange per attempt, on its own doubling schedule.
An accepted batch is never sent twice. If the acknowledge after a 202 hits lock contention, the event ids stay in memory. On the run's next attempt they are removed from the spool before the run sends anything else.
Reporting.
flush_onceno longer raises for one run's error or the exchange's. Each unexpected error type prints one line per scope throughwrite_warning, without a traceback:ValueError(…) delivering one queued run; it will retry that run later and keep delivering the others."TypeError(…) exchanging the Hugging Face credential for a collector token; it will retry later."Lock contention still ends the pass and propagates, as on Keep the telemetry emitter service alive through spool lock contention #1168: the worker stays silent and retries on the next tick. So does an error listing the queue, which the worker reports once per type.
Request counts in 10 one-second ticks, from the #1168 review's probe (adapted to the new spool API; scripts and output in
~/reviews/microcosm-telemetry-per-run-delivery-2026-10-09/probe/):expires_in: nullPer-run retry state vs a single run quarantine
Single run quarantine: on an unexpected error, set the run aside, either in memory for the service's life or as
local_onlyin the spool.local_only, its telemetry is lost for every service.Per-run retry state:
Decision: per-run retry state. A run that fails for 7 days has its events pruned by retention and leaves the queue.
Evaluated and not done
test_a_spool_from_an_unknown_migration_is_refused_without_the_write_lock), so older checkouts sharing the host's spool would be locked out. Each failure would also become a write on the contended spool. Instead, the state is per service: S services on a host each retry a failing run, so at most S attempts a minute.Known limits
Invariants (each has a test in
test_telemetry_delivery_isolation.py)expires_in: null, no token, an exception, 403). Token lifetime: 60 s, 1 h, or none given.flush_onceraises only lock contention.ValueErrorline, retried on its own delay.pending_run_keys()lists exactly the runs the old join-and-distinct query did, in update order.pending_runs()equalsregistration()of each key.Mutation check
Each mutant is an exact edit to
collector.pyorspool.py, injected through a pytest plugin that swaps the module's code without touching tracked files. It ran against this file plustest_telemetry_emitter.py, which leaves out the subprocess test. All 26 were killed, each by a failing assertion, not a collection or fixture error. An identity mutant passed. Runner, plugin and logs:~/reviews/microcosm-telemetry-per-run-delivery-2026-10-09/mutation/.one_delay_for_all_runstest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(8 tests)failure_not_deferredtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(5 tests)delivery_does_not_resettest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)run_error_ends_passtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(3 tests)exchange_failure_not_deferredtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)exchange_error_charged_to_runtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)exchange_delay_ignoredtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)exchange_delay_not_resettest_a_working_exchange_resets_the_exchange_delay(1 test)events_401_keeps_tokentest_a_rejected_token_from_one_run_costs_one_exchange_per_attempt[events](1 test)events_403_retriedtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)exchange_rejection_retriedtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)report_every_timetest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)report_once_across_scopestest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)lock_error_charged_to_runtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)never_forget_runstest_delivery_forgets_runs_that_leave_the_queue(1 test)due_strictly_aftertest_a_persistently_failing_run_settles_to_one_request_a_minute(11 tests)double_before_waitingtest_retry_delay_doubles_to_its_cap_from_each_failure(8 tests)no_delay_captest_retry_delay_doubles_to_its_cap_from_each_failure(4 tests)registration_read_before_tokentest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)waiting_for_token_charges_runtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)register_401_keeps_tokentest_a_rejected_token_from_one_run_costs_one_exchange_per_attempt[registration](1 test)accepted_batch_sent_againtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(2 tests)removed_batch_not_counted_deliveredtest_a_failing_run_never_holds_up_another_and_costs_bounded_requests(1 test)listing_includes_runs_without_eventstest_a_corrupt_registration_fails_only_its_own_run(4 tests)listing_newest_firsttest_a_corrupt_registration_fails_only_its_own_run(3 tests)listing_ignores_statetest_delivery_forgets_runs_that_leave_the_queue(5 tests)Tests run (touched files only,
-p no:cacheprovider --basetemp=<scratch>)845f5e02f(this branch with Keep the telemetry emitter service alive through spool lock contention #1168's headd7fda0def):test_telemetry_delivery_isolation.py,test_telemetry_spool_contention.pyandtest_telemetry_emitter.py. All 80 passed (15 + 32 + 33), includingtest_subprocess_exchanges_ambient_token_and_delivers_events.5bacdba3a(one more test-only commit):test_telemetry_delivery_isolation.py, 15 passed.ruff check .passes.ruff format --checkpasses on the touched files.Follow-ups (not in this PR)
--staging-run-idnow fails only that run, but that run never delivers.axiom: n/a: telemetry infrastructure, no policy encoding
🤖 Generated with Claude Code