Skip to content
91 changes: 88 additions & 3 deletions docs/reliable-reporting-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,94 @@ dropped `status_retention_days` after they close.

`subscription_capabilities(...)` and `advertise_reporting_subscriptions(...)`
project `configuration_tasks`, the deprecated `configuration_task`, the
`subscriptions` block, the feature id and the principal section. The engine
does not yet write ledger obligations, so do not advertise the capability
until obligations are materialized for the periods it owes.
`subscriptions` block, the feature id and the principal section.

**Serving subscriptions from the service.** Pass
`reporting_subscriptions=ReportingSubscriptionServiceOptions(...)` to
`ReliableReportingService`. Without it, capabilities, responses and worker
turns are unchanged.

```python
from adcp.reporting.service_subscriptions import ReportingSubscriptionServiceOptions

reporting = ReliableReportingService(
store=store,
account_context=resolve_account_context, # also receives subscription generations
reporting_subscriptions=ReportingSubscriptionServiceOptions(
engine=engine,
list_principals=list_subscribing_principals,
resolve_principal=resolve_authenticated_principal,
definitions={"daily_v1": daily_definition}, # content-addressed, per offering
),
)
```

What the service does with it:

- **Planning.** Each worker turn first plans every listed principal's frozen
owed set, then executes those obligations alongside the account
configurations.
- A subscription generation is one per (account, principal,
`subscription_id`, version, resolved `offering_id`). Its `consumer_id` is
the principal's `subject`.
- Each generation is installed through the same trusted path as
`configure()`. `account_context` receives it with
`configuration.subscription` set and returns the source route, currency
and advertised offering. The service validates these as it does for an
account configuration.
- Obligations are written only for periods ending by
`min(engine through, turn clock)`. Each carries exactly that period's
media buys and is never rewritten.
- A subscription that cannot be planned is recorded in
`turn.subscription_errors` and reported to `planning_error_handler` (or
the worker error handler). It never stops another subscription or an
account configuration.
- **Capabilities.** The service adds `configuration_tasks`, the
`subscriptions` block, `standard_dimensions` on every offering, the feature
id and the principal section, at the release each caller is served.
- Callers served below 3.3 see the pre-3.3 block.
- The adcp#8585 offering fields reach only 3.3.0-beta.2 callers.
- Advertise `adcp.principal` yourself. The service refuses to advertise
subscriptions without it.
- **Status.** `get_reporting_status` summary and periods views gain
`subscription_coverage[]`.
- **Visibility.** Ledger reads and `sync_reporting_status` are scoped by the
caller's `consumer_id`, so a principal sees only its own subscription
periods. The caller resolver must therefore yield the principal's `subject`;
the service checks `resolve_principal` against it.

**Grants: access means access.** `AuthorizedAccount.allowed_tasks` carries the
grant's `authorization.allowed_tasks`.

- Coverage continues only while the grant lists `get_reporting_status`; a grant
reduced below that ends coverage exactly like a revocation.
- For a caller holding subscription generations on the account, the service
checks each reporting task against the current grant:
- `get_reporting_status`;
- `get_media_buy_delivery` exact reads;
- `sync_reporting_status`;
- `sync_reporting_receipts`.

A task the grant does not list fails with `ACCOUNT_NOT_FOUND`.
- There is no window after a loss of authority: a revoked account is unknown to
every task, receipts and statuses included.
- To let a buyer finish reconciliation, keep a reduced grant (for example
`get_reporting_status`, `sync_reporting_receipts` and `sync_reporting_status`)
rather than revoking.

The beta.2 spec text still describes a post-revocation window; the beta.3
change replaces it with this rule.

**Limits.** These are not served yet:

- the service refuses `production` options;
- subscriptions that resolve a managed or receipt-reconciled offering;
- status notifications for subscription generations: a PostgreSQL store with
them enabled refuses an owed set. `PgReportingLedgerStore` otherwise holds
owed sets, after the generation-key widening described in
[ledger migrations](reporting-ledger-migration.md).

Keep the engine's `status_retention_days` equal to the service's.

## Advanced composition

Expand Down
31 changes: 31 additions & 0 deletions docs/reporting-ledger-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,37 @@ reuses an ID for a different logical period now raises
account's obligation in memory. When filtering consumer statements by
obligation IDs, the named obligations must exist in the requested account.

## Reporting-subscription generations: the widened key (AdCP 3.3)

Reporting subscriptions keep one generation per resolved offering of a
subscription version, so the generation key gains `offering_id`:
`(account_id, consumer_id, delivery_config_id, delivery_config_version, offering_id)`.
`ReportingConfigurationGenerationKey.offering_id` is `None` for every
account-level generation, and their digests, work keys and persisted scopes are
unchanged.

In PostgreSQL this is a **stopped-worker maintenance upgrade**:

1. Stop every reporting worker. Workers of earlier releases refuse the widened
schema; mixed-version workers are unsupported.
2. Run `create_schema()` (or your usual migration entry point) once with the new
release. Under the schema lock it widens the key in place and losslessly:
- `reporting_configurations` gains `offering_id` (`''` for existing rows),
and its primary key includes it;
- `reporting_obligations` gains `offering_id`, and its period unique index
includes it;
- `reporting_consumer_statuses` gains `offering_id`;
- the SDK's own references (reconciliation records, materializer discovery,
status scope checkpoints, production generations) are re-pointed at the
widened key. They hold account-level generations only, enforced by a
`CHECK`, until each feature serves subscriptions.
3. Restart the workers on the new release.

A foreign key outside the SDK that references `reporting_configurations`
refuses the upgrade with a maintenance error, rather than being dropped.
Re-point it at the widened key, then run the upgrade. The upgrade is
idempotent; a fresh schema is created in the widened layout directly.

## Historical account-generation upgrade

The following notes describe the pre-ownership SDK release and are retained for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from adcp.reporting.ledger._record_rows import (
_ADJUSTMENT_COLUMNS,
_CONFIGURATION_COLUMNS,
_OBLIGATION_COLUMNS,
_REVISION_COLUMNS,
_adjustment_from_row,
Expand Down Expand Up @@ -321,19 +322,16 @@ async def _delivery_context(
)
row = await (
await connection.execute(
"SELECT delivery_config_id, delivery_config_version, account_id,"
" report_definition_id, reporting_profile, feed_purpose, required_finality,"
" account_timezone, schedule, media_buy_ids, activated_at, deactivated_at,"
" automated_recovery_seconds, status_retention_days, definition,"
" authoritative_party, consumer_id, quarantined"
f"SELECT {_CONFIGURATION_COLUMNS}" # nosec B608
" FROM reporting_configurations WHERE account_id = %s"
" AND consumer_id = %s AND delivery_config_id = %s AND delivery_co"
"nfig_version = %s",
"nfig_version = %s AND offering_id = %s",
(
who.account_id,
who.consumer_id,
generation.delivery_config_id,
generation.delivery_config_version,
generation.offering_id or "",
),
)
).fetchone()
Expand Down
Loading
Loading