Repository navigation
Carry over calibration-diagnostics#219: apply each event batch incrementally - #11
Merged
Merged
Conversation
…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
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>
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.
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, head3dfbe0558dfccb3edea5e59476a2e180a2fd7043.How it was carried over
tools/extract-from-calibration-diagnostics.shaccepts any branch or commit asEXTRACTION_SOURCE_REF, so it carried over #219's head directly, without cherry-picks:git-filter-repo rewrote #219's commits with their authors and dates:
6993387a2a0db768c55e8815473cdfe548af5a15bb5b38e6fe595c5573dfbe0558dfcae1dc45#219 also merged calibration-diagnostics
main(bbe4ea5b7881). That merge changes nothing undertelemetry-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.iniandDockerfilehere have the same git object IDs as their counterparts undertelemetry-service/at3dfbe05, so the image built from this branch runs exactly #219's code.The second commit updates
tools/extraction-source.env(source_commit3dfbe05…,extracted_commitae1dc45…) and adds the changelog fragment (afixedfragment, so a patch release).Merging
mainkeepsae1dc45, the commit the record names.COLLECTOR_DEPLOY_ENABLEDis set totrue; 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.main, and Accept artifact submissions into private storage and run worker jobs #12 adds migration0002_artifact_intake. microcosm-collector#9, already merged, adds onlytelemetry_collector/orrery/, which the service doesn't import.mainwould then refuse to run, becauseae1dc45would not be in the new history. After the switch, nothing should need carrying over, so this matters only iftelemetry-service/changes again before calibration-diagnostics#234 removes it.mainwhen this merges (this PR adds afixedfragment).mainruleset requires branches to be up to date.564e12bandea621b9mergedmainafter microcosm-collector#9 and its 0.3.0 release; any later merge tomainneeds another update.Tests
Red and green per commit:
832885bCarry over calibration-diagnostics#219tests/test_incremental_batch_apply.pyandtests/test_storage_postgres.pyon this repo's previous code: both modules fail at collection (ImportError: cannot import name 'MATERIALIZED_RUN_COLUMNS'), 0 tests runTEST_DATABASE_URLset; without it they skip7e557feRecord the carry-overtools/check-changelog.shexits 1 (no fragment)tools/check-changelog.shpasses (0.2.0 → 0.2.1);tests/test_extraction.pyandtests/test_release_versioning.py: 22 passedAfter 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 -non every script) passes. Locally I ran only these test files. CI runs the whole suite, the migration andalembic check, actionlint and the image build.Deployment
This PR changes no workflow, Dockerfile, deploy script or configuration. Its merge deploys only if
COLLECTOR_DEPLOY_ENABLEDistrueat 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