@adcp/sdk 15.7.1 → 15.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/core/SingleAgentClient.d.mts +14 -0
- package/dist/lib/core/SingleAgentClient.d.ts +14 -0
- package/dist/lib/core/SingleAgentClient.js +138 -54
- package/dist/lib/core/SingleAgentClient.mjs +141 -54
- package/dist/lib/core/agent-discovery-cache.d.mts +2 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +2 -0
- package/dist/lib/core/creative-schema-support.d.mts +3 -0
- package/dist/lib/core/creative-schema-support.d.ts +3 -0
- package/dist/lib/core/creative-schema-support.js +113 -0
- package/dist/lib/core/creative-schema-support.mjs +89 -0
- package/dist/lib/core/product-cache.js +4 -3
- package/dist/lib/core/product-cache.mjs +4 -3
- package/dist/lib/reporting/ledger/handler.d.mts +10 -3
- package/dist/lib/reporting/ledger/handler.d.ts +10 -3
- package/dist/lib/reporting/ledger/handler.js +17 -4
- package/dist/lib/reporting/ledger/handler.mjs +17 -4
- package/dist/lib/reporting/ledger/managed.d.mts +3 -1
- package/dist/lib/reporting/ledger/managed.d.ts +3 -1
- package/dist/lib/reporting/ledger/managed.js +4 -1
- package/dist/lib/reporting/ledger/managed.mjs +4 -1
- package/dist/lib/reporting/ledger/postgres.d.mts +87 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +87 -0
- package/dist/lib/reporting/ledger/postgres.js +314 -13
- package/dist/lib/reporting/ledger/postgres.mjs +314 -13
- package/dist/lib/reporting/ledger/row-storage.d.mts +23 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +23 -0
- package/dist/lib/reporting/ledger/row-storage.js +56 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +56 -0
- package/dist/lib/reporting/ledger/types.d.mts +26 -0
- package/dist/lib/reporting/ledger/types.d.ts +26 -0
- package/dist/lib/reporting/service/index.d.mts +33 -3
- package/dist/lib/reporting/service/index.d.ts +33 -3
- package/dist/lib/reporting/service/index.js +20 -2
- package/dist/lib/reporting/service/index.mjs +20 -2
- package/dist/lib/reporting/service/maintenance.d.mts +20 -1
- package/dist/lib/reporting/service/maintenance.d.ts +20 -1
- package/dist/lib/reporting/service/maintenance.js +25 -2
- package/dist/lib/reporting/service/maintenance.mjs +25 -2
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +14 -16
- package/dist/lib/testing/storyboard/request-builder.mjs +14 -16
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/v2/projection/creative-delivery.d.mts +22 -0
- package/dist/lib/v2/projection/creative-delivery.d.ts +22 -0
- package/dist/lib/v2/projection/creative-delivery.js +92 -0
- package/dist/lib/v2/projection/creative-delivery.mjs +91 -0
- package/dist/lib/v2/projection/legacy-metadata.d.mts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.d.ts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.js +17 -0
- package/dist/lib/v2/projection/legacy-metadata.mjs +15 -0
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/CREATIVE-DELIVERY.md +16 -0
- package/docs/guides/REPORTING-LEDGER-CUTOVER.md +405 -0
- package/docs/guides/REPORTING-LEDGER.md +130 -0
- package/docs/guides/REPORTING-OPERATIONS.md +12 -0
- package/docs/llms.txt +3 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -444,6 +444,14 @@ class PostgresReportingLedgerStore {
|
|
|
444
444
|
[accountId, kind, recordId, obligationId]
|
|
445
445
|
);
|
|
446
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Throws `INVALID_INPUT` when any configured object row binding's key template omits
|
|
449
|
+
* `{revision_key}`, which makes snapshot compaction unsafe (see `compactSupersededSnapshots`).
|
|
450
|
+
* Called by `compactSupersededSnapshots` and by service construction when compaction is enabled.
|
|
451
|
+
*/
|
|
452
|
+
assertCompactionSafeRowBindings() {
|
|
453
|
+
this.rowStorage.assertCompactionSafeKeyTemplates();
|
|
454
|
+
}
|
|
447
455
|
/** Probe and register row storage when enabled. Safe to call repeatedly. */
|
|
448
456
|
async readyRowStorage() {
|
|
449
457
|
if (this.rowStorageWrites) await this.rowStorage.ready();
|
|
@@ -954,7 +962,14 @@ ${managedDueArm} )
|
|
|
954
962
|
[obligationId]
|
|
955
963
|
);
|
|
956
964
|
const revisions = [];
|
|
957
|
-
for (const row of result.rows)
|
|
965
|
+
for (const row of result.rows) {
|
|
966
|
+
try {
|
|
967
|
+
revisions.push(await this.hydrateRevision(clone(row.data), row.account_id));
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if ((0, import_row_storage.isReportingRowStoreError)(error) && error.code === "ROWS_EXPIRED") continue;
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
958
973
|
return revisions;
|
|
959
974
|
}
|
|
960
975
|
async listRevisionMetadata(obligationId, accountId) {
|
|
@@ -1425,7 +1440,9 @@ ${managedDueArm} )
|
|
|
1425
1440
|
list.push({ objectKey: row.object_key, nativeVersion: row.native_version });
|
|
1426
1441
|
byBinding.set(row.row_binding_id, list);
|
|
1427
1442
|
}
|
|
1428
|
-
for (const [bindingId, list] of byBinding)
|
|
1443
|
+
for (const [bindingId, list] of byBinding) {
|
|
1444
|
+
await this.deleteUnreferencedObjects(bindingId, list, { obligationId }, signal);
|
|
1445
|
+
}
|
|
1429
1446
|
}
|
|
1430
1447
|
await this.transaction(
|
|
1431
1448
|
async (client) => {
|
|
@@ -1461,6 +1478,228 @@ ${managedDueArm} )
|
|
|
1461
1478
|
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1462
1479
|
);
|
|
1463
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Compact superseded snapshot revisions to metadata only, as adcontextprotocol/adcp#8100
|
|
1483
|
+
* permits (`reporting-revision.json` `content_retention`). Opt-in: nothing calls this
|
|
1484
|
+
* unless the host enables `compaction` in maintenance, and it must not be enabled in
|
|
1485
|
+
* production before the AdCP release that takes compaction into effect (the next
|
|
1486
|
+
* eligible minor after November 20, 2026).
|
|
1487
|
+
*
|
|
1488
|
+
* A snapshot revision is compacted only when every one of these holds:
|
|
1489
|
+
*
|
|
1490
|
+
* - it is superseded: a later revision names it in `supersedes_reporting_revision_id`
|
|
1491
|
+
* (so a current revision, and every official revision, is never compacted);
|
|
1492
|
+
* - it is not the revision the current, unsuperseded revision names;
|
|
1493
|
+
* - its recovery grace has passed: `max(automatedRecoveryWindowSeconds, 3600)` seconds
|
|
1494
|
+
* since its successor's `created_at`;
|
|
1495
|
+
* - while its window is open (`statusRetentionDays` since its successor's `created_at`),
|
|
1496
|
+
* no caller's reporting receipt (any status) names it and no caller's current consumer
|
|
1497
|
+
* status statement of `received`, `unreadable` or `content_mismatch` names it. The
|
|
1498
|
+
* check spans every caller, so content any caller is entitled to is kept;
|
|
1499
|
+
* - it has rows (nothing to reclaim otherwise, and an empty exact read is still correct);
|
|
1500
|
+
* - no live obligation lease, no pending materialization of it, and no live change-feed
|
|
1501
|
+
* consumer that has not yet passed its change row (the holds retirement applies,
|
|
1502
|
+
* narrowed to this revision).
|
|
1503
|
+
*
|
|
1504
|
+
* Only row content is dropped: the revision header, its manifest and every metadata
|
|
1505
|
+
* listing stay. The row set is marked `pruned`, so an exact read answers
|
|
1506
|
+
* `ROWS_EXPIRED`, which the handlers surface as the same nondisclosing
|
|
1507
|
+
* `REFERENCE_NOT_FOUND` as an unknown ID. No change-feed row is recorded: the revision's
|
|
1508
|
+
* metadata is unchanged, and a sink still catching up holds compaction back.
|
|
1509
|
+
*
|
|
1510
|
+
* Compaction is resumable and runs in the same three phases as retirement, under the
|
|
1511
|
+
* account lock where eligibility is decided:
|
|
1512
|
+
*
|
|
1513
|
+
* 1. re-check eligibility and mark the row set `pruning`, so reads answer as expired and
|
|
1514
|
+
* no new revision can share its chunks;
|
|
1515
|
+
* 2. delete external objects at their recorded versions, skipping any object another
|
|
1516
|
+
* row set that is not being pruned still references (chunk sharing);
|
|
1517
|
+
* 3. delete the chunk manifest rows and any PostgreSQL chunk body nothing else
|
|
1518
|
+
* references, and mark the row set `pruned`.
|
|
1519
|
+
*
|
|
1520
|
+
* A crash after phase 1 leaves a `pruning` row set that the next pass resumes.
|
|
1521
|
+
*/
|
|
1522
|
+
async compactSupersededSnapshots(input) {
|
|
1523
|
+
this.assertCompactionSafeRowBindings();
|
|
1524
|
+
positiveInteger(input.statusRetentionDays, "statusRetentionDays");
|
|
1525
|
+
positiveInteger(input.automatedRecoveryWindowSeconds, "automatedRecoveryWindowSeconds");
|
|
1526
|
+
const limit = input.limit ?? 100;
|
|
1527
|
+
positiveInteger(limit, "limit");
|
|
1528
|
+
const holdDays = input.maxFeedHoldDays ?? 7;
|
|
1529
|
+
positiveInteger(holdDays, "maxFeedHoldDays");
|
|
1530
|
+
const tables = await this.optionalTables();
|
|
1531
|
+
if (!tables.has("adcp_reporting_row_sets")) return { compacted: 0, failed: [], failures: [] };
|
|
1532
|
+
const graceSeconds = Math.max(input.automatedRecoveryWindowSeconds, 3600);
|
|
1533
|
+
const eligible = `
|
|
1534
|
+
FROM adcp_reporting_revisions revision
|
|
1535
|
+
JOIN adcp_reporting_obligations obligation ON obligation.obligation_id = revision.obligation_id
|
|
1536
|
+
JOIN adcp_reporting_row_sets row_set
|
|
1537
|
+
ON row_set.row_set_id = revision.revision_id AND row_set.row_set_kind = 'revision'
|
|
1538
|
+
JOIN LATERAL (
|
|
1539
|
+
SELECT MAX(later.created_at) AS created_at FROM adcp_reporting_revisions later
|
|
1540
|
+
WHERE later.supersedes_revision_id = revision.revision_id
|
|
1541
|
+
) successor ON successor.created_at IS NOT NULL
|
|
1542
|
+
WHERE revision.finality = 'snapshot' AND revision.kind = 'snapshot'
|
|
1543
|
+
AND ($4::text IS NULL OR obligation.account_id = $4)
|
|
1544
|
+
AND ($5::text IS NULL OR revision.revision_id = $5)
|
|
1545
|
+
AND $3::integer > 0
|
|
1546
|
+
AND row_set.row_count > 0
|
|
1547
|
+
AND row_set.rows_state IN ('live', 'unavailable')
|
|
1548
|
+
AND successor.created_at + ($2::bigint * INTERVAL '1 second') <= clock_timestamp()
|
|
1549
|
+
AND NOT EXISTS (
|
|
1550
|
+
SELECT 1 FROM adcp_reporting_revisions current_revision
|
|
1551
|
+
WHERE current_revision.supersedes_revision_id = revision.revision_id
|
|
1552
|
+
AND NOT EXISTS (
|
|
1553
|
+
SELECT 1 FROM adcp_reporting_revisions next_revision
|
|
1554
|
+
WHERE next_revision.supersedes_revision_id = current_revision.revision_id
|
|
1555
|
+
)
|
|
1556
|
+
)
|
|
1557
|
+
AND (obligation.lease_expires_at IS NULL OR obligation.lease_expires_at <= clock_timestamp())
|
|
1558
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1559
|
+
WHERE tombstone.obligation_id = obligation.obligation_id)
|
|
1560
|
+
AND (
|
|
1561
|
+
successor.created_at + ($1::integer * INTERVAL '1 day') <= clock_timestamp()
|
|
1562
|
+
OR (
|
|
1563
|
+
NOT EXISTS (
|
|
1564
|
+
SELECT 1 FROM adcp_reporting_consumer_statuses statement
|
|
1565
|
+
WHERE statement.revision_id = revision.revision_id AND statement.is_current
|
|
1566
|
+
AND COALESCE(statement.data->>'consumer_status', statement.data->'status'->>'consumer_status')
|
|
1567
|
+
IN ('received', 'unreadable', 'content_mismatch')
|
|
1568
|
+
)
|
|
1569
|
+
${tables.has("adcp_reporting_receipts") ? `AND NOT EXISTS (
|
|
1570
|
+
SELECT 1 FROM adcp_reporting_receipts receipt
|
|
1571
|
+
WHERE receipt.receipt_kind = 'revision' AND receipt.subject_id = revision.revision_id
|
|
1572
|
+
)` : ""}
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
${tables.has("adcp_reporting_materializations") ? `AND NOT EXISTS (
|
|
1576
|
+
SELECT 1 FROM adcp_reporting_materializations hold
|
|
1577
|
+
WHERE hold.revision_id = revision.revision_id AND hold.status = 'pending'
|
|
1578
|
+
)` : ""}
|
|
1579
|
+
${tables.has("adcp_reporting_changes") && tables.has("adcp_reporting_feed_consumers") ? `AND NOT EXISTS (
|
|
1580
|
+
SELECT 1 FROM adcp_reporting_changes change
|
|
1581
|
+
JOIN adcp_reporting_feed_consumers consumer
|
|
1582
|
+
ON consumer.updated_at > clock_timestamp() - ($3::integer * INTERVAL '1 day')
|
|
1583
|
+
WHERE change.record_kind = 'revision' AND change.record_id = revision.revision_id
|
|
1584
|
+
AND ((consumer.account_id IS NULL
|
|
1585
|
+
AND (change.xid, change.seq) > (consumer.cursor_xid, consumer.cursor_seq))
|
|
1586
|
+
OR (consumer.account_id = change.account_id AND change.seq > consumer.cursor_seq)))` : ""}`;
|
|
1587
|
+
const baseValues = [input.statusRetentionDays, graceSeconds, holdDays, input.account_id ?? null];
|
|
1588
|
+
const candidates = await this.query(
|
|
1589
|
+
`SELECT revision_id, obligation_id, account_id FROM (
|
|
1590
|
+
SELECT row_set.row_set_id AS revision_id, row_set.obligation_id, row_set.account_id, 0 AS resume,
|
|
1591
|
+
row_set.recorded_at
|
|
1592
|
+
FROM adcp_reporting_row_sets row_set
|
|
1593
|
+
WHERE row_set.row_set_kind = 'revision' AND row_set.rows_state = 'pruning'
|
|
1594
|
+
AND ($4::text IS NULL OR row_set.account_id = $4)
|
|
1595
|
+
AND EXISTS (SELECT 1 FROM adcp_reporting_revisions revision
|
|
1596
|
+
WHERE revision.revision_id = row_set.row_set_id)
|
|
1597
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1598
|
+
WHERE tombstone.obligation_id = row_set.obligation_id)
|
|
1599
|
+
UNION ALL
|
|
1600
|
+
SELECT revision.revision_id, revision.obligation_id, obligation.account_id, 1, row_set.recorded_at
|
|
1601
|
+
${eligible}
|
|
1602
|
+
) candidate
|
|
1603
|
+
ORDER BY resume, recorded_at, revision_id
|
|
1604
|
+
LIMIT $6`,
|
|
1605
|
+
[...baseValues, null, limit]
|
|
1606
|
+
);
|
|
1607
|
+
let compacted = 0;
|
|
1608
|
+
const failed = [];
|
|
1609
|
+
const failures = [];
|
|
1610
|
+
for (const candidate of candidates.rows) {
|
|
1611
|
+
input.signal?.throwIfAborted();
|
|
1612
|
+
try {
|
|
1613
|
+
const done = await this.compactSnapshot(
|
|
1614
|
+
candidate.revision_id,
|
|
1615
|
+
candidate.obligation_id,
|
|
1616
|
+
candidate.account_id,
|
|
1617
|
+
eligible,
|
|
1618
|
+
baseValues,
|
|
1619
|
+
input.signal
|
|
1620
|
+
);
|
|
1621
|
+
if (done) compacted += 1;
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
failed.push(candidate.revision_id);
|
|
1624
|
+
failures.push({
|
|
1625
|
+
reporting_obligation_id: candidate.obligation_id,
|
|
1626
|
+
reporting_revision_id: candidate.revision_id,
|
|
1627
|
+
cause: (0, import_row_storage.isReportingRowStoreError)(error) ? error.code : error instanceof Error ? error.name : "Error"
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return { compacted, failed, failures };
|
|
1632
|
+
}
|
|
1633
|
+
async compactSnapshot(revisionId, obligationId, accountId, eligible, baseValues, signal) {
|
|
1634
|
+
const claimed = await this.transaction(
|
|
1635
|
+
async (client) => {
|
|
1636
|
+
const marked = await client.query(
|
|
1637
|
+
`UPDATE adcp_reporting_row_sets SET rows_state = 'pruning', rows_state_changed_at = clock_timestamp()
|
|
1638
|
+
WHERE row_set_id = $5 AND rows_state IN ('live', 'unavailable')
|
|
1639
|
+
AND row_set_id IN (SELECT revision.revision_id ${eligible})`,
|
|
1640
|
+
[...baseValues, revisionId]
|
|
1641
|
+
);
|
|
1642
|
+
if (marked.rowCount === 1) return true;
|
|
1643
|
+
const resumed = await client.query(
|
|
1644
|
+
`SELECT 1 FROM adcp_reporting_row_sets row_set
|
|
1645
|
+
WHERE row_set.row_set_id = $1 AND row_set.rows_state = 'pruning'
|
|
1646
|
+
AND NOT EXISTS (SELECT 1 FROM adcp_reporting_obligation_tombstones tombstone
|
|
1647
|
+
WHERE tombstone.obligation_id = row_set.obligation_id)`,
|
|
1648
|
+
[revisionId]
|
|
1649
|
+
);
|
|
1650
|
+
return resumed.rows.length === 1;
|
|
1651
|
+
},
|
|
1652
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1653
|
+
);
|
|
1654
|
+
if (!claimed) return false;
|
|
1655
|
+
const binding = await this.query(
|
|
1656
|
+
`SELECT row_binding_id FROM adcp_reporting_row_sets WHERE row_set_id = $1`,
|
|
1657
|
+
[revisionId]
|
|
1658
|
+
);
|
|
1659
|
+
if (binding.rows.length === 0) return false;
|
|
1660
|
+
const objects = await this.query(
|
|
1661
|
+
`SELECT DISTINCT chunk.object_key, chunk.native_version
|
|
1662
|
+
FROM adcp_reporting_row_chunks chunk
|
|
1663
|
+
WHERE chunk.row_set_id = $1 AND chunk.object_key IS NOT NULL`,
|
|
1664
|
+
[revisionId]
|
|
1665
|
+
);
|
|
1666
|
+
if (objects.rows.length > 0) {
|
|
1667
|
+
await this.deleteUnreferencedObjects(
|
|
1668
|
+
binding.rows[0].row_binding_id,
|
|
1669
|
+
objects.rows.map((row) => ({ objectKey: row.object_key, nativeVersion: row.native_version })),
|
|
1670
|
+
{ rowSetId: revisionId },
|
|
1671
|
+
signal
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
await this.transaction(
|
|
1675
|
+
async (client) => {
|
|
1676
|
+
const dropped = await client.query(
|
|
1677
|
+
`DELETE FROM adcp_reporting_row_chunks WHERE row_set_id = $1 RETURNING sha256, object_key`,
|
|
1678
|
+
[revisionId]
|
|
1679
|
+
);
|
|
1680
|
+
const bodies = [...new Set(dropped.rows.filter((row) => row.object_key === null).map((row) => row.sha256))];
|
|
1681
|
+
if (bodies.length > 0) {
|
|
1682
|
+
await client.query(
|
|
1683
|
+
`DELETE FROM adcp_reporting_chunk_bodies body
|
|
1684
|
+
WHERE body.account_id = $1 AND body.obligation_id = $2 AND body.sha256 = ANY($3::text[])
|
|
1685
|
+
AND NOT EXISTS (
|
|
1686
|
+
SELECT 1 FROM adcp_reporting_row_chunks other
|
|
1687
|
+
WHERE other.account_id = body.account_id AND other.obligation_id = body.obligation_id
|
|
1688
|
+
AND other.sha256 = body.sha256 AND other.object_key IS NULL
|
|
1689
|
+
)`,
|
|
1690
|
+
[accountId, obligationId, bodies]
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
await client.query(
|
|
1694
|
+
`UPDATE adcp_reporting_row_sets SET rows_state = 'pruned', rows_state_changed_at = clock_timestamp()
|
|
1695
|
+
WHERE row_set_id = $1 AND rows_state = 'pruning'`,
|
|
1696
|
+
[revisionId]
|
|
1697
|
+
);
|
|
1698
|
+
},
|
|
1699
|
+
{ preBeginAdvisoryLock: accountLock(accountId) }
|
|
1700
|
+
);
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1464
1703
|
/**
|
|
1465
1704
|
* Resolve abandoned object uploads (spec §4.3). An expired `open` write
|
|
1466
1705
|
* intent whose ledger commit never happened is moved to `sweeping` under
|
|
@@ -1502,35 +1741,85 @@ ${managedDueArm} )
|
|
|
1502
1741
|
{ preBeginAdvisoryLock: accountLock(intent.account_id) }
|
|
1503
1742
|
);
|
|
1504
1743
|
if (!claimed) continue;
|
|
1505
|
-
const
|
|
1506
|
-
for (const object of claimed) {
|
|
1507
|
-
const referenced = await this.query(
|
|
1508
|
-
`SELECT 1 FROM adcp_reporting_row_chunks WHERE object_key = $1 AND native_version = $2 LIMIT 1`,
|
|
1509
|
-
[object.objectKey, object.nativeVersion]
|
|
1510
|
-
);
|
|
1511
|
-
if (referenced.rows.length === 0) orphaned.push(object);
|
|
1512
|
-
}
|
|
1513
|
-
if (orphaned.length > 0) await this.rowStorage.deleteObjects(intent.row_binding_id, orphaned, input.signal);
|
|
1744
|
+
const deleted = await this.deleteUnreferencedObjects(intent.row_binding_id, claimed, {}, input.signal);
|
|
1514
1745
|
await this.query(
|
|
1515
1746
|
`DELETE FROM adcp_reporting_row_write_intents
|
|
1516
1747
|
WHERE row_binding_id = $1 AND row_set_id = $2 AND content_sha256 = $3 AND state = 'sweeping'`,
|
|
1517
1748
|
key
|
|
1518
1749
|
);
|
|
1519
1750
|
swept += 1;
|
|
1520
|
-
objectsDeleted +=
|
|
1751
|
+
objectsDeleted += deleted;
|
|
1521
1752
|
} catch {
|
|
1522
1753
|
failed += 1;
|
|
1523
1754
|
}
|
|
1524
1755
|
}
|
|
1525
1756
|
return { swept, objectsDeleted, failed };
|
|
1526
1757
|
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Delete external row objects, skipping any object another row set still references.
|
|
1760
|
+
*
|
|
1761
|
+
* A custom `keyTemplate` need only include `{content_sha256}` and `{chunk_index}`, so row sets
|
|
1762
|
+
* from different obligations or accounts with identical chunk content can share one object (a
|
|
1763
|
+
* create-only write adopts an existing object). An object is therefore deleted only when no
|
|
1764
|
+
* chunk of a live (or unavailable) row set outside this operation references the same binding,
|
|
1765
|
+
* key and native version, and no other open write intent created it. Row sets already `pruning`
|
|
1766
|
+
* or `pruned` do not count: their own operation deletes the object, and counting them would let
|
|
1767
|
+
* two concurrent retirements each skip the object and leak it. `scope` names what this
|
|
1768
|
+
* operation is deleting: an obligation (retirement) or a row set (compaction).
|
|
1769
|
+
*
|
|
1770
|
+
* The check and the provider delete run in one transaction under a per-object advisory lock.
|
|
1771
|
+
* A commit that adopts an existing object takes the same lock before it inserts its chunk rows
|
|
1772
|
+
* and re-verifies the object under it (`writePreparedRowSet`), and holds it until commit. So
|
|
1773
|
+
* either the delete wins the lock and the adopting commit then fails cleanly, or the commit wins
|
|
1774
|
+
* and the delete sees its chunk rows and skips. A skipped object stays for the row set that owns
|
|
1775
|
+
* it: that row set's own retirement, compaction or sweep deletes it, because by then nothing
|
|
1776
|
+
* else references it. Locks are taken one object at a time, never while waiting on the account
|
|
1777
|
+
* lock, so commits (account lock, then object locks) cannot deadlock with these deletes.
|
|
1778
|
+
*/
|
|
1779
|
+
async deleteUnreferencedObjects(bindingId, objects, scope, signal) {
|
|
1780
|
+
let deleted = 0;
|
|
1781
|
+
for (const object of objects) {
|
|
1782
|
+
signal?.throwIfAborted();
|
|
1783
|
+
const removed = await this.transaction(async (client) => {
|
|
1784
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
1785
|
+
objectLock(bindingId, object.objectKey)
|
|
1786
|
+
]);
|
|
1787
|
+
const referenced = await client.query(
|
|
1788
|
+
`(SELECT 1 FROM adcp_reporting_row_chunks chunk
|
|
1789
|
+
JOIN adcp_reporting_row_sets row_set ON row_set.row_set_id = chunk.row_set_id
|
|
1790
|
+
WHERE row_set.row_binding_id = $1 AND chunk.object_key = $2 AND chunk.native_version = $3
|
|
1791
|
+
AND row_set.rows_state IN ('live', 'unavailable')
|
|
1792
|
+
AND ($4::text IS NULL OR chunk.obligation_id <> $4)
|
|
1793
|
+
AND ($5::text IS NULL OR chunk.row_set_id <> $5)
|
|
1794
|
+
LIMIT 1)
|
|
1795
|
+
UNION ALL
|
|
1796
|
+
(SELECT 1 FROM adcp_reporting_row_write_intents intent
|
|
1797
|
+
WHERE intent.row_binding_id = $1 AND intent.state = 'open' AND intent.created_objects @> $6::jsonb
|
|
1798
|
+
LIMIT 1)`,
|
|
1799
|
+
[
|
|
1800
|
+
bindingId,
|
|
1801
|
+
object.objectKey,
|
|
1802
|
+
object.nativeVersion,
|
|
1803
|
+
scope.obligationId ?? null,
|
|
1804
|
+
scope.rowSetId ?? null,
|
|
1805
|
+
JSON.stringify([{ objectKey: object.objectKey, nativeVersion: object.nativeVersion }])
|
|
1806
|
+
]
|
|
1807
|
+
);
|
|
1808
|
+
if (referenced.rows.length > 0) return false;
|
|
1809
|
+
await this.rowStorage.deleteObjects(bindingId, [object], signal);
|
|
1810
|
+
return true;
|
|
1811
|
+
});
|
|
1812
|
+
if (removed) deleted += 1;
|
|
1813
|
+
}
|
|
1814
|
+
return deleted;
|
|
1815
|
+
}
|
|
1527
1816
|
async optionalTables() {
|
|
1528
1817
|
const result = await this.query(
|
|
1529
1818
|
`SELECT table_name AS name FROM information_schema.tables
|
|
1530
1819
|
WHERE table_schema = current_schema()
|
|
1531
1820
|
AND table_name IN ('adcp_reporting_row_sets', 'adcp_reporting_row_write_intents',
|
|
1532
1821
|
'adcp_reporting_materializations', 'adcp_reporting_changes',
|
|
1533
|
-
'adcp_reporting_feed_consumers')`
|
|
1822
|
+
'adcp_reporting_feed_consumers', 'adcp_reporting_receipts')`
|
|
1534
1823
|
);
|
|
1535
1824
|
return new Set(result.rows.map((row) => row.name));
|
|
1536
1825
|
}
|
|
@@ -1634,6 +1923,15 @@ ${managedDueArm} )
|
|
|
1634
1923
|
throw new import_row_storage.ReportingRowStoreError("STATE_UNAVAILABLE", "the row upload intent was taken over or swept");
|
|
1635
1924
|
}
|
|
1636
1925
|
}
|
|
1926
|
+
if (!upload.sharedFrom) {
|
|
1927
|
+
const adopted = upload.locators.filter((locator) => !locator.created).sort((left, right) => left.objectKey < right.objectKey ? -1 : left.objectKey > right.objectKey ? 1 : 0);
|
|
1928
|
+
for (const locator of adopted) {
|
|
1929
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [
|
|
1930
|
+
objectLock(prepared.bindingId, locator.objectKey)
|
|
1931
|
+
]);
|
|
1932
|
+
}
|
|
1933
|
+
if (adopted.length > 0) await this.rowStorage.assertObjectsPresent(prepared.bindingId, adopted);
|
|
1934
|
+
}
|
|
1637
1935
|
await this.rowStorage.writeInTransaction(client, prepared, upload.locators, upload.sharedFrom);
|
|
1638
1936
|
}
|
|
1639
1937
|
/** Attach verified rows to a revision document whose rows live in row storage. */
|
|
@@ -3822,6 +4120,9 @@ function rfc3339Microseconds(expression) {
|
|
|
3822
4120
|
function accountLock(accountId) {
|
|
3823
4121
|
return `adcp-reporting-account:${accountId}`;
|
|
3824
4122
|
}
|
|
4123
|
+
function objectLock(bindingId, objectKey) {
|
|
4124
|
+
return `adcp-reporting-object:${bindingId}:${objectKey}`;
|
|
4125
|
+
}
|
|
3825
4126
|
async function resolveStoredFinalityBaseline(transaction, obligationId) {
|
|
3826
4127
|
const latest = await transaction.query(
|
|
3827
4128
|
`SELECT transition_id, data->>'finality' AS finality
|