Repository navigation
feat(reporting-storage): make the contract-2 ledger upgrade rolling-deploy safe with expand and contract - #1868
Conversation
…eploy safe with expand and contract A ledger created by adcp-reporting-storage 0.1.x/0.2.0b0 is now upgraded in two halves, so a default rolling update works and a rollback keeps working until the operator contracts (#1856, #1831). Expand (0002, 0006; online-safe): offering_id is added with CHECK (offering_id = ''), the keys and references the earlier release uses keep their names and definitions (four-column reporting_configurations_pkey, six-column period, checkpoint and event keys, four/six-column references), and the widened ones are added beside them (reporting_configurations_generation_key, reporting_obligations_offering_period_key, reporting_status_scope_checkpoints_offering_key, reporting_status_notification_events_offering_fkey/_offering_key). While uncontracted, both releases install the earlier release's bodies of the three reconciliation functions and two status guards, so an old pod's restart changes nothing either release validates. The validators accept the expanded catalog through outbox/required_expand_schema.json, derived from a real expanded ledger. Contract (0007, "-- adcp-upgrade: contract"): drops the expand-only objects, renames the widened keys back, re-creates the offering-aware references and restores this release's function bodies; the result is catalog-identical to a new ledger. Only "python -m adcp migrate --contract" (migrate(contract=True)) applies it to an expanded ledger; plain migrate, create_schema() and apply_pending() stop before it, and record it unprompted on a ledger that was never expanded. postgres_storage().check_schema() ignores a pending contract step. A new ledger is created contracted. A subscription generation written before contract raises ReportingUpgradeNotContractedError (a LedgerConflictError, code REPORTING_UPGRADE_NOT_CONTRACTED) naming the contract command. The published-wheel tests drive the full rolling sequence for 0.1.0b4/9.3.1 and 0.2.0b0/10.0.0b2, with and without notifications, plus rollback, concurrent old/new boots, the CLI, a 0.2.0b1-widened ledger and the expand manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….0b0 ledger upgrade Rewrite "Deploying the upgrade", the storage README and MIGRATION_v9_to_v10: roll out normally, run python -m adcp migrate --contract once every earlier-release reporting process has stopped, subscriptions need the contract step, and a rollback works until it. Recreate/stopped-worker advice remains only for the materializer, production and projection extensions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reates while the ledger is expanded An earlier-release process that turns status notifications on after the expand creates its own six-column status tables without offering_id. A later new boot with notifications then failed on the seven-column offering key, and the contract step failed too. The checkpoint-offerings DDL now adds offering_id and the five-column generation reference to any checkpoint table that lacks them, before the expand or in-place widening, and 0007 re-runs it first, so contract never meets a half-expanded table. The published-wheel tests cover the sequence for both pairs: seed without notifications, expand, old pod with notifications, then a new pod with notifications or the contract step directly, and a contracted catalog equal to a fresh install. Docs: scope the rolling-update headline to the ledger, outbox and status notifications; say the contract step can time out or deadlock against busy new pods and is atomic, so retry; and say an old pod running past contract can still touch subscription generations' lease columns, so stop it first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| IF EXISTS (SELECT 1 FROM pg_constraint | ||
| WHERE conrelid = 'reporting_configurations'::regclass | ||
| AND conname = 'reporting_configurations_generation_key') THEN | ||
| LOCK TABLE reporting_configurations, reporting_obligations, reporting_consumer_statuses |
There was a problem hiding this comment.
Medium: This LOCK TABLE … ACCESS EXCLUSIVE runs with no bounded lock_timeout. The docs (docs/reporting-ledger-migration.md:291-293) explicitly support running --contract while new-release pods keep serving, and frame the risk as "it waits out a lock or statement timeout … run it again." But a queued ACCESS EXCLUSIVE request blocks all subsequent access (including new pods' reads) to reporting_configurations behind it, and with the server default lock_timeout = 0 that wait is unbounded — the contract does not fail fast, it wedges live new pods until a human kills it. The "atomic, retry it" model only holds if the lock actually times out.
The SDK already bounds this on worker paths: ledger/pg.py:1010-1016 caps lock_timeout to ≤5000ms before taking locks. The contract step should do the same (set a bounded lock_timeout before the LOCK TABLEs), or the docs must state the operator MUST set lock_timeout before --contract.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — expand/contract reporting ledger upgrade (#1856).
What I checked:
- No critical/high findings (row 1 clear).
gated_paths: false, no deletions,high_risk: false(rows 2–5 clear).- No prior decision; author not on any no-auto-approve team (rows 6–7 clear).
- Only 1 medium finding — below the 3-finding threshold (row 8 clear).
feat(reporting-storage):is the correct non-breaking semver signal; new public export is paired with the api snapshot update. Expand → side-by-side → subscription refusal → contract pathway is driven by real wheel pairs (0.1.0b4/9.3.1, 0.2.0b0/10.0.0b2) with byte-identical catalog assertions including function bodies, notifications, rollback, concurrency, and late-created status tables. put_configuration's CHECK → ReportingUpgradeNotContractedError path and needs_contract gating verified.
Falls through to row 9 → approve.
Medium findings (non-blocking):
packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_contract.sql:47— Contract step takes ACCESS EXCLUSIVE locks with no bounded lock_timeout; a contract run against still-serving new pods can block live access indefinitely rather than failing fast. Worth bounding like the worker paths (pg.py:1010-1016), but a single medium with no escalation trigger does not block.
Medium findings
- packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_contract.sql:47 — Contract step takes ACCESS EXCLUSIVE locks with no bounded lock_timeout
Closes #1856. An independent review found one blocker (an old pod enabling status notifications after the expand left checkpoint tables new pods and the contract step could not handle); it is fixed in bb4bac4 and re-verified for both published pairs.
A default rolling update from
adcp==9.3.1+adcp-reporting-storage==0.1.0b4, or from 0.2.0b0 + 10.0.0b2, now works. While old and new processes share the ledger:Contracting then leaves exactly the fresh-install catalog.
Design
stopped-worker. What the earlier release needs keeps its original name:reporting_configurations_pkeystays the 4-column PK, the 6-column obligation period key stays, and so do the old FKs and the checkpoint/event keys. The widened objects get new names (reporting_configurations_generation_keyis the 5-column UNIQUE, and so on), and the widened tables carryCHECK (offering_id = ''). On the expand state, 0002 and 0006 are no-ops.outbox/required_expand_schema.json: alternate fingerprints, each accepted only when the widened object replacing it is present.0007_reporting_contract_generation_keyshas the header-- adcp-upgrade: contract. Onlypython -m adcp migrate --contract(ormigrate(contract=True)) applies it to an expanded ledger. Plainmigrate,create_schema()andapply_pendingstop before it, somigratestays safe as a pre-deploy job.--statusends with "up to date except the contract step: run … once every earlier-release reporting process has stopped".postgres_storage().check_schema()doesn't count a pending contract step as behind.ReportingUpgradeNotContractedError, aLedgerConflictErrorwith codeREPORTING_UPGRADE_NOT_CONTRACTED. It names the command and is exported.Tests (real PostgreSQL, published wheels)
The new rolling test runs the full sequence for both published pairs, with and without notifications:
put_configuration,commit_obligation+commit_revision, and a worker turn;--contract;There are also tests for rollback before contract,
migratewithout the flag (plus--status/describeandcheck_schema), fresh installs, a 0.2.0b1-widened ledger, the expand manifest against a real expanded ledger, and concurrency (two new boots plus one old boot: each version recorded once, no deadlock).Local validation:
make lint,make typecheck-all,make validate-generated: pass.origin/next, and the one test that failed only on this branch passes when run on its own.Notes and limits
main(refuse on the key, not the column) before it's published. With that, 0.1.0b5 runs on the expand state like 0.1.0b4.DuplicateColumn(nothing is corrupted). The docs say to skip them.🤖 Generated with Claude Code