Skip to content

Carry over calibration-diagnostics#219: apply each event batch incrementally - #11

Merged
anth-volk merged 9 commits into
mainfrom
claude/carry-over-incremental-batch-apply
Oct 20, 2026
Merged

anth-volk merged 9 commits into
mainfrom
claude/carry-over-incremental-batch-apply

Conversation

@anth-volk

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

Copy link
Copy Markdown
Contributor

Fixes #10

Summary

Carries over PolicyEngine/calibration-diagnostics#219, which is still open there: the Postgres store applies each stored event batch onto the run's stored columns instead of re-reading every event of the run and re-materializing it for every batch (calibration-diagnostics#217, O(n²) over a run). Run state, database contents and HTTP responses are unchanged, and there is no schema change or migration.

Source: calibration-diagnostics#219, branch collector-incremental-batch-apply, head 3dfbe0558dfccb3edea5e59476a2e180a2fd7043.

How it was carried over

tools/extract-from-calibration-diagnostics.sh accepts any branch or commit as EXTRACTION_SOURCE_REF, so it carried over #219's head directly, without cherry-picks:

EXTRACTION_SOURCE_REF=3dfbe0558dfccb3edea5e59476a2e180a2fd7043 \
  tools/extract-from-calibration-diagnostics.sh ../collector-extraction tools/extraction-source.env
git fetch ../collector-extraction main
git merge --no-ff FETCH_HEAD

git-filter-repo rewrote #219's commits with their authors and dates:

calibration-diagnostics commit Here Change
6993387a2a0d b768c55 Add an incremental event-batch reducer equivalent to full materialization
e8815473cdfe 548af5a Apply each stored event batch incrementally in the Postgres store
15bb5b38e6fe 595c557 Test incremental batch application with non-UTC timestamps
3dfbe0558dfc ae1dc45 Document that the producer read must follow the run row lock

#219 also merged calibration-diagnostics main (bbe4ea5b7881). That merge changes nothing under telemetry-service/, so the filter drops it. The merge here was conflict-free: none of #219's four files changed in this repo since the extraction.

After the merge, telemetry_collector/, migrations/, alembic.ini and Dockerfile here have the same git object IDs as their counterparts under telemetry-service/ at 3dfbe05, so the image built from this branch runs exactly #219's code.

The second commit updates tools/extraction-source.env (source_commit 3dfbe05…, extracted_commit ae1dc45…) and adds the changelog fragment (a fixed fragment, so a patch release).

Merging

  • Merge with a merge commit (the only method this repo allows), so that main keeps ae1dc45, the commit the record names.
  • When to merge: this PR is meant to be the first deployment from this repo. Follow the switch-over runbook in calibration-diagnostics#233. In short: calibration-diagnostics#219 merges there first and deploys from there; the switch PR stops calibration-diagnostics from deploying; COLLECTOR_DEPLOY_ENABLED is set to true; then this PR merges. If it merges while the variable is still unset, the release is tagged but not deployed, and the next merge deploys it.
  • Merge before microcosm-collector#12, and before any other PR that adds an Alembic migration, a dependency or a runtime setting. The first deployment from this repo deploys everything on main, and Accept artifact submissions into private storage and run worker jobs #12 adds migration 0002_artifact_intake. microcosm-collector#9, already merged, adds only telemetry_collector/orrery/, which the service doesn't import.
  • If #219 gains more commits before it merges, re-run the extraction from its new head on this branch and merge again. The recorded commit is already in this branch's history, so the tool only adds the new commits.
  • #219 should be merged with "Create a merge commit", or closed unmerged if the team prefers to ship it only from here. A squash or rebase merge there would give its changes new commits, and a later extraction from calibration-diagnostics main would then refuse to run, because ae1dc45 would not be in the new history. After the switch, nothing should need carrying over, so this matters only if telemetry-service/ changes again before calibration-diagnostics#234 removes it.
  • microcosm-collector#4 is independent: it touches no file this PR touches. The release version depends on the fragments pending on main when this merges (this PR adds a fixed fragment).
  • Update the branch before merging. The main ruleset requires branches to be up to date. 564e12b and ea621b9 merged main after microcosm-collector#9 and its 0.3.0 release; any later merge to main needs another update.

Tests

Red and green per commit:

Commit Red (before the change) Green (after)
832885b Carry over calibration-diagnostics#219 #219's tests/test_incremental_batch_apply.py and tests/test_storage_postgres.py on this repo's previous code: both modules fail at collection (ImportError: cannot import name 'MATERIALIZED_RUN_COLUMNS'), 0 tests run 16 passed (8 + 8). The 8 PostgreSQL tests ran against a local PostgreSQL 16 container with TEST_DATABASE_URL set; without it they skip
7e557fe Record the carry-over tools/check-changelog.sh exits 1 (no fragment) tools/check-changelog.sh passes (0.2.0 → 0.2.1); tests/test_extraction.py and tests/test_release_versioning.py: 22 passed

After committing, the same files passed again (16 and 22). After each update from main (564e12b, ea621b9, which brought in microcosm-collector#9 and its release; no change of their own), the four files passed again together (38) and the changelog check passes (now 0.3.0 → 0.3.1). Throughout, tools/check-quality.sh (Ruff lint and format, bash -n on every script) passes. Locally I ran only these test files. CI runs the whole suite, the migration and alembic check, actionlint and the image build.

Deployment

This PR changes no workflow, Dockerfile, deploy script or configuration. Its merge deploys only if COLLECTOR_DEPLOY_ENABLED is true at that time. The deployment's variables, secrets and Google Cloud access were checked end to end for the switch; the table is in calibration-diagnostics#233.

🤖 Generated with Claude Code

anth-volk and others added 8 commits October 15, 2026 23:52
…tion

The collector re-materializes a run from every stored event whenever a
batch arrives, so ingestion cost grows with the run's history. This adds
the reducer that lets the store fold a batch onto the run's stored columns
instead.

The fold is exact because of how the materializer's columns behave.
updated_at is a maximum over every event, and started_at is a minimum over
the final producer's events. Every other column is written by the latest
event that sets it, from that event alone. So new events that follow the
active producer's stored events fold directly onto the stored columns.

Two cases need targeted reads:

- an event delivered after later events of its producer replays only that
  producer's stored events from it onward;
- an earlier producer's first event reads the active producer's earliest
  timestamp, once per run.

materialize_run now shares the fold loop, so both paths apply events and
producer resets identically. The store is unchanged in this commit.

Tests replay scripted and seeded delivery schedules covering every event
kind, out-of-order and duplicate deliveries, and restarted producers.
After each batch they compare the folded columns with full
materialization, and they count the stored events read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
append_events re-read every stored event of the run, joined to its
producer, and re-materialized the run whenever a batch added an event.
Each batch therefore cost time proportional to the run's history, which
is O(n^2) over a run. It also held the run's row lock for longer as the
build went on.

The store now reads each producer's highest stored sequence with one
query: an index-only max per producer. It inserts the batch with
RETURNING, so the reducer sees the same database values (time zone,
JSONB) that a full re-read would. It then folds the inserted events onto
the stored columns with apply_event_batch. A producer delivers in
sequence order, so a normal batch reads no stored events. Two cases read
more:

- a late event reads only its producer's events from it onward;
- an earlier producer's first event reads one minimum timestamp, at most
  once per run.

The stored columns, the database contents and the HTTP responses are
unchanged, and there is no schema change. get_run still materializes
from every event, because it returns them all.

Tests count the event rows each batch reads: constant per in-order batch,
and only those after a late event, whatever the run's length. They also
check, after every batch of scripted and seeded schedules, that the
stored row equals the full materialization over the stored events, and
that the run listing matches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every earlier test sent UTC timestamps, so nothing checked that the
store folds the rows its insert returns rather than the submitted
values. They differ only in offset, and work.updated_at is stored as
text, so a fold over the submitted values keeps the producer's offset
where a full re-read of the stored events gives the database's.

The new Postgres test sends each schedule twice, in UTC and in
alternating +14:00 and -12:00 offsets: in-order batches of every event
kind, a late event that replays the events stored after it, and an
earlier attempt that first reports after a later one. After every batch
the stored row must equal a full re-materialization, and each non-UTC
run must equal its UTC run.

It passes against this branch, which already folds the returned rows.
Against a store that folds the submitted timestamps, it fails at the
second batch on the work column, while the seven existing Postgres
tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apply_event_batch is exact only if each producer's highest stored
sequence, the run's stored columns and the targeted event reads all
describe every event stored before the batch. append_events gets that
from the run's FOR UPDATE lock, which producer registration also takes.
Say so at the read and in apply_event_batch's docstring: read before the
lock, a concurrent batch could commit in between, and this batch's
earlier sequences would fold on top of its events instead of replaying
as late events.

Comments only; no behaviour change.

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

Merge the telemetry-service/ commits of PolicyEngine/calibration-diagnostics#219
(branch collector-incremental-batch-apply, head 3dfbe0558dfccb3edea5e59476a2e180a2fd7043),
extracted with tools/extract-from-calibration-diagnostics.sh. #219 is not merged
in calibration-diagnostics yet, so EXTRACTION_SOURCE_REF named its head commit.

Source commit -> extracted commit:
- 6993387a2a0d Add an incremental event-batch reducer equivalent to full materialization -> b768c55
- e8815473cdfe Apply each stored event batch incrementally in the Postgres store -> 548af5a
- 15bb5b3      Test incremental batch application with non-UTC timestamps -> 595c557
- 3dfbe0558dfc Document that the producer read must follow the run row lock -> ae1dc45

The branch also merged calibration-diagnostics main (bbe4ea5); that merge changes
nothing under telemetry-service/, so git-filter-repo drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/extraction-source.env now names #219's head (3dfbe05) as the source
commit and ae1dc45 as the extracted commit, so the next carry-over adds only
telemetry-service/ commits after it. Add the changelog fragment for the
release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk merged commit f59d94c into main Oct 20, 2026
4 checks passed
@anth-volk
anth-volk deleted the claude/carry-over-incremental-batch-apply branch October 20, 2026 23:12
anth-volk pushed a commit that referenced this pull request Oct 27, 2026
Brings in #6 (the submission contract, released as 0.4.0), #4, #11 and #23.
No conflicts: README.md, pyproject.toml and uv.lock merged automatically, and
the consumed fragment changelog.d/artifact-submission-contract.added.md is
removed, leaving only this branch's artifact-intake.added.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Carry over calibration-diagnostics#219 before deploying the collector from this repo

1 participant