Repository navigation
feat(reporting-storage): add PostgreSQL stores for reporting subscriptions - #1724
Conversation
…tions AdCP 3.3 reporting subscriptions need their principal records and engine state to survive restarts and agree across workers. PgReportingSubscriptionStateStore implements the SDK's ReportingSubscriptionStateStore and PgPrincipalRecordStore implements PrincipalRecordStore. Each keeps one JSONB document per subject in adcp_reporting_subscription_state or adcp_principal_records and replaces it with one conditional statement on its revision, so the compare-and-swap is atomic across workers. create_schema applies the idempotent DDL, which reporting_subscriptions_schema_sql also returns. Rows are never pruned; the documents keep every subscription version for as long as obligations cite it. The stores need adcp 10.0.0b3 or later. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean, well-scoped storage-only PR.
This PR adds two Postgres CAS stores to the adcp-reporting-storage package (5 files, +218/-0). No findings were raised.
Checks performed:
- No author no-auto-approve team match.
- gated_paths: false — no hard path gate.
- high_risk: false — no sensitive-path modifications/deletions.
- Reviewer verified both
compare_and_swappaths against the SDK protocols: state-store insert hardcodes revision=1, update does revision+1 WHERE revision=%s; principal-store writes record.store_revision satisfying CHECK >= 1; rowcount==1 → True correct on both ON CONFLICT DO NOTHING and WHERE-revision paths. - Boundary stays on public adcp names and is validated by the in-PR boundary test; compat uses lazy imports with a clear ImportError on <10.0.0b3.
No Critical/High/Medium findings. The only non-blocking note is a deferred live-Postgres conformance test, which follows package convention (cannot run in unit CI; import/boundary coverage lands here) and is not a blocking gap.
Decision table: rows 1–8 do not fire (no critical/high/medium findings, no gates, no team match) → row 9 approve.
…I snapshot Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — subsequent pass, clean diff.
This PR adds reporting-storage subscription support (new package files) plus a public API snapshot update. The delta since the prior approve is confined to tests/fixtures/public_api_snapshot.json: two additive entries (PgPrincipalRecordStore, PgReportingSubscriptionStateStore) both resolving to adcp_reporting_storage.subscriptions.pg. Both symbols are defined and exported there, so the snapshot update is consistent and additive. The change is correctly carried under feat(reporting-storage): with no breaking semver signal owed (additive public surface only).
No critical/high/medium findings in this run; no prior blockers to re-surface. high_risk is false, gated_paths is false, review_decision is APPROVED, and no no-auto-approve team match. None of decision rows 1–8 fire, so this falls through to row 9.
ReliableReportingService(storage=...) takes one storage object (DX plan slice P7). memory_storage() and postgres_storage(pool= | dsn=) from adcp.storage each hold the reporting ledger, staged source objects and seals, sync_principal principal records and the reporting-subscription engine state; the managed subscription path uses its principal and state stores. Durability is a property of the storage, never a flag. postgres_storage ships in adcp-reporting-storage and folds in #1724's PostgreSQL principal and engine stores, which gain list_principals() and declare is_durable. - Reporting subscriptions on process-local storage are refused outside development with REPORTING_STORAGE_NOT_DURABLE; the message names postgres_storage and ADCP_ENV=development. pytest and ADCP_ENV=development or test allow it. Without storage=, every store the managed path uses must declare is_durable. - The PostgreSQL schema is managed by numbered migration files in adcp_reporting_storage.migrations, recorded in adcp_schema_migrations. Each file names its version, may include the package's idempotent DDL files, so every statement lives in one place, and may declare a stopped-worker upgrade. Each migration commits on its own under the reporting schema lock. - python -m adcp migrate [--dsn URL] [--status | --dry-run] applies them; --status flags stopped-worker upgrades. - Durable storage checks the schema at startup instead of changing it, and refuses one that is behind with REPORTING_SCHEMA_BEHIND, naming the command and the pending versions. - 0002 reporting_subscription_generations, #1729's widened generation key, carries the stopped-worker note in its header, in --status and in the startup refusal. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ReliableReportingService(storage=...) takes one storage object (DX plan slice P7). memory_storage() and postgres_storage(pool= | dsn=) from adcp.storage each hold the reporting ledger, staged source objects and seals, sync_principal principal records and the reporting-subscription engine state; the managed subscription path uses its principal and state stores. Durability is a property of the storage, never a flag. postgres_storage ships in adcp-reporting-storage and folds in #1724's PostgreSQL principal and engine stores, which gain list_principals() and declare is_durable. - Reporting subscriptions on process-local storage are refused outside development with REPORTING_STORAGE_NOT_DURABLE; the message names postgres_storage and ADCP_ENV=development. pytest and ADCP_ENV=development or test allow it. Without storage=, every store the managed path uses must declare is_durable. - The PostgreSQL schema is managed by numbered migration files in adcp_reporting_storage.migrations, recorded in adcp_schema_migrations. Each file names its version, may include the package's idempotent DDL files, so every statement lives in one place, and may declare a stopped-worker upgrade. Each migration commits on its own under the reporting schema lock. - python -m adcp migrate [--dsn URL] [--status | --dry-run] applies them; --status flags stopped-worker upgrades. - Durable storage checks the schema at startup instead of changing it, and refuses one that is behind with REPORTING_SCHEMA_BEHIND, naming the command and the pending versions. - 0002 reporting_subscription_generations, #1729's widened generation key, carries the stopped-worker note in its header, in --status and in the startup refusal. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ReliableReportingService(storage=...) takes one storage object (DX plan slice P7). memory_storage() and postgres_storage(pool= | dsn=) from adcp.storage each hold the reporting ledger, staged source objects and seals, sync_principal principal records and the reporting-subscription engine state; the managed subscription path uses its principal and state stores. Durability is a property of the storage, never a flag. postgres_storage ships in adcp-reporting-storage and folds in #1724's PostgreSQL principal and engine stores, which gain list_principals() and declare is_durable. - Reporting subscriptions on process-local storage are refused outside development with REPORTING_STORAGE_NOT_DURABLE; the message names postgres_storage and ADCP_ENV=development. pytest and ADCP_ENV=development or test allow it. Without storage=, every store the managed path uses must declare is_durable. - The PostgreSQL schema is managed by numbered migration files in adcp_reporting_storage.migrations, recorded in adcp_schema_migrations. Each file names its version, may include the package's idempotent DDL files, so every statement lives in one place, and may declare a stopped-worker upgrade. Each migration commits on its own under the reporting schema lock. - python -m adcp migrate [--dsn URL] [--status | --dry-run] applies them; --status flags stopped-worker upgrades. - Durable storage checks the schema at startup instead of changing it, and refuses one that is behind with REPORTING_SCHEMA_BEHIND, naming the command and the pending versions. - 0002 reporting_subscription_generations, #1729's widened generation key, carries the stopped-worker note in its header, in --status and in the startup refusal. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ReliableReportingService(storage=...) takes one storage object (DX plan slice P7). memory_storage() and postgres_storage(pool= | dsn=) from adcp.storage each hold the reporting ledger, staged source objects and seals, sync_principal principal records and the reporting-subscription engine state; the managed subscription path uses its principal and state stores. Durability is a property of the storage, never a flag. postgres_storage ships in adcp-reporting-storage and folds in #1724's PostgreSQL principal and engine stores, which gain list_principals() and declare is_durable. - Reporting subscriptions on process-local storage are refused outside development with REPORTING_STORAGE_NOT_DURABLE; the message names postgres_storage and ADCP_ENV=development. pytest and ADCP_ENV=development or test allow it. Without storage=, every store the managed path uses must declare is_durable. - The PostgreSQL schema is managed by numbered migration files in adcp_reporting_storage.migrations, recorded in adcp_schema_migrations. Each file names its version, may include the package's idempotent DDL files, so every statement lives in one place, and may declare a stopped-worker upgrade. Each migration commits on its own under the reporting schema lock. - python -m adcp migrate [--dsn URL] [--status | --dry-run] applies them; --status flags stopped-worker upgrades. - Durable storage checks the schema at startup instead of changing it, and refuses one that is behind with REPORTING_SCHEMA_BEHIND, naming the command and the pending versions. - 0002 reporting_subscription_generations, #1729's widened generation key, carries the stopped-worker note in its header, in --status and in the startup refusal. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ReliableReportingService(storage=...) takes one storage object (DX plan slice P7). memory_storage() and postgres_storage(pool= | dsn=) from adcp.storage each hold the reporting ledger, staged source objects and seals, sync_principal principal records and the reporting-subscription engine state; the managed subscription path uses its principal and state stores. Durability is a property of the storage, never a flag. postgres_storage ships in adcp-reporting-storage and folds in #1724's PostgreSQL principal and engine stores, which gain list_principals() and declare is_durable. - Reporting subscriptions on process-local storage are refused outside development with REPORTING_STORAGE_NOT_DURABLE; the message names postgres_storage and ADCP_ENV=development. pytest and ADCP_ENV=development or test allow it. Without storage=, every store the managed path uses must declare is_durable. - The PostgreSQL schema is managed by numbered migration files in adcp_reporting_storage.migrations, recorded in adcp_schema_migrations. Each file names its version, may include the package's idempotent DDL files, so every statement lives in one place, and may declare a stopped-worker upgrade. Each migration commits on its own under the reporting schema lock. - python -m adcp migrate [--dsn URL] [--status | --dry-run] applies them; --status flags stopped-worker upgrades. - Durable storage checks the schema at startup instead of changing it, and refuses one that is behind with REPORTING_SCHEMA_BEHIND, naming the command and the pending versions. - 0002 reporting_subscription_generations, #1729's widened generation key, carries the stopped-worker note in its header, in --status and in the startup refusal. Refs #1695 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part of #1695, item 6 (storage). The change is under
packages/adcp-reporting-storage/, apart from one root file:tests/fixtures/public_api_snapshot.jsongains the two new storage exports. The public-API snapshot test coversadcp_reporting_storagefrom the root test tree, so adding a storage export can't be a storage-only commit without turningnextred, and rebase-merging is ruled out by the squash-only IPR policy (docs/releasing.md). The squash commit therefore also appears inadcp's next changelog. It builds on the SDK protocols merged in #1722.PgReportingSubscriptionStateStore: implementsadcp.reporting.subscriptions.ReportingSubscriptionStateStore, the engine's grants, exceptions, slots and version snapshots, inadcp_reporting_subscription_state.PgPrincipalRecordStore: implementsadcp.server.PrincipalRecordStore, thesync_principalrecords includingreporting_subscriptionsand their versions, inadcp_principal_recordsviaprincipal_record_to_document.INSERT … ON CONFLICT DO NOTHINGorUPDATE … WHERE revision = expected. The compare-and-swap is therefore atomic across every worker on the database.create_schema()applies idempotent DDL, whichreporting_subscriptions_schema_sql()also returns. Rows are never pruned here; the documents keep every subscription version for as long as obligations cite it.subscriptionssubmodule. The pool is borrowed, never owned.adcp10.0.0b3 or later, and import the SDK codec lazily with a clearImportErroron older versions.Tests: the PostgreSQL conformance test lands in a separate
test:PR, so this commit stays storage-only. Against a local PostgreSQL 16 it covers schema idempotence, CAS semantics, 20 concurrent writers losing nothing, and a restarted seller (newPrincipalServiceand engine on the same database) serving the same readback and owing identical periods. That test plustests/test_reporting_storage_boundary.py: 9 passed.Refs #1695
🤖 Generated with Claude Code