Repository navigation
fix(reporting): key status checkpoints by resolved offering - #1775
Conversation
A reporting subscription keeps one configuration generation per resolved
offering of a version, but status checkpoints were keyed per subscription
version: (account, consumer, delivery_config_id, version, scope kind,
obligation). Two offerings of one version for one account and owner shared a
configuration checkpoint. The memory store silently merged the second into
the first; the PostgreSQL store refused owed sets whenever status
notifications were on, and would otherwise have dropped the second scope on
ON CONFLICT DO NOTHING. The production stores refused a second offering with
reporting_production_offering_conflict for the same reason.
ReportingStatusScope.checkpoint_key now ends with the generation's
offering_id ('' for an account-level generation). checkpoint_digest feeds
the status cause_id and the projection history keys; it digests the six
original columns when the offering is '', so every id and digest that
deployments already store is unchanged. The memory store re-keys an old
six-column state image.
PostgreSQL: reporting_status_checkpoint_offerings.sql widens the checkpoint
primary key to include offering_id, adds offering_id to the status and
projection status event tables (filled from their checkpoint), re-points
their checkpoint references and per-generation unique keys at the widened
key under their existing names, and defines the checkpoint and event guards,
which now compare the offering. Migration 0006 includes it; it is a
stopped-worker upgrade, and a no-op without status notifications. The status
store, projection and production create_schema chains run it too, and the
status and projection manifests are regenerated. The refusals that existed
only because of the shared checkpoint are lifted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean review, no blocking or medium findings.
The reviewer verified the load-bearing concerns on this fix(reporting): change that keys status checkpoints by resolved offering:
checkpoint_keygrows to a 7-tuple;checkpoint_digestdigests six columns whenoffering_id == ''so account-levelcause_id/history keys stay byte-identical (pinned bytest_account_level_checkpoint_identity_is_unchanged).- The
_lock_scopes_onINSERT (status_pg.py:296-307) stays aligned (7 plain %s → key, 8th%s::jsonb→ scope);_enqueue_ongained exactly one %s. - Backfill ordering in reporting_status_checkpoint_offerings.sql is safe (lines 299-311 set offering_id under ACCESS EXCLUSIVE with USER triggers disabled, FK re-added only after at line 336); migration fail-closes on a foreign FK rather than dropping it (259-265).
- Cross-release wire surface unchanged (offering_id on the wire since #1729), so no DR-0032/0033 pathway owed. Manifests regenerated; all five create_schema chains run the new file in the correct slot; lock orderings end in offering_id consistently.
Gate checks: no critical/high findings (row 1 N/A); gated_paths false (row 2 N/A); high_risk false (rows 3/5 N/A); no medium findings (rows 4/8 N/A); no prior decision (row 6 N/A); no no-auto-approve team match (row 7 N/A). Falls through to row 9 → approve.
The producer row-upload test stamps its worker lease with the settling harness clock, fixed at 2026-11-01T02:10Z, while the row store checks the lease against the database's clock_timestamp. Once the wall clock passed that instant plus the lease, every run failed with STATE_UNAVAILABLE: no live worker lease covers this row upload, on next and on every pull request. The test now runs its turn at the wall clock, so the lease lies in the real future. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — test-only fix, no blocking or medium findings.
This PR's reviewable delta is a single test-only change in tests/conformance/reporting/test_reporting_row_storage_object_pg.py: it sets clock[0] = datetime.now(timezone.utc) before run_worker() so the Postgres worker lease (compared against the DB's clock_timestamp()) lands in the real future rather than the harness's fixed instant. The inline reasoning is correct and no production path is touched.
Gate checks:
- No critical/high/medium findings (rows 1, 4, 5, 8 don't fire).
gated_paths: falseandreview_decision: APPROVED(row 2 doesn't fire).high_risk: false, no deletions/modifications triggers (rows 3, 5 don't fire).- Prior decision was
approve, so sticky escalation (row 6) doesn't apply. - No no-auto-approve team match (row 7 doesn't fire).
Falls through to row 9 → approve.
What was wrong
Since #1729 a reporting subscription keeps one configuration generation per resolved offering of a version (
ReportingConfigurationGenerationKeyincludesoffering_id), but reporting status checkpoints were still keyed per subscription version:ReportingStatusScope.checkpoint_keywas(account_id, consumer, delivery_config_id, delivery_config_version, scope_kind, obligation_id). Two offerings of one subscription version, for one account and owner, mapped to the same configuration-scope checkpoint:InMemoryStatusNotificationStore): the second offering's scope silently replaced the first, so one offering had no checkpoint or status events;PgStatusNotificationStore): the ledger refused owed sets whenever status notifications were on (UNSUPPORTED_FEATURE), and with that guard removed,ON CONFLICT DO NOTHINGon the six-column key dropped the second offering's checkpoint;reporting_production_offering_conflict, for the same reason.Reproduction:
test_each_resolved_offering_of_a_subscription_version_has_its_own_checkpointintest_reporting_status_projection_contract.pyruns the same case on both stores. Onnextthe memory case fails with three checkpoints instead of four (one configuration scope lost), and the PostgreSQL case fails with theUNSUPPORTED_FEATURErefusal. With the guard lifted but the key unchanged, it also fails with a missing configuration checkpoint.The fix
ReportingStatusScope.checkpoint_keyends with the generation'soffering_id(''for an account-level generation). Both stores key checkpoints, leases and status events by it.checkpoint_digestfeeds the statuscause_id(rpsc_...) and the projection historyscope_keys. Foroffering_id == ''it digests the original six columns, so every id and digest existing deployments already store is byte-identical. A unit test pins this._status_covers_owed_setsguard inPgReportingLedgerStoreand thereporting_production_offering_conflictchecks in both production stores are removed. They existed only because of the shared checkpoint. The production test now asserts that a second offering of one version is served.PostgreSQL and the migration
New idempotent DDL file
ledger/reporting_status_checkpoint_offerings.sql:reporting_status_scope_checkpoints' primary key to(account_id, consumer_namespace, delivery_config_id, version, scope_kind, obligation_namespace, offering_id). The table already hadoffering_id, but it was not part of the key;offering_idtoreporting_status_notification_events, and toreporting_projection_notification_eventswhere the projection is installed. The column is filled from each event's checkpoint (the immutable-event trigger is disabled only for that one-time backfill). Each table's checkpoint foreign key and per-checkpoint-generation unique key are re-pointed at the widened key under their existing PostgreSQL names. The other tables' auto-generated constraint names, and so the manifests' keys, therefore don't shift;CHECKs that tie each table'soffering_idto its scope document, as the other key columns already are;reporting_status_scope_guard,reporting_status_event_guard,reporting_projection_event_guard), which now compare the offering. Each function body lives only in this file;migrations/0006_reporting_status_checkpoint_offerings.sqlis-- include:of that file. It is marked-- adcp-upgrade: stopped-workerbecause workers of earlier releases insert checkpoints withON CONFLICTon the six-column key, which no longer exists, and swapping the primary key takes exclusive locks on the checkpoint and status event tables. On a schema without status notifications it is a no-op. The status store, projection, production and maintenance (migrate_legacy_reporting)create_schemachains run the same file, afterreporting_projection.sqland beforereporting_projection_notifications.sql. That last file now creates the projection event table on the widened key.outbox/required_status_schema.jsonandprojection/required_schema.jsonwere regenerated from a fresh install. A schema without the widening failsvalidate_status_schema(the checkpoint primary key and guard fingerprints changed), andpostgres_storagestartup names0006as pending, with thepython -m adcp migratecommand and the stopped-worker note. That message now pluralizes correctly when two stopped-worker upgrades are pending.Tests
cause_ids.test_a_six_column_status_checkpoint_schema_is_keyed_by_offering. It builds checkpoints and events on the published six-column key, with an account-level checkpoint and an offering checkpoint, and applies0006throughmigrate(). It then checks idempotency, the widened key, the event offering backfill, andvalidate_status_schema, and that a second offering's checkpoint and generation-1 event are then accepted.test_status_notifications_cover_subscription_generationsreplaces the refusal test. The production test now asserts that a second offering is served. The migration list and pending-message tests now include0006.test_reporting_status_*.pyexcept the frozen-artifactactual_*cases, which need a full checkout of the reviewed SHAs (267 passed), plus the migration, subscription, projection, production, empty-ledger and catalog suites (1037 passed). Under-n 2on a loaded machine, about 25 timing-sensitive production and projection cases failed. Their failures are the same on unchangednext(9 of those 25 failed there). Run sequentially, every one passes on this branch. I also ran a separate check against realnextcode: it built a production schema, wrote status events with the old code, applied0006alone, and continued writing with the new code. The schema matched every manifest, existingcause_ids were unchanged, and new events were written.Note: the cherry-picked test commit
This branch also carries
ed7a19fcf test(reporting): run the row-upload lease test at the wall clock, the single commit of #1767, cherry-picked unchanged. Since 2026-11-01,Postgres conformance tests (Postgres 16, core)fails on every PR intonextintest_the_producer_stages_object_uploads_outside_its_publication_transaction(STATE_UNAVAILABLE), because the harness clock is fixed in the past. On this PR's first CI run that was the only failure: 2889 passed. Whichever of the two merges first, the other's copy becomes a no-op.🤖 Generated with Claude Code