Skip to content

Build monitor: show collector runs' uploads, keep dry runs apart, share the collector run list - #268

Merged
anth-volk merged 15 commits into
mainfrom
claude/build-monitor-uploads-dry-runs-cache
Oct 27, 2026
Merged

anth-volk merged 15 commits into
mainfrom
claude/build-monitor-uploads-dry-runs-cache

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #267

What changes

1. Uploads come from the Hugging Face copy. When a collector record replaces a run's Hugging Face copy in the build monitor's list (mergeHostedDocuments), it now keeps the copy's upload record (withHuggingFaceUploads in build-monitor.ts): the version 2 and later delivery block, or the version 1 failed write check. Refreshing the selected run from the collector (refreshHostedRun) keeps the uploads the list read (keepListedUploads). Uploads are unknown only when neither source records them. Example: uk-frs-calibration-attempt-20261021T195620Z-a858c100 now reports uploaded, 39 of 39, to policyengine/populace-uk-staging.

2. Dry runs form a pipeline of their own. pipelineIdentity now gives run kind dry_run the key <country>:<pipeline>:dry_run and the label " (dry run)", as it already did for smoke. Dry runs get their own group in the run selector and no longer enter full runs' history, forecasts or stage statistics. The rule reads the run kind each run registers, so it applies to every country (the US release registers dry_run too).

  • Spine builds needed no change: they register pipeline id uk-frs-spine and run kind spine. Production lists them under GB:uk-frs-spine, apart from calibration runs; a test records this.
  • Not done (optional part): linking a spine build to the attempt that read its output. The match would be the spine's spine_h5_creation details.sha256 against the attempt's input_pinning details.dataset_sha256, but no spine run's events could be read to check that the values match (spine runs are not on Hugging Face, and the collector's events need the read credential).

3. A cached collector run list. Build progress and both Candidate routes now read the run list through collectorRunLists in build-runs.ts, one per country on each server instance:

  • A list that answered is reused for 15 seconds.
  • A request in flight is shared for 5 seconds after it was made. A later call makes its own request: once the Candidate tab has stopped waiting, nothing keeps the request running after the response, and Vercel can suspend the instance with it in flight, so sharing it could hand the later call its overdue timeout error.
  • A failed request is dropped, so the next call asks again.
  • Every collector request needs the collector's read credential from Secret Manager, and that read (createSecretReader in secret-manager.ts) follows the same rule: a read still in flight is shared for 5 seconds after it began, and a read that answered is reused for 60 seconds after it began. Before, a read in flight was shared for 60 seconds, so a new list request could still share the credential read an old one had left running, and receive its overdue timeout as "Unable to load the telemetry credential from Secret Manager."
  • The Candidate tab (loadCollectorTimelines) waits at most 5 seconds for the list. After that it uses the list's latest earlier answer if that answered less than 60 seconds ago (two of the tab's 30-second polls), and otherwise shows the Hugging Face states with the note "The telemetry collector did not answer within 5 seconds, …". The late answer serves the next poll: without the earlier answer, a list that took 5 to 15 seconds was already past its 15 seconds of reuse at the next poll, so a collector that stayed that slow never set the tab's states. The late answer arrives only while the server instance keeps running after the response. Vercel documents that only for work passed to after() (its waitUntil), which this PR does not use, so on an otherwise idle instance the next poll can still show the note.
  • Each run's own record is read as before: a running run's documents are fetched on every request.
  • Build progress still waits for the list, because without it its run list would drop every collector-only run.
  • The cache is per server instance, so on Vercel the Staging routes do not all share one list. Vercel groups Next.js routes into one function only when their settings, including maxDuration, match. staging/run sets 300 and the other three routes 120. On the preview deployment of e103f4f, builds, builds/run and staging/runs are one function and staging/run is another, which keeps its own cache. A Candidate poll therefore makes one list request per country in each of the two functions' instances, and only the Build progress list and the Candidate run list share one.

docs/build-monitor.md documents all three.

Tests (red, then green)

Commit Before the fix After
30d451a / 97687fd uploads 2 of 4 new tests fail (build-runs.test.ts: list and refreshed run report unknown) pass
3221e9d / c7c0bbb dry runs 2 of 3 new tests fail (build-monitor.test.ts: key GB:uk-frs-calibration; the dry run is in the history) pass
fe76db6 / bf8fded run list 3 of 4 new tests fail (build-runs.test.ts: 3 list requests instead of 1; a new request after 14 s; the Candidate tab waits for the slow list) pass
ab9b668 / e103f4f earlier list (review) 1 of 1 new test fails (build-runs.test.ts: the poll 30 seconds after a 6-second list shows the "did not answer" note) pass, with a second test that an answer a minute old no longer stands in
7a98b27 / 47ad374 request left running (second review) 1 of 2 new tests fails (build-runs.test.ts: the next poll shares the first poll's request and shows "could not be read (The operation was aborted due to timeout)") pass; the other new test checks that a call within the 5 seconds still shares the request
7efea28 / a61441d credential read left running (third review) 1 of 1 new test fails (secret-manager.test.ts: a read 5 seconds after an unanswered one shares it and fails with "Unable to load the telemetry credential from Secret Manager.") pass; the same test checks that a read within the 5 seconds is shared and an answered read is reused for 60 seconds

Local: bun test for the changed and related files (123 tests in 11 files) passes, and bun run lint (both tsc projects) passes. bun run build was left to CI. After the review commits, 128 tests in the 10 related files pass, and bun run lint passes. After the second review's commits, 130 tests in those files pass, and bun run lint passes. After the third review's commits, 131 tests in those files pass, and bun run lint passes.

Settings

Not changed. The four routes still read the collector through telemetry-collector-client.ts and secret-manager.ts. MICROCOSM_TELEMETRY_COLLECTOR_URL (the GitHub Production variable, passed by deploy.yml to vercel deploy --env, and set in the Vercel project), MICROCOSM_TELEMETRY_READ_SECRET_VERSION and MICROCOSM_TELEMETRY_GOOGLE_IDENTITY_PROVIDER are set in the Vercel project's Production and Preview settings. The Hugging Face reads these routes make (HF_TOKEN, from the GitHub repository secret HF_RELEASE_READ_TOKEN through vercel deploy --env; POPULACE_UK_STAGING_HF_TOKEN, set in the Vercel project) are unchanged too.

Overlaps

🤖 Generated with Claude Code

anth-volk and others added 7 commits October 26, 2026 18:41
The collector records delivery mode "collector" with no counts for every
run, and its record replaces the run's Hugging Face copy, so the build
monitor reports uploads as "unknown" even for a run that uploaded all its
files (UK attempt a858c100, 39 of 39). These tests expect the list and the
refreshed selected run to report the copy's uploads, and "unknown" only
when neither source records them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a collector record replaces a run's Hugging Face copy in the build
monitor's list, it now keeps the copy's upload record: the version 1
failed write check or the version 2 and later delivery block. Refreshing
the selected run from the collector keeps the uploads the list read.
Uploads are "unknown" only when neither source records them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A calibration dry run registers run kind "dry_run", but the build
monitor keys runs by pipeline id and gives only smoke runs a key of their
own. Its two stages are the start of a full run's, so it counts as a
comparable passed run in the full runs' history and forecast. These tests
expect a key and label of its own. A spine build already has its own
pipeline id ("uk-frs-spine"); a test records that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs with run kind "dry_run" now get the key "<country>:<pipeline>:dry_run"
and the label "<pipeline> (dry run)", as smoke runs already did. They form
their own group in the run selector and no longer enter full runs' history,
forecasts or stage statistics. The rule reads the registered run kind, so
it applies to every country's pipelines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build progress and both Candidate routes each request the collector's run
list, every 30-second poll, and the Candidate routes wait up to the
30-second request timeout when the collector does not answer. These tests
expect the routes to share one list for 15 seconds, a failed request not
to be reused, and the Candidate tab to wait at most 5 seconds for the list
before it shows the Hugging Face states, with the late answer serving the
next poll. A running run's own record is still read for every request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build progress and both Candidate routes now read the collector's run
list through one cache per country on each server instance. A list that
answered is reused for 15 seconds, a request in flight is shared, and a
failed request is dropped so the next caller asks again. The Candidate
tab waits at most 5 seconds for the list; past that it shows the Hugging
Face states with a note, and the request's later answer serves the next
poll. Each run's own record is still read as before, so a running run's
documents are as fresh as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/build-monitor.md now says where a collector run's uploads come from,
that smoke and dry runs form pipelines of their own, and how the Staging
routes share and wait for the collector's run list.

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

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
calibration-diagnostics Ready Ready Preview Oct 27, 2026 11:15am UTC

Request Review

anth-volk and others added 2 commits October 26, 2026 22:06
The Candidate tab polls every 30 seconds and reuses a collector run list
for 15 seconds after it answered. A list that answers between 5 and 15
seconds after its request is therefore expired at the next poll, whose
own request is again slower than the 5-second wait, so the tab never
shows the collector's states. This test expects the late answer to serve
the next poll while the collector stays slow.

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

When the collector's run list does not answer within the Candidate tab's
5-second wait, the tab now uses the list's latest earlier answer if that
answered less than 60 seconds ago (two of the tab's 30-second polls), and
shows the Hugging Face states with a note only when there is none. A
collector that keeps answering after the wait therefore still sets the
tab's states from the previous poll's list, as the late answer was meant
to. A second test checks that an answer a minute old no longer stands in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits October 26, 2026 22:31
When the Candidate tab's 5-second wait runs out, the collector run list
request carries on with no caller waiting for it. Vercel can suspend the
instance with the request in flight; the next poll then shares it, and
receives its overdue 30-second timeout error instead of the collector's
states. This test expects the next poll to make its own request.

A second test checks that a call made while the request is still within
the wait does share it. The two tests of a late answer now keep the first
request's promise to wait for its answer, instead of asking the cache
again once the wait is over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A collector run list request in flight is now shared only for 5 seconds
after it was made, the Candidate tab's wait. A request can be left with
no caller waiting only after that time, when the Candidate tab stops
waiting and nothing keeps the request running, so Vercel can suspend the
instance with it in flight. A later call now makes its own request
instead of sharing that one and receiving its overdue timeout error.

A request that a newer one replaced still records its answer for the
Candidate tab's 60-second fallback, unless a newer request has already
answered or the cache was cleared after it was made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vercel groups Next.js routes into one function only when their settings
match, including `maxDuration`. `staging/run` sets 300 and `builds`,
`builds/run` and `staging/runs` set 120, and the preview deployment of
e103f4f serves `staging/run` from a different function than the other
three. The comment and the build monitor documentation said one list
serves every Staging route; they now say `staging/run` has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 2 commits October 27, 2026 15:10
The Candidate tab stops waiting for the collector's run list after 5
seconds. When the Secret Manager read for the collector credential is
what is slow, that read is left in flight with nothing keeping it
running, so Vercel can suspend the instance. The reader shares a read in
flight for up to 60 seconds, so the next poll's new list request shares
it and receives its overdue timeout as "Unable to load the telemetry
credential from Secret Manager." This test expects a call made 5 seconds
or more after a read began to make its own read, while a read that
answered is still reused for 60 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Secret Manager reader shared a read still in flight for up to 60
seconds. The Candidate tab's 5-second wait for the collector's run list
can leave that read in flight with nothing keeping it running, so a
later list request, which no longer shares the old list request, still
shared the old credential read and could receive its overdue timeout.

A read in flight is now shared only for 5 seconds after it began, and a
later caller makes its own read. A read that answered is still reused
for 60 seconds after it began, and a failed read is still dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sentence added in a61441d said a list request starts the Secret
Manager read. Each run's record read needs the credential too, and
shares or starts the same read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk marked this pull request as ready for review October 27, 2026 11:32
@anth-volk
anth-volk merged commit 92b6979 into main Oct 27, 2026
5 checks passed
@anth-volk
anth-volk deleted the claude/build-monitor-uploads-dry-runs-cache branch October 27, 2026 11:32

This branch was successfully deployed

1 active deployment
Preview — 734db746 Deployed Oct 27, 2026 by vercel[bot]
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.

Build monitor: show collector runs' uploads, keep dry runs apart, share the collector run list

1 participant