@substrat-run/adapter-cloudflare 0.147.0 → 0.149.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/control-plane-do.d.ts +131 -10
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +165 -35
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +75 -16
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +395 -61
- package/dist/host.js.map +1 -1
- package/dist/route-resolver.js +2 -2
- package/dist/route-resolver.js.map +1 -1
- package/dist/scope-do.d.ts +7 -0
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +180 -11
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
package/dist/control-plane-do.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
-
import { membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, MEMBERSHIP_FENCES_TABLE, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink as insertConnectLinkRow, listConnectLinks as listConnectLinkRows, readConnectLink as readConnectLinkRow, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, clearSwitchOwed, markSwitchOwed, scopeOwesSwitch, switchesOwedOf, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, recordSwitchedOff, recordSwitchedOn, recordWriteSuperseded, restoreSwitchRecord, scopesSwitchedOffFor, switchRecordStatesOf, switchRecordsOf, switchesBackfillSqlOf, switchesDdlOf, switchesTableExists, switchesTableOf, tenantHeldOf, VERSION_MIGRATIONS_DDL, splitVersionMigrationsBatch, versionMigrationsOf, versionsAwaitSplit, writeVersionMigrations, MODEL_USAGE_RETENTION_DAYS, DO_SQL_LIMITS, ulid, isPrimaryScopeRow, samePeerVertical, peerVerticalSpellings, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, LEGACY_SCOPE_ROWS_BACKFILL, loadDirectoryDump, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, unknownOutcomeOf, ADMIN_LOG_INDEX_DDL, ADMIN_LOG_INDEXES_SQL, readAuditedOperations, } from '@substrat-run/kernel';
|
|
2
|
+
import { membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, MEMBERSHIP_FENCES_TABLE, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink as insertConnectLinkRow, listConnectLinks as listConnectLinkRows, readConnectLink as readConnectLinkRow, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, clearSwitchOwed, markSwitchOwed, scopeOwesSwitch, switchesOwedOf, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, recordSwitchedOff, recordSwitchedOn, recordWriteSuperseded, restoreSwitchRecord, scopesSwitchedOffFor, switchRecordStatesOf, switchRecordsOf, switchesBackfillSqlOf, switchesDdlOf, switchesTableExists, switchesTableOf, tenantHeldOf, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, STORE_DRIVER_RELEASES_DDL, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, driverDeclarationsOf, recordStoreDriverReleaseIn, releaseStoreDriverHoldIn, storeDriverReleaseIn, storeDriverStateIn, storeDriverRolloutPendingIn, storeDriverRolloutWalkedIn, forgetVerticalScopeStores, indexVersionScopeStores, verticalScopeStoresOf, splitVersionMigrationsBatch, versionMigrationsOf, versionsAwaitSplit, versionsAwaitDigest, deriveMigrationDigestsBatch, deriveVersionMigrationDigest, writeVersionMigrations, MODEL_USAGE_RETENTION_DAYS, DO_SQL_LIMITS, ulid, isPrimaryScopeRow, samePeerVertical, peerVerticalSpellings, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, LEGACY_SCOPE_ROWS_BACKFILL, loadDirectoryDump, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, unknownOutcomeOf, ADMIN_LOG_INDEX_DDL, ADMIN_LOG_INDEXES_SQL, readAuditedOperations, } from '@substrat-run/kernel';
|
|
3
3
|
import { replyOf } from './do-reply.js';
|
|
4
4
|
import { switchSqlOver } from './scope-do.js';
|
|
5
5
|
import { blankSqlComments, boundShapesColumn, executableSqlStatements, installsReconciledOf, PROVISIONED_VERSIONS_SQL, provisionedVersionsParams, } from '@substrat-run/kernel';
|
|
6
|
-
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, } from '@substrat-run/kernel';
|
|
6
|
+
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, SUBJECT_ERASURE_CLAIM_SQL, subjectErasureClaimParams, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, } from '@substrat-run/kernel';
|
|
7
7
|
import { doBuiltColumnsOf, doRedactionSql } from './sql.js';
|
|
8
8
|
import { assertReplayableDump, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
|
|
9
9
|
/**
|
|
@@ -247,6 +247,8 @@ const DIRECTORY_DDL = `
|
|
|
247
247
|
created_at TEXT NOT NULL,
|
|
248
248
|
PRIMARY KEY (tenant_id, vertical, binding)
|
|
249
249
|
);
|
|
250
|
+
-- A store driver's stored releases, rollouts and holds (D-63, #2300).
|
|
251
|
+
${STORE_DRIVER_RELEASES_DDL}
|
|
250
252
|
CREATE TABLE IF NOT EXISTS verticals (
|
|
251
253
|
slug TEXT PRIMARY KEY,
|
|
252
254
|
name TEXT NOT NULL,
|
|
@@ -305,10 +307,14 @@ const DIRECTORY_DDL = `
|
|
|
305
307
|
migration_count INTEGER,
|
|
306
308
|
-- 1 once the SQL is out of manifest_json. NULL = stored before #1764, not yet backfilled.
|
|
307
309
|
migrations_split INTEGER,
|
|
310
|
+
-- 1 once migration_digest is the one derived from the stored migrations (#2339).
|
|
311
|
+
-- NULL = stored before, still the pushed digest; backfilled, and derived on read until then.
|
|
312
|
+
migration_digest_derived INTEGER,
|
|
308
313
|
created_at TEXT NOT NULL,
|
|
309
314
|
UNIQUE (vertical_slug, version)
|
|
310
315
|
);
|
|
311
316
|
${VERSION_MIGRATIONS_DDL}
|
|
317
|
+
${VERTICAL_SCOPE_STORES_DDL}
|
|
312
318
|
CREATE TABLE IF NOT EXISTS vertical_channels (
|
|
313
319
|
vertical_slug TEXT NOT NULL,
|
|
314
320
|
channel TEXT NOT NULL,
|
|
@@ -776,6 +782,22 @@ export const REPLIED_METHODS = [
|
|
|
776
782
|
'linkIdentity',
|
|
777
783
|
// #1748: refuses an expiry outside its bounds with validation_failed.
|
|
778
784
|
'createFindingRule',
|
|
785
|
+
// #2286: refuses a scope-store binding moved to another driver with conflict, and an unreadable manifest with unavailable.
|
|
786
|
+
'insertVersion',
|
|
787
|
+
'verticalScopeStores',
|
|
788
|
+
// #2300: a store driver's release, rollout and hold writes refuse with conflict, not_found,
|
|
789
|
+
// validation_failed or unavailable.
|
|
790
|
+
'recordStoreDriverRelease',
|
|
791
|
+
'storeDriverDeclarations',
|
|
792
|
+
// #2312: a rollout page refuses a limit that is not positive, and an unknown rollout.
|
|
793
|
+
'storeDriverRolloutPending',
|
|
794
|
+
'storeDriverRolloutWalked',
|
|
795
|
+
'holdStoreDriver',
|
|
796
|
+
'beginStoreDriverRollout',
|
|
797
|
+
'finishStoreDriverRollout',
|
|
798
|
+
// #2309: a fleet rebuild's lease write refuses with conflict or validation_failed.
|
|
799
|
+
'claimStoreDriverRebuild',
|
|
800
|
+
'advanceStoreDriverRebuild',
|
|
779
801
|
];
|
|
780
802
|
export class ControlPlaneDO extends DurableObject {
|
|
781
803
|
sql;
|
|
@@ -800,7 +822,7 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
800
822
|
* instead (`importDump`).
|
|
801
823
|
*/
|
|
802
824
|
async armVersionMigrationsBackfill() {
|
|
803
|
-
if (!versionsAwaitSplit(this.kernelSql))
|
|
825
|
+
if (!versionsAwaitSplit(this.kernelSql) && !versionsAwaitDigest(this.kernelSql))
|
|
804
826
|
return;
|
|
805
827
|
// An alarm already set is kept: it is the backfill's own next run, and a backoff retry
|
|
806
828
|
// pulled forward to 1 s by any ordinary request would make the backoff meaningless.
|
|
@@ -821,6 +843,9 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
821
843
|
let more;
|
|
822
844
|
try {
|
|
823
845
|
more = this.ctx.storage.transactionSync(() => splitVersionMigrationsBatch(this.kernelSql)).more;
|
|
846
|
+
// #2339: then re-derive the migration digests of versions stored before the server did.
|
|
847
|
+
// Not in the transaction: the digest is computed between its read and its guarded write.
|
|
848
|
+
more = (await deriveMigrationDigestsBatch(this.kernelSql)).more || more;
|
|
824
849
|
}
|
|
825
850
|
catch (err) {
|
|
826
851
|
const failures = ((await this.ctx.storage.get(BACKFILL_FAILURES_KEY)) ?? 0) + 1;
|
|
@@ -895,6 +920,18 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
895
920
|
* creates each table with its backfill; `holdFences` does the same for #1184's removal fence.
|
|
896
921
|
*/
|
|
897
922
|
buildDirectorySchema({ holdSwitchRecords, holdFences = false }) {
|
|
923
|
+
// A directory built before the store driver rename keeps its rows under the old names (and one
|
|
924
|
+
// that ran pre-rename code again, a rollback, holds both names), and
|
|
925
|
+
// the DDL below would only create empty tables beside them. Probed, not attempted: an empty
|
|
926
|
+
// `SELECT` reads the columns here, and an emptied directory (a restore) has nothing to move.
|
|
927
|
+
const tables = this.sql.exec(`SELECT name FROM sqlite_master WHERE type = 'table'`).toArray().map((t) => t.name);
|
|
928
|
+
const hasRows = (table) => this.sql.exec(`SELECT 1 FROM "${table}" LIMIT 1`).toArray().length > 0;
|
|
929
|
+
// Asked before the moves below, which drop a name with no rows.
|
|
930
|
+
const bothNames = legacyStoreDriverConflict(tables, hasRows);
|
|
931
|
+
if (bothNames)
|
|
932
|
+
console.error(bothNames);
|
|
933
|
+
for (const stmt of legacyStoreDriverRenames(tables, (table) => doBuiltColumnsOf(this.sql, table), hasRows))
|
|
934
|
+
this.sql.exec(stmt);
|
|
898
935
|
const held = holdSwitchRecords.map(switchesTableOf);
|
|
899
936
|
for (const stmt of DIRECTORY_DDL_PLAN.loop) {
|
|
900
937
|
if (held.some((table) => stmt.includes(table)))
|
|
@@ -907,6 +944,8 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
907
944
|
// #1764's, held back by `planDirectoryDdl`: its index names a column added just above.
|
|
908
945
|
for (const stmt of DIRECTORY_DDL_PLAN.afterColumns)
|
|
909
946
|
this.sql.exec(stmt);
|
|
947
|
+
// #2322: a migration's declared moves and discards, on a table created before they were carried.
|
|
948
|
+
this.addColumn('vertical_version_migrations', 'declarations TEXT');
|
|
910
949
|
}
|
|
911
950
|
/**
|
|
912
951
|
* The directory's own migration path (control-plane.md §7). A singleton DO that
|
|
@@ -1124,6 +1163,8 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1124
1163
|
// #1764: the SQL migrations moved out of the manifest, and the backfill's progress.
|
|
1125
1164
|
this.addColumn('vertical_versions', 'migration_count INTEGER');
|
|
1126
1165
|
this.addColumn('vertical_versions', 'migrations_split INTEGER');
|
|
1166
|
+
// #2339: whether the migration digest is the derived one; NULL rows are backfilled.
|
|
1167
|
+
this.addColumn('vertical_versions', 'migration_digest_derived INTEGER');
|
|
1127
1168
|
// #33: the SKU flag learns to express a plan. All nullable — a legacy row
|
|
1128
1169
|
// reads as a perpetual boolean flag, exactly its pre-widening semantics.
|
|
1129
1170
|
this.addColumn('_substrat_entitlements', 'expires_at TEXT');
|
|
@@ -1246,8 +1287,9 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1246
1287
|
// next construction arms the backfill, since it finds none set.
|
|
1247
1288
|
try {
|
|
1248
1289
|
await this.ctx.storage.delete(BACKFILL_FAILURES_KEY);
|
|
1249
|
-
if (versionsAwaitSplit(this.kernelSql))
|
|
1290
|
+
if (versionsAwaitSplit(this.kernelSql) || versionsAwaitDigest(this.kernelSql)) {
|
|
1250
1291
|
await this.ctx.storage.setAlarm(Date.now() + BACKFILL_PAUSE_MS);
|
|
1292
|
+
}
|
|
1251
1293
|
}
|
|
1252
1294
|
catch (err) {
|
|
1253
1295
|
console.error('substrat: the restore committed, but its version-migrations backfill was not armed', err);
|
|
@@ -1470,6 +1512,48 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1470
1512
|
' ORDER BY tenant_id, vertical, binding', ...params)
|
|
1471
1513
|
.toArray();
|
|
1472
1514
|
}
|
|
1515
|
+
// -- store-driver releases, rollouts and holds (D-63, #2300) ---------------------
|
|
1516
|
+
// Kernel code over the directory (store-driver-releases.ts), shared with the SQLite host. Each
|
|
1517
|
+
// call is one transaction, and the object runs one at a time, so a hold is taken atomically.
|
|
1518
|
+
recordStoreDriverRelease(input) {
|
|
1519
|
+
return this.ctx.storage.transactionSync(() => recordStoreDriverReleaseIn(this.kernelSql, input));
|
|
1520
|
+
}
|
|
1521
|
+
getStoreDriverRelease(driver, sha256) {
|
|
1522
|
+
return storeDriverReleaseIn(this.kernelSql, driver, sha256);
|
|
1523
|
+
}
|
|
1524
|
+
storeDriverState(driver) {
|
|
1525
|
+
return this.ctx.storage.transactionSync(() => storeDriverStateIn(this.kernelSql, driver, Date.now()));
|
|
1526
|
+
}
|
|
1527
|
+
storeDriverDeclarations(driver) {
|
|
1528
|
+
return this.ctx.storage.transactionSync(() => driverDeclarationsOf(this.kernelSql, driver));
|
|
1529
|
+
}
|
|
1530
|
+
storeDriverRolloutPending(input) {
|
|
1531
|
+
return this.ctx.storage.transactionSync(() => storeDriverRolloutPendingIn(this.kernelSql, input));
|
|
1532
|
+
}
|
|
1533
|
+
storeDriverRolloutWalked(input) {
|
|
1534
|
+
return this.ctx.storage.transactionSync(() => storeDriverRolloutWalkedIn(this.kernelSql, input));
|
|
1535
|
+
}
|
|
1536
|
+
holdStoreDriver(input) {
|
|
1537
|
+
return this.ctx.storage.transactionSync(() => holdStoreDriverIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1538
|
+
}
|
|
1539
|
+
releaseStoreDriverHold(input) {
|
|
1540
|
+
releaseStoreDriverHoldIn(this.kernelSql, input);
|
|
1541
|
+
}
|
|
1542
|
+
beginStoreDriverRollout(input) {
|
|
1543
|
+
return this.ctx.storage.transactionSync(() => beginStoreDriverRolloutIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1544
|
+
}
|
|
1545
|
+
finishStoreDriverRollout(input) {
|
|
1546
|
+
return this.ctx.storage.transactionSync(() => finishStoreDriverRolloutIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1547
|
+
}
|
|
1548
|
+
storeDriverRebuild(driver) {
|
|
1549
|
+
return storeDriverRebuildIn(this.kernelSql, driver);
|
|
1550
|
+
}
|
|
1551
|
+
claimStoreDriverRebuild(input) {
|
|
1552
|
+
return this.ctx.storage.transactionSync(() => claimStoreDriverRebuildIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1553
|
+
}
|
|
1554
|
+
advanceStoreDriverRebuild(input) {
|
|
1555
|
+
return this.ctx.storage.transactionSync(() => advanceStoreDriverRebuildIn(this.kernelSql, { ...input, nowMs: Date.now() }));
|
|
1556
|
+
}
|
|
1473
1557
|
// -- scope lifecycle (control-plane.md §4.1/§4.2) ---------------------------
|
|
1474
1558
|
/**
|
|
1475
1559
|
* Mandatory active tenant: a scope with no tenant record is the "tenant is an
|
|
@@ -2020,8 +2104,14 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2020
2104
|
this.sql.exec(`UPDATE hostnames SET status = ?, status_note = ?, validation_records = ? WHERE hostname = ?`, fields.status, fields.note, fields.validationRecords, hostname);
|
|
2021
2105
|
}
|
|
2022
2106
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2107
|
+
/**
|
|
2108
|
+
* Delete a binding and answer the row it removed, or null. With `owner`, only while the row
|
|
2109
|
+
* still names that tenant and scope (#2285): the delete decides it, not an earlier read.
|
|
2110
|
+
*/
|
|
2111
|
+
deleteHostname(hostname, owner) {
|
|
2112
|
+
const rows = this.sql.exec(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
|
|
2113
|
+
RETURNING tenant_id, scope_id, status`, hostname, ...(owner ? [owner.tenantId, owner.scopeId] : [])).toArray();
|
|
2114
|
+
return rows[0] ?? null;
|
|
2025
2115
|
}
|
|
2026
2116
|
listHostnames(filter) {
|
|
2027
2117
|
const where = [];
|
|
@@ -2112,6 +2202,7 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2112
2202
|
this.sql.exec('DELETE FROM vertical_channel_history WHERE vertical_slug = ?', slug);
|
|
2113
2203
|
this.sql.exec(`DELETE FROM vertical_version_migrations
|
|
2114
2204
|
WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`, slug);
|
|
2205
|
+
forgetVerticalScopeStores(this.kernelSql, slug);
|
|
2115
2206
|
this.sql.exec('DELETE FROM vertical_versions WHERE vertical_slug = ?', slug);
|
|
2116
2207
|
this.sql.exec('DELETE FROM verticals WHERE slug = ?', slug);
|
|
2117
2208
|
}
|
|
@@ -2122,10 +2213,17 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2122
2213
|
const sql = 'SELECT * FROM verticals' + (where.length ? ` WHERE ${where.join(' AND ')}` : '') + tail;
|
|
2123
2214
|
return this.sql.exec(sql, ...params).toArray();
|
|
2124
2215
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2216
|
+
/**
|
|
2217
|
+
* One version. A version stored before the server derived migration digests (#2339) is derived
|
|
2218
|
+
* first, so a gate never compares the digest its push sent while the backfill has not reached it.
|
|
2219
|
+
*/
|
|
2220
|
+
async readVersion(id) {
|
|
2221
|
+
const read = () => this.sql.exec('SELECT * FROM vertical_versions WHERE id = ?', id).toArray()[0];
|
|
2222
|
+
const row = read();
|
|
2223
|
+
if (row?.migration_digest_derived !== null)
|
|
2224
|
+
return row;
|
|
2225
|
+
await deriveVersionMigrationDigest(this.kernelSql, id);
|
|
2226
|
+
return read();
|
|
2129
2227
|
}
|
|
2130
2228
|
/**
|
|
2131
2229
|
* A version and its SQL migrations (#1764), in one transaction. `manifestJson` arrives with
|
|
@@ -2137,11 +2235,17 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2137
2235
|
this.sql.exec(`INSERT INTO vertical_versions
|
|
2138
2236
|
(id, vertical_slug, version, manifest_digest, permission_digest,
|
|
2139
2237
|
migration_digest, deployment_ref, admission, admission_note, manifest_json,
|
|
2140
|
-
origin_json, migration_count, migrations_split, created_at)
|
|
2141
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`, v.id, v.verticalSlug, v.version, v.manifestDigest, v.permissionDigest, v.migrationDigest, v.deploymentRef, v.admission, v.admissionNote, v.manifestJson, v.originJson, v.migrations?.length ?? null, v.createdAt);
|
|
2238
|
+
origin_json, migration_count, migrations_split, migration_digest_derived, created_at)
|
|
2239
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?)`, v.id, v.verticalSlug, v.version, v.manifestDigest, v.permissionDigest, v.migrationDigest, v.deploymentRef, v.admission, v.admissionNote, v.manifestJson, v.originJson, v.migrations?.length ?? null, v.createdAt);
|
|
2142
2240
|
writeVersionMigrations(this.kernelSql, v.id, v.migrations);
|
|
2241
|
+
// #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
|
|
2242
|
+
indexVersionScopeStores(this.kernelSql, v.verticalSlug, v.id, v.manifestJson);
|
|
2143
2243
|
});
|
|
2144
2244
|
}
|
|
2245
|
+
/** Every scope-store binding any version of the vertical declared (#2286); indexes versions not indexed yet. */
|
|
2246
|
+
verticalScopeStores(slug) {
|
|
2247
|
+
return this.ctx.storage.transactionSync(() => verticalScopeStoresOf(this.kernelSql, slug));
|
|
2248
|
+
}
|
|
2145
2249
|
/** One version's SQL migrations (#1764), or `undefined` for no such version. */
|
|
2146
2250
|
readVersionMigrations(id) {
|
|
2147
2251
|
return versionMigrationsOf(this.kernelSql, id);
|
|
@@ -2162,27 +2266,45 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2162
2266
|
setScopeExpiresAt(scopeId, expiresAt) {
|
|
2163
2267
|
this.sql.exec('UPDATE scopes SET expires_at = ? WHERE scope_id = ?', expiresAt, scopeId);
|
|
2164
2268
|
}
|
|
2165
|
-
listVersions(verticalSlug, page) {
|
|
2269
|
+
async listVersions(verticalSlug, page) {
|
|
2166
2270
|
const where = ['vertical_slug = ?'];
|
|
2167
2271
|
const params = [verticalSlug];
|
|
2168
2272
|
const tail = keysetTail(where, params, 'id', page);
|
|
2169
2273
|
// Named columns, never `manifest_json` itself. `json_valid` first: a manifest that is not
|
|
2170
2274
|
// JSON lists with null surfaces, as `outboundOfManifestJson` reads it, rather than
|
|
2171
2275
|
// failing the whole page on `->`.
|
|
2172
|
-
|
|
2276
|
+
const read = () => this.sql
|
|
2173
2277
|
.exec(`SELECT id, vertical_slug, version, manifest_digest, permission_digest, migration_digest,
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2278
|
+
migration_digest_derived, deployment_ref, admission, admission_note, origin_json, created_at,
|
|
2279
|
+
CASE WHEN json_valid(manifest_json) THEN manifest_json -> '$.outbound' END AS outbound_json,
|
|
2280
|
+
CASE WHEN json_valid(manifest_json) THEN manifest_json -> '$.calls' END AS calls_json
|
|
2281
|
+
FROM vertical_versions WHERE ${where.join(' AND ')}${tail}`, ...params)
|
|
2178
2282
|
.toArray();
|
|
2283
|
+
const rows = read();
|
|
2284
|
+
// #2339: a page holding versions whose digest is still the pushed one derives them first.
|
|
2285
|
+
const underived = rows.filter((r) => r.migration_digest_derived === null);
|
|
2286
|
+
if (underived.length === 0)
|
|
2287
|
+
return rows;
|
|
2288
|
+
for (const r of underived)
|
|
2289
|
+
await deriveVersionMigrationDigest(this.kernelSql, r.id);
|
|
2290
|
+
return read();
|
|
2179
2291
|
}
|
|
2180
2292
|
setAdmission(id, admission, note) {
|
|
2181
2293
|
this.sql.exec('UPDATE vertical_versions SET admission = ?, admission_note = ? WHERE id = ?', admission, note, id);
|
|
2182
2294
|
}
|
|
2183
|
-
/**
|
|
2184
|
-
|
|
2185
|
-
|
|
2295
|
+
/**
|
|
2296
|
+
* False when a conditional bind lost its compare-and-set (the host throws, as above). #2347:
|
|
2297
|
+
* `servingRef` moves the route in the same write, and `expectedServing` holds it to what the
|
|
2298
|
+
* target lineage's serving script runs, read in this same synchronous transaction.
|
|
2299
|
+
*/
|
|
2300
|
+
bindScopeVersion(scopeId, versionId, verticalSlug, expectedVersionId, expectedErasureEpoch, confirmMove, servingRef, expectedServing) {
|
|
2301
|
+
return servingRef === undefined
|
|
2302
|
+
? this.moveScopeRow('vertical_version_id = ?, vertical = ?', [versionId, verticalSlug], scopeId, { expectedVersionId, expectedErasureEpoch, confirmMove, expectedServing })
|
|
2303
|
+
: this.moveScopeRow('vertical_version_id = ?, vertical = ?, serving_ref = ?', [versionId, verticalSlug, servingRef], scopeId, { expectedVersionId, expectedErasureEpoch, confirmMove, expectedServing });
|
|
2304
|
+
}
|
|
2305
|
+
/** #2347: whether a lineage's serving script still runs the version a rebind was judged against. */
|
|
2306
|
+
servesNow(expected) {
|
|
2307
|
+
return this.sql.exec(`SELECT 1 FROM verticals WHERE ${SERVES_NOW_CONDITION}`, ...servesNowParams(expected)).toArray().length > 0;
|
|
2186
2308
|
}
|
|
2187
2309
|
/**
|
|
2188
2310
|
* Move a scope's binding or route (#1722), conditionally: on the binding the caller read, on
|
|
@@ -2196,7 +2318,8 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2196
2318
|
const moved = this.sql.exec(`UPDATE scopes SET ${set} WHERE scope_id = ?
|
|
2197
2319
|
${cond.expectedVersionId === undefined ? '' : 'AND vertical_version_id IS ?'}
|
|
2198
2320
|
${cond.expectedErasureEpoch === undefined ? '' : 'AND COALESCE(erasure_epoch, 0) = ? AND reap_claimed_at IS NULL'}
|
|
2199
|
-
${cond.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}
|
|
2321
|
+
${cond.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}
|
|
2322
|
+
${cond.expectedServing === undefined ? '' : SERVES_NOW_PREDICATE}`, ...setArgs, scopeId, ...(cond.expectedVersionId === undefined ? [] : [cond.expectedVersionId]), ...(cond.expectedErasureEpoch === undefined ? [] : [cond.expectedErasureEpoch]), ...(cond.confirmMove === undefined ? [] : copyMoveLiveParams(cond.confirmMove, now)), ...(cond.expectedServing === undefined ? [] : servesNowParams(cond.expectedServing)));
|
|
2200
2323
|
if (moved.rowsWritten === 0)
|
|
2201
2324
|
return false;
|
|
2202
2325
|
if (cond.confirmMove) {
|
|
@@ -2215,15 +2338,9 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2215
2338
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2216
2339
|
return row.erasure_epoch;
|
|
2217
2340
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
|
|
2222
|
-
AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
|
|
2223
|
-
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
|
|
2224
|
-
AND copy.state = 'pending')
|
|
2225
|
-
AND (SELECT COUNT(*) FROM scope_script_copies AS copy
|
|
2226
|
-
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id) = ?`, tenantId, scopeId, expectedVersionId, expectedServingRef, expectedEpoch, expectedCopyCount).rowsWritten > 0;
|
|
2341
|
+
/** The erasure claim (`SUBJECT_ERASURE_CLAIM_SQL`): false when the scope moved since the fence was read. */
|
|
2342
|
+
claimSubjectErasure(tenantId, scopeId, fence) {
|
|
2343
|
+
return this.sql.exec(SUBJECT_ERASURE_CLAIM_SQL, ...subjectErasureClaimParams(tenantId, scopeId, fence)).rowsWritten > 0;
|
|
2227
2344
|
}
|
|
2228
2345
|
recordScopeScriptCopy(tenantId, scopeId, scriptRef, moveId, role = null, loadStamp = null, leaseMs = SCOPE_COPY_LEASE_MS) {
|
|
2229
2346
|
if (!scriptRef || !moveId)
|
|
@@ -2376,10 +2493,23 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2376
2493
|
.exec('SELECT * FROM vertical_channels WHERE vertical_slug = ? AND channel = ?', verticalSlug, channel)
|
|
2377
2494
|
.toArray()[0];
|
|
2378
2495
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2496
|
+
/**
|
|
2497
|
+
* #2333: move a channel's pointer, and record the move, only if it still names `from` (null: the
|
|
2498
|
+
* channel has none yet). A compare-and-set in one synchronous transaction, so no other promote
|
|
2499
|
+
* runs between the read and the write: a promote judged its gates against `from` across awaits,
|
|
2500
|
+
* and the one that lost the race is told so (`false`) and writes nothing.
|
|
2501
|
+
*/
|
|
2502
|
+
moveChannel(from, history) {
|
|
2503
|
+
return this.ctx.storage.transactionSync(() => {
|
|
2504
|
+
const now = this.readChannel(history.vertical_slug, history.channel);
|
|
2505
|
+
if ((now?.version_id ?? null) !== from)
|
|
2506
|
+
return false;
|
|
2507
|
+
this.sql.exec(`INSERT INTO vertical_channels (vertical_slug, channel, version_id, updated_at)
|
|
2508
|
+
VALUES (?, ?, ?, ?)
|
|
2509
|
+
ON CONFLICT (vertical_slug, channel) DO UPDATE SET version_id = ?, updated_at = ?`, history.vertical_slug, history.channel, history.version_id, history.at, history.version_id, history.at);
|
|
2510
|
+
this.insertChannelHistory(history);
|
|
2511
|
+
return true;
|
|
2512
|
+
});
|
|
2383
2513
|
}
|
|
2384
2514
|
listChannels(verticalSlug, page) {
|
|
2385
2515
|
const where = ['vertical_slug = ?'];
|