Skip to content

feat(reporting): compose PostgreSQL services from adapters - #1231

Merged
bokelley merged 2 commits into
mainfrom
feat/reporting-postgres-composition
Sep 28, 2026
Merged

bokelley merged 2 commits into
mainfrom
feat/reporting-postgres-composition

Conversation

@bokelley

@bokelley bokelley commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

ReliableReportingService.postgres() previously left ordinary adapter staging and replay seals in memory and required adopters to assemble the managed reporting graph. It now defaults to PostgreSQL staging/seals and accepts ReportingProductionOptions to construct producers, materialization, status/exact reads, receipts and optional signed notification workers from registered adapters and trusted domain inputs.

The factory uses the existing production admission, live source-binding checks, per-request authorization and durable capability gates. Explicit source-store overrides and the existing from_production() bridge remain available. The borrowed pool stays open after service shutdown. The service guide, production wiring example and strict adopter type fixture show the new path.

Validation: make lint, make typecheck-all, 11 focused memory/PostgreSQL tests and 85 affected lifecycle/admission/binding tests pass. Coverage includes durable replay after restart, both pool transaction modes, mounted admission/materialization/exact reads, and in-flight revocation/remapping followed by restoration. Independent db56 review found no blockers at 583efd49 and passed all 11 focused tests against PostgreSQL. Current-head hosted CI passed all 50 executable checks, including Python 3.12 coverage and installed PostgreSQL lanes; the draft-only code review was skipped.

The prior local full run passed 12,059 tests but three child interpreters loaded an older shared editable install; all three passed in the branch virtualenv. A current-head make test rerun in a virtualenv verified to import this checkout reached 922 passed / 850 skipped with no failures, then was stopped as the shared disk fell below 0.5 GiB during distribution packaging. Generated files were cleaned. Hosted CI supplies the complete current-head full-suite result.

Addresses the factory gap in #1172. The lifecycle/failure-point kit is separate in #1232. Rebased onto merged rc.7 adoption #1229 at 4756715; all seven contributed file blobs are unchanged. The branch is ready for the current-head formal review; its contributed files are unchanged from the independent technical review.

Upgrade note: ReliableReportingService.postgres() now bootstraps PostgreSQL staging and seal tables at initialize() even when production is not enabled. Existing deployments must grant the borrowed pool's startup role permission to run reporting_inline_storage.sql before adopting this version; DDL-restricted roles need an updated startup grant or deployment sequence. This is an intentional durable-default change. Commit 7f5ca415 also adds the two new production types to the package's __all__/dir() exports.

Post-review validation on 7f5ca415: make lint, make typecheck-all, public dir() and import * checks, and the focused factory test (4 passed; 7 PostgreSQL cases skipped because the local test environment has no PostgreSQL URL) passed. The full hosted CI rerun is pending on this head.

Copy link
Copy Markdown
Contributor Author

Review — PostgreSQL factory wiring, head 583efd49

No blocking findings. The #1172 gap is closed and I verified it at runtime rather than by reading. Two non-blocking observations below.

The in-memory fallback is gone — introspected, not inferred

Constructed a service through the factory against a real pool and looked at what's actually wired:

ledger store : PgReportingLedgerStore
staging store: PgReportingStagingStore   is_durable = True
seal store   : PgReportingSealStore      is_durable = True
IN-MEMORY components on the postgres path: NONE

The staging or InMemoryStagingStore() / seals or InMemorySealStore() defaults still exist at service.py:293/295, but postgres() replaces service.sources with a registry constructed from PG stores, so those branches are unreachable from this path. That's the right shape — the defaults stay available for the non-postgres registration path instead of being deleted.

Schema: one create_schema call does cover seals

Worth confirming explicitly, because _initialize_source_storage is bound to staging's method and a missing seal table would only surface on the first seal write. It's fine: create_schema is inherited from the shared _PgStorage base, executes the whole reporting_inline_storage.sql migration, and _transaction locks both reporting_inline_objects and reporting_inline_seals. One call provisions both.

Docs moved with the behaviour

The previous caveat — "The PostgreSQL factory makes the ledger durable; it does not make the default adapter staging or replay-seal stores durable" — is gone and replaced with accurate text. I flagged this as a risk before reading the diff (a wiring change that leaves the old caveat makes docs wrong in the opposite direction); it didn't materialise.

Truthful capabilities and restart replay are both asserted, not assumed

  • assert bool(caps["media_buy"]["reporting_delivery"]["managed_delivery"]) == (backend == "postgres") — parametrised across memory and postgres, so the memory factory is proven not to advertise managed delivery.
  • The restart test asserts isinstance(registered.object_reader, PgReportingStagingStore) and isinstance(registered.executor._seals, PgReportingSealStore), then after a full restart asserts result == original with len(adapter.calls) == 1 — identical bytes, no refetch. That's the durability claim proven end to end.

Real-PG coverage is genuine

I measured it rather than trusting the label: 7 of the 11 tests skip without ADCP_PG_TEST_URL. Against a real PostgreSQL 15 instance all 11 passed, 0 skipped; make lint clean.


Observation 1 — two type checks on the same object disagree (non-blocking)

ReliableReportingService._compose_production gates with isinstance(self.store, (InMemoryReportingProductionStore, PgReportingProductionStore)), but ReportingProductionOptions._compose branches on exact type(store) is PgReportingProductionStore. Verified empirically:

subclass of PgReportingProductionStore
  passes isinstance gate  : True
  passes exact-type check : False

So a subclass slips past the clear ReliableReportingConfigurationError and fails later with a generic ValueError("production requires the factory's matching production store"). Given #1172's criterion that advanced adopters can replace individual stores without forking the orchestrator, it's worth deciding which is intended and making both agree — exact-type in _compose_production for a clear early rejection, or isinstance branching in _compose if subclassing is meant to work.

Observation 2 — is_durable is inert inside reporting (informational)

is_durable = True is declared on inline_storage._PgStorage and ledger/pg.PgReportingLedgerStore, but nothing under src/adcp/reporting/ reads it — every consumer lives in src/adcp/decisioning/. Capability truthfulness here is enforced by store type instead, which works. Flagging only so nobody later assumes the marker is what gates reporting durability; a custom durable store setting is_durable = True would not thereby be accepted.

What I ran at 583efd49

  • make lint — clean
  • tests/conformance/reporting/test_reporting_production_service_factory.py against real PostgreSQL — 11 passed, 0 skipped (4 passed / 7 skipped without a database)
  • Runtime introspection of the composed stores, and the subclass probe above

CI at time of writing: 18 SUCCESS / 1 skipped / 29 pending, no failures.

Copy link
Copy Markdown
Contributor Author

Coordinator: #1229 has merged into main at c45a2c7. Please rebase #1231 on that commit now, run the clean full suite in this worktree's own virtualenv plus affected PG gates, push, and request renewed exact-head review. I will monitor CI and handle the merge gate.

@bokelley
bokelley force-pushed the feat/reporting-postgres-composition branch from 583efd4 to 4756715 Compare September 27, 2026 23:40

Copy link
Copy Markdown
Contributor Author

Coordinator rebased #1231 onto merged rc.7 main c45a2c7 and pushed 4756715. All seven contributed file blobs are byte-identical to 583efd4; the rebase had no conflicts. Fresh integrated CI is starting. Please fetch this head and run the clean full suite in the worktree virtualenv as planned, then request renewed review.

Copy link
Copy Markdown
Contributor Author

Coordinator validation on rebased head 4756715: installed this checkout editable into its own .venv, verified python -I imports this exact checkout, and started make test. Hosted CI is 50/50 green (draft-only review skipped). I will update the PR with the clean full-suite result and mark ready if it passes.

Copy link
Copy Markdown
Contributor Author

Current-head validation update: hosted CI passed all 50 executable checks, including full Python suites and PostgreSQL lanes; zero review threads. I reran local make test after installing this exact checkout into its .venv and verifying python -I resolves it. It reached 922 passed / 850 skipped with no test failures, then I interrupted during a distribution-packaging case because shared disk fell below 0.5 GiB. This is an environment-capacity stop, not a candidate failure. Generated build and pytest temp directories from my run were removed; disk recovered. The earlier local full run passed 12,059 tests with three stale-editable-install child failures that passed in the branch venv. CI is the complete current-head gate.

@bokelley
bokelley marked this pull request as ready for review September 28, 2026 00:38
Comment thread src/adcp/reporting/__init__.py
Comment thread src/adcp/reporting/service.py
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 28, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — additive managed-reporting factory with no blocking findings.

What I checked:

  • Additive public surface (adapter-first reporting factory) shipped under a correct feat: prefix — no removed/renamed/retyped public exports, no required→optional flips, no wire-shape breaks, so the semver signal is satisfied.
  • Import layering verified sound; no direct imports of generated codegen names.
  • Schema coverage, notification validation, and construction order confirmed with real memory/postgres test coverage.
  • No credentials in ctx_metadata; no CI gates disabled.

Medium findings (non-blocking, 2):

  • src/adcp/reporting/__init__.py — new exports ReportingProductionOptions/ReportingServiceOffering omitted from adcp.reporting.__all__.
  • src/adcp/reporting/service_production.py — postgres() now installs durable Pg source stores plus source DDL unconditionally (a documented runtime default change).

Decision path: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3/5 n/a); mediums are not in data-loss/schema/infra categories (row 4 n/a); no prior escalation (row 6 n/a); no no-auto-approve team match (row 7 n/a); only 2 medium findings (row 8 n/a). Falls through to row 9 — approve.

Note: review_decision is REVIEW_REQUIRED, but this only matters as a hard gate when gated_paths is true, which it is not here.

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean subsequent pass.

This is a follow-up push (prior decision: approve) on a non-high-risk, non-gated PR with no no-auto-approve team match. The reviewer found no Critical/High/Medium findings.

What was checked:

  • src/adcp/reporting/__init__.py adds ReportingProductionOptions and ReportingServiceOffering to __all__, completing the import */dir() surface for names already wired into _LAZY_EXPORTS and the TYPE_CHECKING block. PEP 562 facade and import-layering contract intact.
  • docs/reliable-reporting-service.md documents the load-bearing DDL-bootstrap default change; table names verified against inline_storage.py.

No blocking findings, no escalation triggers (high_risk false, gated_paths false, no deletions, no data-loss/schema/infra medium findings, no team gate). Falls through to row 9.

@bokelley
bokelley merged commit 65be431 into main Sep 28, 2026
52 checks passed
@bokelley
bokelley deleted the feat/reporting-postgres-composition branch September 28, 2026 01:43
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.

1 participant