deepline 0.1.212 → 0.1.213
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/bundling-sources/apps/play-runner-workers/src/entry.ts +167 -329
- package/dist/bundling-sources/sdk/src/index.ts +2 -0
- package/dist/bundling-sources/sdk/src/play.ts +8 -1
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -1
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +49 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
- package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +142 -101
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +30 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +160 -2
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +54 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +477 -85
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
- package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
- package/dist/index.d.mts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/package.json +1 -1
|
@@ -105,7 +105,13 @@ import {
|
|
|
105
105
|
PLAY_RUNTIME_TEST_FAULT_HEADER,
|
|
106
106
|
type RuntimeTestFaultName,
|
|
107
107
|
} from './test-runtime-seams';
|
|
108
|
+
import { COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID } from './durable-receipt-execution';
|
|
108
109
|
import { vercelProtectionBypassHeader } from './vercel-protection';
|
|
110
|
+
import {
|
|
111
|
+
RuntimePostgresAdmission,
|
|
112
|
+
type RuntimePostgresAdmissionSnapshot,
|
|
113
|
+
type RuntimePostgresLane,
|
|
114
|
+
} from './runtime-postgres-admission';
|
|
109
115
|
|
|
110
116
|
type RuntimeApiContext = {
|
|
111
117
|
baseUrl?: string | null;
|
|
@@ -269,6 +275,7 @@ type RuntimeSheetPreparedRowDisposition = {
|
|
|
269
275
|
const dbSessionCache = new Map<string, DbSessionCacheEntry>();
|
|
270
276
|
const dbSessionInFlight = new Map<string, Promise<CreateDbSessionResponse>>();
|
|
271
277
|
const postgresPools = new Map<string, RuntimePool>();
|
|
278
|
+
const runtimePostgresAdmissions = new Map<string, RuntimePostgresAdmission>();
|
|
272
279
|
const runtimeWorkReceiptEnsureCache = new Map<string, Promise<void>>();
|
|
273
280
|
const runtimeSheetEnsureCache = new Map<
|
|
274
281
|
string,
|
|
@@ -327,7 +334,14 @@ const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS = 10_000;
|
|
|
327
334
|
// Daytona executes one play per runner process. This is the bounded data-plane
|
|
328
335
|
// budget for that one run's sheet reads/writes, receipts, and terminal/export
|
|
329
336
|
// reads over the PgBouncer URL; it is not a scheduler/control-plane fan-out.
|
|
330
|
-
|
|
337
|
+
// One runner gets four tenant-gateway clients, split evenly so a large sheet
|
|
338
|
+
// flush cannot starve receipt claims (and receipt traffic cannot block the
|
|
339
|
+
// persistence barrier). The admission queues add a driver-independent bound:
|
|
340
|
+
// Neon Pool and node-postgres may queue internally, but runtime callers never
|
|
341
|
+
// enter those unobservable queues without first holding a lane permit.
|
|
342
|
+
const RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE = 2;
|
|
343
|
+
const RUNTIME_POSTGRES_ADMISSION_MAX_QUEUED_PER_LANE = 2_000;
|
|
344
|
+
const RUNTIME_POSTGRES_ADMISSION_TIMEOUT_MS = 10_000;
|
|
331
345
|
const RECEIPT_STATUS_QUEUED = RECEIPT_STATUS_CODE.queued;
|
|
332
346
|
const RECEIPT_STATUS_PENDING = RECEIPT_STATUS_CODE.pending;
|
|
333
347
|
const RECEIPT_STATUS_RUNNING = RECEIPT_STATUS_CODE.running;
|
|
@@ -369,6 +383,7 @@ export type PrepareRuntimeSheetResult = {
|
|
|
369
383
|
attemptOwnerRunId?: string;
|
|
370
384
|
attemptExpiresAt?: string;
|
|
371
385
|
attemptSeq?: number;
|
|
386
|
+
writeVersion?: number;
|
|
372
387
|
};
|
|
373
388
|
|
|
374
389
|
export type RuntimeSheetTiming = {
|
|
@@ -1292,9 +1307,7 @@ async function deleteRuntimeDbSessionCacheEntry(
|
|
|
1292
1307
|
): Promise<void> {
|
|
1293
1308
|
dbSessionCache.delete(cacheKey);
|
|
1294
1309
|
if (session.postgresUrl) {
|
|
1295
|
-
|
|
1296
|
-
postgresPools.delete(session.postgresUrl);
|
|
1297
|
-
await pool?.end().catch(() => {});
|
|
1310
|
+
await resetRuntimePostgresPool(session.postgresUrl);
|
|
1298
1311
|
}
|
|
1299
1312
|
}
|
|
1300
1313
|
|
|
@@ -1415,18 +1428,10 @@ async function prewarmRuntimePostgresSession(
|
|
|
1415
1428
|
attempt >= RUNTIME_POSTGRES_PREWARM_MAX_ATTEMPTS ||
|
|
1416
1429
|
!isTransientRuntimePostgresConnectionError(error)
|
|
1417
1430
|
) {
|
|
1418
|
-
|
|
1419
|
-
postgresPools.delete(session.postgresUrl);
|
|
1420
|
-
if (pool) {
|
|
1421
|
-
await Promise.resolve(pool.end()).catch(() => {});
|
|
1422
|
-
}
|
|
1431
|
+
await resetRuntimePostgresPool(session.postgresUrl);
|
|
1423
1432
|
throw error;
|
|
1424
1433
|
}
|
|
1425
|
-
|
|
1426
|
-
postgresPools.delete(session.postgresUrl);
|
|
1427
|
-
if (pool) {
|
|
1428
|
-
await Promise.resolve(pool.end()).catch(() => {});
|
|
1429
|
-
}
|
|
1434
|
+
await resetRuntimePostgresPool(session.postgresUrl);
|
|
1430
1435
|
await sleep(
|
|
1431
1436
|
RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS[attempt - 1] ??
|
|
1432
1437
|
RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS[
|
|
@@ -1491,7 +1496,10 @@ function sleep(ms: number): Promise<void> {
|
|
|
1491
1496
|
|
|
1492
1497
|
async function connectRuntimePostgresPool(
|
|
1493
1498
|
pool: RuntimePool,
|
|
1499
|
+
admission: RuntimePostgresAdmission,
|
|
1500
|
+
lane: RuntimePostgresLane,
|
|
1494
1501
|
): Promise<RuntimePoolClient> {
|
|
1502
|
+
const releaseAdmission = await admission.acquire(lane);
|
|
1495
1503
|
let timedOut = false;
|
|
1496
1504
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
1497
1505
|
const connectPromise = pool.connect();
|
|
@@ -1499,11 +1507,12 @@ async function connectRuntimePostgresPool(
|
|
|
1499
1507
|
.then((client) => {
|
|
1500
1508
|
if (timedOut) {
|
|
1501
1509
|
client.release();
|
|
1510
|
+
releaseAdmission();
|
|
1502
1511
|
}
|
|
1503
1512
|
})
|
|
1504
1513
|
.catch(() => {});
|
|
1505
1514
|
try {
|
|
1506
|
-
|
|
1515
|
+
const client = await Promise.race([
|
|
1507
1516
|
connectPromise,
|
|
1508
1517
|
new Promise<never>((_, reject) => {
|
|
1509
1518
|
timeout = setTimeout(() => {
|
|
@@ -1516,6 +1525,21 @@ async function connectRuntimePostgresPool(
|
|
|
1516
1525
|
}, RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS);
|
|
1517
1526
|
}),
|
|
1518
1527
|
]);
|
|
1528
|
+
let released = false;
|
|
1529
|
+
return {
|
|
1530
|
+
query: (text, params) => client.query(text, params),
|
|
1531
|
+
release: () => {
|
|
1532
|
+
if (released) return;
|
|
1533
|
+
released = true;
|
|
1534
|
+
client.release();
|
|
1535
|
+
releaseAdmission();
|
|
1536
|
+
},
|
|
1537
|
+
};
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
if (!timedOut) {
|
|
1540
|
+
releaseAdmission();
|
|
1541
|
+
}
|
|
1542
|
+
throw error;
|
|
1519
1543
|
} finally {
|
|
1520
1544
|
if (timeout) {
|
|
1521
1545
|
clearTimeout(timeout);
|
|
@@ -1523,35 +1547,81 @@ async function connectRuntimePostgresPool(
|
|
|
1523
1547
|
}
|
|
1524
1548
|
}
|
|
1525
1549
|
|
|
1526
|
-
function
|
|
1550
|
+
function runtimePostgresLane(
|
|
1551
|
+
session: RuntimePostgresSession,
|
|
1552
|
+
): RuntimePostgresLane {
|
|
1553
|
+
return session.target.logicalTable === RUNTIME_WORK_RECEIPT_LOGICAL_TABLE
|
|
1554
|
+
? 'receipts'
|
|
1555
|
+
: 'sheets';
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function runtimePostgresPoolKey(
|
|
1559
|
+
postgresUrl: string,
|
|
1560
|
+
lane: RuntimePostgresLane,
|
|
1561
|
+
): string {
|
|
1562
|
+
return `${postgresUrl}::${lane}`;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function getRuntimePostgresAdmission(
|
|
1566
|
+
postgresUrl: string,
|
|
1567
|
+
): RuntimePostgresAdmission {
|
|
1568
|
+
const existing = runtimePostgresAdmissions.get(postgresUrl);
|
|
1569
|
+
if (existing) return existing;
|
|
1570
|
+
const admission = new RuntimePostgresAdmission({
|
|
1571
|
+
maxActivePerLane: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
|
|
1572
|
+
maxQueuedPerLane: RUNTIME_POSTGRES_ADMISSION_MAX_QUEUED_PER_LANE,
|
|
1573
|
+
acquireTimeoutMs: RUNTIME_POSTGRES_ADMISSION_TIMEOUT_MS,
|
|
1574
|
+
});
|
|
1575
|
+
runtimePostgresAdmissions.set(postgresUrl, admission);
|
|
1576
|
+
return admission;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export function runtimePostgresAdmissionTelemetry(
|
|
1580
|
+
postgresUrl: string,
|
|
1581
|
+
): RuntimePostgresAdmissionSnapshot | null {
|
|
1582
|
+
return runtimePostgresAdmissions.get(postgresUrl)?.snapshot() ?? null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function getPostgresPool(
|
|
1586
|
+
postgresUrl: string,
|
|
1587
|
+
lane: RuntimePostgresLane,
|
|
1588
|
+
cachePool = true,
|
|
1589
|
+
): RuntimePool {
|
|
1527
1590
|
if (!cachePool) {
|
|
1528
1591
|
return createRuntimePool({
|
|
1529
1592
|
connectionString: postgresUrl,
|
|
1530
|
-
maxConnections:
|
|
1593
|
+
maxConnections: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
|
|
1531
1594
|
idleTimeoutMs: 0,
|
|
1532
1595
|
connectTimeoutMs: RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS,
|
|
1533
1596
|
});
|
|
1534
1597
|
}
|
|
1535
|
-
const
|
|
1598
|
+
const poolKey = runtimePostgresPoolKey(postgresUrl, lane);
|
|
1599
|
+
const existing = postgresPools.get(poolKey);
|
|
1536
1600
|
if (existing) {
|
|
1537
1601
|
return existing;
|
|
1538
1602
|
}
|
|
1539
1603
|
const pool = createRuntimePool({
|
|
1540
1604
|
connectionString: postgresUrl,
|
|
1541
|
-
maxConnections:
|
|
1605
|
+
maxConnections: RUNTIME_POSTGRES_POOL_CONNECTIONS_PER_LANE,
|
|
1542
1606
|
idleTimeoutMs: 15_000,
|
|
1543
1607
|
connectTimeoutMs: RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS,
|
|
1544
1608
|
});
|
|
1545
|
-
postgresPools.set(
|
|
1609
|
+
postgresPools.set(poolKey, pool);
|
|
1546
1610
|
return pool;
|
|
1547
1611
|
}
|
|
1548
1612
|
|
|
1549
1613
|
async function resetRuntimePostgresPool(postgresUrl: string): Promise<void> {
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1614
|
+
const pools = (['receipts', 'sheets'] as const)
|
|
1615
|
+
.map((lane) => runtimePostgresPoolKey(postgresUrl, lane))
|
|
1616
|
+
.map((key) => {
|
|
1617
|
+
const pool = postgresPools.get(key);
|
|
1618
|
+
postgresPools.delete(key);
|
|
1619
|
+
return pool;
|
|
1620
|
+
})
|
|
1621
|
+
.filter((pool): pool is RuntimePool => pool !== undefined);
|
|
1622
|
+
await Promise.all(
|
|
1623
|
+
pools.map((pool) => Promise.resolve(pool.end()).catch(() => {})),
|
|
1624
|
+
);
|
|
1555
1625
|
}
|
|
1556
1626
|
|
|
1557
1627
|
async function withRuntimePostgres<T>(
|
|
@@ -1567,13 +1637,15 @@ async function withRuntimePostgres<T>(
|
|
|
1567
1637
|
options.maxConnectAttempts === undefined
|
|
1568
1638
|
? RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS
|
|
1569
1639
|
: Math.max(1, Math.floor(options.maxConnectAttempts));
|
|
1640
|
+
const lane = runtimePostgresLane(session);
|
|
1641
|
+
const admission = getRuntimePostgresAdmission(session.postgresUrl);
|
|
1570
1642
|
for (let attempt = 1; attempt <= maxConnectAttempts; attempt += 1) {
|
|
1571
1643
|
try {
|
|
1572
|
-
const pool = getPostgresPool(session.postgresUrl, cachePool);
|
|
1644
|
+
const pool = getPostgresPool(session.postgresUrl, lane, cachePool);
|
|
1573
1645
|
if (!cachePool) {
|
|
1574
1646
|
requestLocalPool = pool;
|
|
1575
1647
|
}
|
|
1576
|
-
client = await connectRuntimePostgresPool(pool);
|
|
1648
|
+
client = await connectRuntimePostgresPool(pool, admission, lane);
|
|
1577
1649
|
if (session.executionRole) {
|
|
1578
1650
|
await client.query(
|
|
1579
1651
|
`SET ROLE ${quoteIdentifier(session.executionRole)}`,
|
|
@@ -1586,8 +1658,9 @@ async function withRuntimePostgres<T>(
|
|
|
1586
1658
|
client = null;
|
|
1587
1659
|
}
|
|
1588
1660
|
if (cachePool) {
|
|
1589
|
-
const
|
|
1590
|
-
postgresPools.
|
|
1661
|
+
const poolKey = runtimePostgresPoolKey(session.postgresUrl, lane);
|
|
1662
|
+
const pool = postgresPools.get(poolKey);
|
|
1663
|
+
postgresPools.delete(poolKey);
|
|
1591
1664
|
if (pool) {
|
|
1592
1665
|
await Promise.resolve(pool.end()).catch(() => {});
|
|
1593
1666
|
}
|
|
@@ -1889,6 +1962,10 @@ function workReceiptTable(session: RuntimePostgresSession): string {
|
|
|
1889
1962
|
);
|
|
1890
1963
|
}
|
|
1891
1964
|
|
|
1965
|
+
function receiptExecutionLockTable(session: RuntimePostgresSession): string {
|
|
1966
|
+
return fqRuntimeTable(session, '_deepline_receipt_execution_locks');
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1892
1969
|
function postgresUrlCacheKey(value: string): string {
|
|
1893
1970
|
return createHash('sha256').update(value).digest('hex').slice(0, 24);
|
|
1894
1971
|
}
|
|
@@ -2030,6 +2107,14 @@ async function ensureRuntimeWorkReceiptTable(
|
|
|
2030
2107
|
`,
|
|
2031
2108
|
)
|
|
2032
2109
|
.then(async () => {
|
|
2110
|
+
await client.query(`
|
|
2111
|
+
CREATE TABLE IF NOT EXISTS ${receiptExecutionLockTable(session)} (
|
|
2112
|
+
k bytea PRIMARY KEY,
|
|
2113
|
+
owner_execution_id text NOT NULL,
|
|
2114
|
+
expires_at timestamptz NOT NULL,
|
|
2115
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
2116
|
+
)
|
|
2117
|
+
`);
|
|
2033
2118
|
const missingColumns = await missingRuntimeWorkReceiptSelfHealColumns(
|
|
2034
2119
|
session,
|
|
2035
2120
|
client,
|
|
@@ -2064,6 +2149,77 @@ async function ensureRuntimeWorkReceiptTable(
|
|
|
2064
2149
|
}
|
|
2065
2150
|
}
|
|
2066
2151
|
|
|
2152
|
+
export async function acquireRuntimeReceiptExecutionLock(
|
|
2153
|
+
context: RuntimeApiContext,
|
|
2154
|
+
input: {
|
|
2155
|
+
playName: string;
|
|
2156
|
+
key: string;
|
|
2157
|
+
ownerExecutionId: string;
|
|
2158
|
+
ttlMs: number;
|
|
2159
|
+
},
|
|
2160
|
+
): Promise<{ ownerExecutionId: string; expiresAt: string } | null> {
|
|
2161
|
+
const ttlMs = Math.floor(input.ttlMs);
|
|
2162
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > 10 * 60_000) {
|
|
2163
|
+
throw new Error('Runtime receipt execution lock TTL must be 1..600000ms.');
|
|
2164
|
+
}
|
|
2165
|
+
const ownerExecutionId = input.ownerExecutionId.trim();
|
|
2166
|
+
if (!ownerExecutionId) {
|
|
2167
|
+
throw new Error('Runtime receipt execution lock owner is required.');
|
|
2168
|
+
}
|
|
2169
|
+
const session = await getRuntimeWorkReceiptSession(context, input);
|
|
2170
|
+
return await withRuntimeWorkReceiptClient(
|
|
2171
|
+
context,
|
|
2172
|
+
session,
|
|
2173
|
+
async (client) => {
|
|
2174
|
+
const { rows } = await client.query<Record<string, unknown>>(
|
|
2175
|
+
`
|
|
2176
|
+
INSERT INTO ${receiptExecutionLockTable(session)} AS target (
|
|
2177
|
+
k, owner_execution_id, expires_at, updated_at
|
|
2178
|
+
) VALUES (
|
|
2179
|
+
decode($1, 'hex'), $2, now() + ($3::double precision * interval '1 millisecond'), now()
|
|
2180
|
+
)
|
|
2181
|
+
ON CONFLICT (k) DO UPDATE
|
|
2182
|
+
SET owner_execution_id = EXCLUDED.owner_execution_id,
|
|
2183
|
+
expires_at = EXCLUDED.expires_at,
|
|
2184
|
+
updated_at = now()
|
|
2185
|
+
WHERE target.expires_at <= now()
|
|
2186
|
+
OR target.owner_execution_id = EXCLUDED.owner_execution_id
|
|
2187
|
+
RETURNING owner_execution_id, expires_at
|
|
2188
|
+
`,
|
|
2189
|
+
[workReceiptKeyHex(input.key), ownerExecutionId, ttlMs],
|
|
2190
|
+
);
|
|
2191
|
+
const row = rows[0];
|
|
2192
|
+
return row
|
|
2193
|
+
? {
|
|
2194
|
+
ownerExecutionId: String(row.owner_execution_id),
|
|
2195
|
+
expiresAt: new Date(String(row.expires_at)).toISOString(),
|
|
2196
|
+
}
|
|
2197
|
+
: null;
|
|
2198
|
+
},
|
|
2199
|
+
);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
export async function releaseRuntimeReceiptExecutionLock(
|
|
2203
|
+
context: RuntimeApiContext,
|
|
2204
|
+
input: { playName: string; key: string; ownerExecutionId: string },
|
|
2205
|
+
): Promise<boolean> {
|
|
2206
|
+
const session = await getRuntimeWorkReceiptSession(context, input);
|
|
2207
|
+
return await withRuntimeWorkReceiptClient(
|
|
2208
|
+
context,
|
|
2209
|
+
session,
|
|
2210
|
+
async (client) => {
|
|
2211
|
+
const { rows } = await client.query(
|
|
2212
|
+
`DELETE FROM ${receiptExecutionLockTable(session)}
|
|
2213
|
+
WHERE k = decode($1, 'hex')
|
|
2214
|
+
AND owner_execution_id = $2
|
|
2215
|
+
RETURNING 1`,
|
|
2216
|
+
[workReceiptKeyHex(input.key), input.ownerExecutionId],
|
|
2217
|
+
);
|
|
2218
|
+
return rows.length > 0;
|
|
2219
|
+
},
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2067
2223
|
async function withRuntimeWorkReceiptClient<T>(
|
|
2068
2224
|
context: RuntimeApiContext,
|
|
2069
2225
|
session: RuntimePostgresSession,
|
|
@@ -2273,6 +2429,31 @@ async function mintRuntimeSheetAttemptExpiresAt(
|
|
|
2273
2429
|
throw new Error('Runtime sheet attempt expiry mint did not return a value.');
|
|
2274
2430
|
}
|
|
2275
2431
|
|
|
2432
|
+
async function mintRuntimeSheetWriteVersion(
|
|
2433
|
+
client: RuntimeQueryClient,
|
|
2434
|
+
session: RuntimePostgresSession,
|
|
2435
|
+
requested?: number | null,
|
|
2436
|
+
): Promise<number> {
|
|
2437
|
+
if (requested != null) {
|
|
2438
|
+
if (!Number.isSafeInteger(requested) || requested <= 0) {
|
|
2439
|
+
throw new Error(
|
|
2440
|
+
'Runtime sheet writeVersion must be a positive safe integer.',
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return requested;
|
|
2444
|
+
}
|
|
2445
|
+
const { rows } = await client.query<{ write_version: number | string }>(
|
|
2446
|
+
`SELECT ${nextRuntimeSheetVersionExpression(session)} AS write_version`,
|
|
2447
|
+
);
|
|
2448
|
+
const value = Number(rows[0]?.write_version);
|
|
2449
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
'Runtime sheet write version allocation did not return a safe integer.',
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
return value;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2276
2457
|
function parseRuntimeCellMeta(value: unknown): Record<string, unknown> {
|
|
2277
2458
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
2278
2459
|
return value as Record<string, unknown>;
|
|
@@ -2773,6 +2954,7 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2773
2954
|
attemptOwnerRunId: string;
|
|
2774
2955
|
attemptExpiresAt: string;
|
|
2775
2956
|
attemptSeq: number;
|
|
2957
|
+
writeVersion: number;
|
|
2776
2958
|
physicalInsertColumnsSql: string;
|
|
2777
2959
|
physicalInsertValuesSql: string;
|
|
2778
2960
|
physicalRefreshSetSql: string;
|
|
@@ -2851,6 +3033,44 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2851
3033
|
ON index_values.ord = key_values.ord
|
|
2852
3034
|
ORDER BY key_values._key, key_values.ord
|
|
2853
3035
|
),
|
|
3036
|
+
preexisting_rows AS (
|
|
3037
|
+
SELECT target._key
|
|
3038
|
+
FROM ${sheetTable(session)} AS target
|
|
3039
|
+
JOIN input_rows ON input_rows._key = target._key
|
|
3040
|
+
),
|
|
3041
|
+
versioned_completed_rows AS (
|
|
3042
|
+
UPDATE ${sheetTable(session)} AS target
|
|
3043
|
+
SET _run_id = $4::text,
|
|
3044
|
+
_writer_run_id = $4::text,
|
|
3045
|
+
_write_version = $12::bigint,
|
|
3046
|
+
_input_index = input_rows._input_index,
|
|
3047
|
+
_updated_at = now(),
|
|
3048
|
+
_version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
|
|
3049
|
+
FROM input_rows
|
|
3050
|
+
WHERE target._key = input_rows._key
|
|
3051
|
+
AND target._status = 'enriched'
|
|
3052
|
+
AND COALESCE(target._write_version, 0) < $12::bigint
|
|
3053
|
+
RETURNING target._key,
|
|
3054
|
+
($11::boolean OR (${targetMissingOutputSql})) AS needs_recompute
|
|
3055
|
+
),
|
|
3056
|
+
versioned_failed_rows AS (
|
|
3057
|
+
UPDATE ${sheetTable(session)} AS target
|
|
3058
|
+
SET _run_id = $4::text,
|
|
3059
|
+
_writer_run_id = $4::text,
|
|
3060
|
+
_write_version = $12::bigint,
|
|
3061
|
+
_input_index = input_rows._input_index,
|
|
3062
|
+
_attempt_id = $7::text,
|
|
3063
|
+
_attempt_owner_run_id = $8::text,
|
|
3064
|
+
_attempt_expires_at = NULL,
|
|
3065
|
+
_attempt_seq = $10::integer,
|
|
3066
|
+
_updated_at = now(),
|
|
3067
|
+
_version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
|
|
3068
|
+
FROM input_rows
|
|
3069
|
+
WHERE target._key = input_rows._key
|
|
3070
|
+
AND target._status = 'failed'
|
|
3071
|
+
AND COALESCE(target._write_version, 0) < $12::bigint
|
|
3072
|
+
RETURNING target._key
|
|
3073
|
+
),
|
|
2854
3074
|
existing_rows AS (
|
|
2855
3075
|
UPDATE ${sheetTable(session)} AS target
|
|
2856
3076
|
SET _input_index = input_rows._input_index,
|
|
@@ -2858,6 +3078,7 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2858
3078
|
_version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
|
|
2859
3079
|
FROM input_rows
|
|
2860
3080
|
WHERE target._key = input_rows._key
|
|
3081
|
+
AND target._write_version = $12::bigint
|
|
2861
3082
|
AND target._status <> 'stale'
|
|
2862
3083
|
AND (
|
|
2863
3084
|
NOT (${targetNewerTerminalRowSql})
|
|
@@ -2881,8 +3102,8 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2881
3102
|
RETURNING target._key
|
|
2882
3103
|
),
|
|
2883
3104
|
inserted_rows AS (
|
|
2884
|
-
INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq${input.physicalInsertColumnsSql})
|
|
2885
|
-
SELECT _key, 'pending', $4::text, _input_index, $7::text, $8::text, $9::timestamptz, $10::integer${input.physicalInsertValuesSql}
|
|
3105
|
+
INSERT INTO ${sheetTable(session)} (_key, _status, _run_id, _input_index, _attempt_id, _attempt_owner_run_id, _attempt_expires_at, _attempt_seq, _write_version, _writer_run_id${input.physicalInsertColumnsSql})
|
|
3106
|
+
SELECT _key, 'pending', $4::text, _input_index, $7::text, $8::text, $9::timestamptz, $10::integer, $12::bigint, $4::text${input.physicalInsertValuesSql}
|
|
2886
3107
|
FROM input_rows
|
|
2887
3108
|
ON CONFLICT (_key) DO UPDATE SET
|
|
2888
3109
|
_status = 'pending',
|
|
@@ -2892,6 +3113,8 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2892
3113
|
_attempt_owner_run_id = EXCLUDED._attempt_owner_run_id,
|
|
2893
3114
|
_attempt_expires_at = EXCLUDED._attempt_expires_at,
|
|
2894
3115
|
_attempt_seq = EXCLUDED._attempt_seq,
|
|
3116
|
+
_write_version = EXCLUDED._write_version,
|
|
3117
|
+
_writer_run_id = EXCLUDED._writer_run_id,
|
|
2895
3118
|
_updated_at = now(),
|
|
2896
3119
|
_version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalUpsertSetSql}
|
|
2897
3120
|
WHERE ${sheetTable(session)}._status = 'stale'
|
|
@@ -2911,10 +3134,30 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2911
3134
|
FROM input_rows
|
|
2912
3135
|
WHERE target._key = input_rows._key
|
|
2913
3136
|
AND target._status = 'enriched'
|
|
3137
|
+
AND target._write_version = $12::bigint
|
|
2914
3138
|
AND (${targetMissingOutputSql})
|
|
2915
3139
|
AND COALESCE(target._attempt_seq, 0) <= $10::integer
|
|
2916
3140
|
RETURNING target._key
|
|
2917
3141
|
),
|
|
3142
|
+
superseded_rows AS (
|
|
3143
|
+
UPDATE ${sheetTable(session)} AS target
|
|
3144
|
+
SET _run_id = $4::text,
|
|
3145
|
+
_input_index = input_rows._input_index,
|
|
3146
|
+
_attempt_id = $7::text,
|
|
3147
|
+
_attempt_owner_run_id = $8::text,
|
|
3148
|
+
_attempt_expires_at = NULL,
|
|
3149
|
+
_attempt_seq = $10::integer,
|
|
3150
|
+
_write_version = $12::bigint,
|
|
3151
|
+
_writer_run_id = $4::text,
|
|
3152
|
+
_error = NULL,
|
|
3153
|
+
_updated_at = now(),
|
|
3154
|
+
_version = ${nextRuntimeSheetVersionExpression(session)}${input.physicalRefreshSetSql}
|
|
3155
|
+
FROM input_rows
|
|
3156
|
+
WHERE target._key = input_rows._key
|
|
3157
|
+
AND target._status IN ('pending', 'running')
|
|
3158
|
+
AND COALESCE(target._write_version, 0) < $12::bigint
|
|
3159
|
+
RETURNING target._key
|
|
3160
|
+
),
|
|
2918
3161
|
claimed_existing_rows AS (
|
|
2919
3162
|
UPDATE ${sheetTable(session)} AS target
|
|
2920
3163
|
SET _run_id = $4::text,
|
|
@@ -2928,6 +3171,7 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2928
3171
|
FROM input_rows
|
|
2929
3172
|
WHERE target._key = input_rows._key
|
|
2930
3173
|
AND target._status IN ('pending', 'running', 'failed')
|
|
3174
|
+
AND target._write_version = $12::bigint
|
|
2931
3175
|
AND NOT (${targetNewerTerminalRowSql})
|
|
2932
3176
|
AND (
|
|
2933
3177
|
${targetAttemptFenceSql}
|
|
@@ -2950,6 +3194,9 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2950
3194
|
FROM inserted_rows
|
|
2951
3195
|
UNION
|
|
2952
3196
|
SELECT _key
|
|
3197
|
+
FROM superseded_rows
|
|
3198
|
+
UNION
|
|
3199
|
+
SELECT _key
|
|
2953
3200
|
FROM missing_output_rows
|
|
2954
3201
|
UNION
|
|
2955
3202
|
SELECT _key
|
|
@@ -2959,10 +3206,18 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2959
3206
|
FROM existing_rows
|
|
2960
3207
|
WHERE $11::boolean
|
|
2961
3208
|
UNION
|
|
3209
|
+
SELECT _key
|
|
3210
|
+
FROM versioned_completed_rows
|
|
3211
|
+
WHERE needs_recompute
|
|
3212
|
+
UNION
|
|
3213
|
+
SELECT _key FROM versioned_failed_rows
|
|
3214
|
+
UNION
|
|
2962
3215
|
SELECT existing._key
|
|
2963
3216
|
FROM ${sheetTable(session)} AS existing
|
|
2964
3217
|
JOIN input_rows ON input_rows._key = existing._key
|
|
2965
3218
|
WHERE (
|
|
3219
|
+
existing._write_version = $12::bigint
|
|
3220
|
+
AND (
|
|
2966
3221
|
existing._status IN ('pending', 'running', 'failed')
|
|
2967
3222
|
AND NOT (${existingNewerTerminalRowSql})
|
|
2968
3223
|
AND (
|
|
@@ -2975,8 +3230,10 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2975
3230
|
AND COALESCE(existing._attempt_seq, 0) < $10::integer
|
|
2976
3231
|
)
|
|
2977
3232
|
)
|
|
2978
|
-
)
|
|
3233
|
+
))
|
|
2979
3234
|
OR (
|
|
3235
|
+
existing._write_version = $12::bigint
|
|
3236
|
+
AND
|
|
2980
3237
|
existing._status = 'enriched'
|
|
2981
3238
|
AND (${existingMissingOutputSql})
|
|
2982
3239
|
AND (
|
|
@@ -2995,6 +3252,9 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2995
3252
|
AND NOT EXISTS (
|
|
2996
3253
|
SELECT 1 FROM pending_rows WHERE pending_rows._key = existing._key
|
|
2997
3254
|
)
|
|
3255
|
+
UNION
|
|
3256
|
+
SELECT _key FROM versioned_completed_rows
|
|
3257
|
+
WHERE NOT needs_recompute
|
|
2998
3258
|
),
|
|
2999
3259
|
blocked_rows AS (
|
|
3000
3260
|
SELECT input_rows._key
|
|
@@ -3017,7 +3277,11 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
3017
3277
|
FROM blocked_rows
|
|
3018
3278
|
),
|
|
3019
3279
|
inserted_count_cte AS (
|
|
3020
|
-
SELECT count(*)::bigint AS c
|
|
3280
|
+
SELECT count(*)::bigint AS c
|
|
3281
|
+
FROM inserted_rows
|
|
3282
|
+
WHERE NOT EXISTS (
|
|
3283
|
+
SELECT 1 FROM preexisting_rows WHERE preexisting_rows._key = inserted_rows._key
|
|
3284
|
+
)
|
|
3021
3285
|
),
|
|
3022
3286
|
missing_output_count_cte AS (
|
|
3023
3287
|
SELECT count(*)::bigint AS c FROM missing_output_rows
|
|
@@ -3077,6 +3341,7 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
3077
3341
|
input.attemptExpiresAt,
|
|
3078
3342
|
input.attemptSeq,
|
|
3079
3343
|
input.force === true,
|
|
3344
|
+
input.writeVersion,
|
|
3080
3345
|
],
|
|
3081
3346
|
);
|
|
3082
3347
|
inserted += Number(rows[0]?.inserted_count ?? 0);
|
|
@@ -3113,6 +3378,7 @@ async function buildRuntimeSheetDatasetStartResult(
|
|
|
3113
3378
|
attemptOwnerRunId: string;
|
|
3114
3379
|
attemptExpiresAt: string;
|
|
3115
3380
|
attemptSeq: number;
|
|
3381
|
+
writeVersion: number;
|
|
3116
3382
|
inserted: number;
|
|
3117
3383
|
rowDispositions: RuntimeSheetPreparedRowDisposition[];
|
|
3118
3384
|
force?: boolean;
|
|
@@ -3148,6 +3414,7 @@ async function buildRuntimeSheetDatasetStartResult(
|
|
|
3148
3414
|
completedRows: [],
|
|
3149
3415
|
blockedRows: [],
|
|
3150
3416
|
tableNamespace: input.tableNamespace,
|
|
3417
|
+
writeVersion: input.writeVersion,
|
|
3151
3418
|
};
|
|
3152
3419
|
}
|
|
3153
3420
|
|
|
@@ -3167,6 +3434,7 @@ async function buildRuntimeSheetDatasetStartResult(
|
|
|
3167
3434
|
attemptOwnerRunId: input.attemptOwnerRunId,
|
|
3168
3435
|
attemptExpiresAt: input.attemptExpiresAt,
|
|
3169
3436
|
attemptSeq: input.attemptSeq,
|
|
3437
|
+
writeVersion: input.writeVersion,
|
|
3170
3438
|
normalizedPlayName: input.normalizedPlayName,
|
|
3171
3439
|
normalizedTableNamespace: input.normalizedTableNamespace,
|
|
3172
3440
|
outputFields: input.force === true ? datasetFields : [],
|
|
@@ -3197,6 +3465,7 @@ async function buildRuntimeSheetDatasetStartResult(
|
|
|
3197
3465
|
completedRows: [],
|
|
3198
3466
|
blockedRows,
|
|
3199
3467
|
tableNamespace: input.tableNamespace,
|
|
3468
|
+
writeVersion: input.writeVersion,
|
|
3200
3469
|
};
|
|
3201
3470
|
}
|
|
3202
3471
|
const persistedRows = await readRuntimeRowsByKey(
|
|
@@ -3246,6 +3515,7 @@ async function buildRuntimeSheetDatasetStartResult(
|
|
|
3246
3515
|
completedRows,
|
|
3247
3516
|
blockedRows,
|
|
3248
3517
|
tableNamespace: input.tableNamespace,
|
|
3518
|
+
writeVersion: input.writeVersion ?? undefined,
|
|
3249
3519
|
};
|
|
3250
3520
|
}
|
|
3251
3521
|
|
|
@@ -3259,6 +3529,7 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3259
3529
|
attemptOwnerRunId: string;
|
|
3260
3530
|
attemptExpiresAt: string;
|
|
3261
3531
|
attemptSeq: number;
|
|
3532
|
+
writeVersion: number | null;
|
|
3262
3533
|
normalizedPlayName: string;
|
|
3263
3534
|
normalizedTableNamespace: string;
|
|
3264
3535
|
outputFields?: string[];
|
|
@@ -3266,28 +3537,13 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3266
3537
|
},
|
|
3267
3538
|
): Promise<void> {
|
|
3268
3539
|
if (input.keys.length === 0) return;
|
|
3269
|
-
const targetAttemptFenceSql = activeRuntimeSheetAttemptFenceSql(
|
|
3270
|
-
'target',
|
|
3271
|
-
'$5::text',
|
|
3272
|
-
'$6::text',
|
|
3273
|
-
'$7::timestamptz',
|
|
3274
|
-
'$8::integer',
|
|
3275
|
-
);
|
|
3276
|
-
const targetOlderForeignAttemptOwnerSql =
|
|
3277
|
-
'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $6::text AND COALESCE(target._attempt_seq, 0) < $8::integer';
|
|
3278
|
-
const targetWritableEnrichedAttemptSql =
|
|
3279
|
-
writableEnrichedRuntimeSheetAttemptSql(
|
|
3280
|
-
'target',
|
|
3281
|
-
'$6::text',
|
|
3282
|
-
'$7::timestamptz',
|
|
3283
|
-
'$8',
|
|
3284
|
-
);
|
|
3285
3540
|
const outputFields = [...new Set(input.outputFields ?? [])];
|
|
3286
3541
|
await client.query(
|
|
3287
3542
|
`WITH target_rows AS (
|
|
3288
3543
|
SELECT _key, _status, _cell_meta
|
|
3289
3544
|
FROM ${sheetTable(session)}
|
|
3290
3545
|
WHERE _key = ANY($1::text[])
|
|
3546
|
+
AND ($10::boolean OR NOT $10::boolean)
|
|
3291
3547
|
FOR UPDATE
|
|
3292
3548
|
),
|
|
3293
3549
|
updated AS (
|
|
@@ -3298,33 +3554,13 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3298
3554
|
_attempt_owner_run_id = $6::text,
|
|
3299
3555
|
_attempt_expires_at = $7::timestamptz,
|
|
3300
3556
|
_attempt_seq = $8::integer,
|
|
3557
|
+
_write_version = $11::bigint,
|
|
3558
|
+
_writer_run_id = $2::text,
|
|
3301
3559
|
_updated_at = now(),
|
|
3302
3560
|
_version = ${nextRuntimeSheetVersionExpression(session)}
|
|
3303
3561
|
FROM target_rows
|
|
3304
3562
|
WHERE target._key = target_rows._key
|
|
3305
|
-
AND
|
|
3306
|
-
(
|
|
3307
|
-
target._status IN ('pending', 'running', 'failed')
|
|
3308
|
-
AND (
|
|
3309
|
-
${targetAttemptFenceSql}
|
|
3310
|
-
OR ${targetOlderForeignAttemptOwnerSql}
|
|
3311
|
-
OR (
|
|
3312
|
-
$10::boolean
|
|
3313
|
-
AND target._status = 'failed'
|
|
3314
|
-
)
|
|
3315
|
-
)
|
|
3316
|
-
)
|
|
3317
|
-
OR ${targetAttemptFenceSql}
|
|
3318
|
-
OR (
|
|
3319
|
-
$10::boolean
|
|
3320
|
-
AND target._status = 'enriched'
|
|
3321
|
-
AND COALESCE(target._attempt_seq, 0) <= $8::integer
|
|
3322
|
-
)
|
|
3323
|
-
OR (
|
|
3324
|
-
target._status = 'enriched'
|
|
3325
|
-
AND ${targetWritableEnrichedAttemptSql}
|
|
3326
|
-
)
|
|
3327
|
-
)
|
|
3563
|
+
AND target._write_version = $11::bigint
|
|
3328
3564
|
AND (
|
|
3329
3565
|
target._status <> 'pending'
|
|
3330
3566
|
OR target._run_id IS DISTINCT FROM $2::text
|
|
@@ -3414,6 +3650,7 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3414
3650
|
input.attemptSeq,
|
|
3415
3651
|
outputFields,
|
|
3416
3652
|
input.force === true,
|
|
3653
|
+
input.writeVersion,
|
|
3417
3654
|
],
|
|
3418
3655
|
);
|
|
3419
3656
|
}
|
|
@@ -3603,6 +3840,7 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3603
3840
|
playName: string;
|
|
3604
3841
|
runId: string;
|
|
3605
3842
|
key: string;
|
|
3843
|
+
leaseId?: string | null;
|
|
3606
3844
|
runAttempt?: number | null;
|
|
3607
3845
|
leaseAware?: boolean;
|
|
3608
3846
|
reclaimRunning?: boolean;
|
|
@@ -3620,7 +3858,8 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3620
3858
|
session,
|
|
3621
3859
|
async (client) => {
|
|
3622
3860
|
const leaseId =
|
|
3623
|
-
input.
|
|
3861
|
+
input.leaseId?.trim() ||
|
|
3862
|
+
(input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null);
|
|
3624
3863
|
const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
|
|
3625
3864
|
const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
|
|
3626
3865
|
input.leaseTtlMs,
|
|
@@ -3748,6 +3987,15 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3748
3987
|
}
|
|
3749
3988
|
|
|
3750
3989
|
const latest = await readRuntimeWorkReceipt(client, session, input.key);
|
|
3990
|
+
if (
|
|
3991
|
+
latest &&
|
|
3992
|
+
leaseId !== null &&
|
|
3993
|
+
latest.leaseId === leaseId &&
|
|
3994
|
+
(latest.leaseOwnerRunId ?? latest.runId) === input.runId &&
|
|
3995
|
+
(latest.leaseOwnerAttempt ?? 0) === runAttempt
|
|
3996
|
+
) {
|
|
3997
|
+
return { disposition: 'claimed', receipt: latest };
|
|
3998
|
+
}
|
|
3751
3999
|
if (
|
|
3752
4000
|
latest &&
|
|
3753
4001
|
input.forceRefresh !== true &&
|
|
@@ -3809,6 +4057,7 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3809
4057
|
playName: string;
|
|
3810
4058
|
runId: string;
|
|
3811
4059
|
keys: string[];
|
|
4060
|
+
leaseIds?: string[];
|
|
3812
4061
|
runAttempt?: number | null;
|
|
3813
4062
|
leaseAware?: boolean;
|
|
3814
4063
|
reclaimRunning?: boolean;
|
|
@@ -3830,9 +4079,21 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3830
4079
|
session,
|
|
3831
4080
|
async (client) => {
|
|
3832
4081
|
const keyHexes = keys.map(workReceiptKeyHex);
|
|
3833
|
-
|
|
3834
|
-
input.
|
|
3835
|
-
|
|
4082
|
+
if (
|
|
4083
|
+
input.leaseIds !== undefined &&
|
|
4084
|
+
input.leaseIds.length !== keys.length
|
|
4085
|
+
) {
|
|
4086
|
+
throw new Error(
|
|
4087
|
+
`Runtime receipt bulk claim requires one lease ID per key. Received ${input.leaseIds.length} lease IDs for ${keys.length} keys.`,
|
|
4088
|
+
);
|
|
4089
|
+
}
|
|
4090
|
+
const leaseIds = keys.map((_, index) => {
|
|
4091
|
+
const providedLeaseId = input.leaseIds?.[index]?.trim();
|
|
4092
|
+
return (
|
|
4093
|
+
providedLeaseId ||
|
|
4094
|
+
(input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null)
|
|
4095
|
+
);
|
|
4096
|
+
});
|
|
3836
4097
|
const claimStatus =
|
|
3837
4098
|
input.leaseAware === true
|
|
3838
4099
|
? RECEIPT_STATUS_QUEUED
|
|
@@ -3935,7 +4196,10 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3935
4196
|
receipts.lease_expires_at,
|
|
3936
4197
|
receipts.updated_at,
|
|
3937
4198
|
input_keys.ord,
|
|
3938
|
-
|
|
4199
|
+
receipts.lease_id = input_keys.lease_id
|
|
4200
|
+
AND receipts.lease_owner_run_id = $4::text
|
|
4201
|
+
AND COALESCE(receipts.lease_owner_attempt, 0) = $9::integer
|
|
4202
|
+
AS claimed
|
|
3939
4203
|
FROM input_keys
|
|
3940
4204
|
JOIN ${workReceiptTable(session)} AS receipts
|
|
3941
4205
|
ON receipts.k = decode(input_keys.key_hex, 'hex')
|
|
@@ -4411,7 +4675,48 @@ export async function completeRuntimeWorkReceipt(
|
|
|
4411
4675
|
context,
|
|
4412
4676
|
session,
|
|
4413
4677
|
async (client) => {
|
|
4414
|
-
const
|
|
4678
|
+
const immutableCacheInsert =
|
|
4679
|
+
input.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID;
|
|
4680
|
+
const leaseId = immutableCacheInsert
|
|
4681
|
+
? null
|
|
4682
|
+
: input.leaseId?.trim() || null;
|
|
4683
|
+
if (immutableCacheInsert) {
|
|
4684
|
+
const { rows: insertedRows } = await client.query<
|
|
4685
|
+
Record<string, unknown>
|
|
4686
|
+
>(
|
|
4687
|
+
`
|
|
4688
|
+
INSERT INTO ${workReceiptTable(session)} (
|
|
4689
|
+
k, status, output, error, failure_kind, run_id, updated_at
|
|
4690
|
+
) VALUES (
|
|
4691
|
+
decode($1, 'hex'), $2::smallint, $3::jsonb, NULL, 0, $4, now()
|
|
4692
|
+
)
|
|
4693
|
+
ON CONFLICT (k) DO NOTHING
|
|
4694
|
+
RETURNING convert_from(k, 'UTF8') AS k, status, output, error,
|
|
4695
|
+
failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
4696
|
+
lease_owner_attempt, lease_expires_at, updated_at
|
|
4697
|
+
`,
|
|
4698
|
+
[
|
|
4699
|
+
workReceiptKeyHex(input.key),
|
|
4700
|
+
RECEIPT_STATUS_COMPLETED,
|
|
4701
|
+
input.output === null ? null : stringifyPostgresJson(input.output),
|
|
4702
|
+
input.runId,
|
|
4703
|
+
],
|
|
4704
|
+
);
|
|
4705
|
+
if (insertedRows[0]) return mapRuntimeWorkReceiptRow(insertedRows[0]);
|
|
4706
|
+
const { rows: winnerRows } = await client.query<
|
|
4707
|
+
Record<string, unknown>
|
|
4708
|
+
>(
|
|
4709
|
+
`SELECT convert_from(k, 'UTF8') AS k, status, output, error,
|
|
4710
|
+
failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
4711
|
+
lease_owner_attempt, lease_expires_at, updated_at
|
|
4712
|
+
FROM ${workReceiptTable(session)}
|
|
4713
|
+
WHERE k = decode($1, 'hex')
|
|
4714
|
+
AND status IN ($2::smallint, ${RECEIPT_STATUS_SKIPPED}::smallint)
|
|
4715
|
+
LIMIT 1`,
|
|
4716
|
+
[workReceiptKeyHex(input.key), RECEIPT_STATUS_COMPLETED],
|
|
4717
|
+
);
|
|
4718
|
+
return winnerRows[0] ? mapRuntimeWorkReceiptRow(winnerRows[0]) : null;
|
|
4719
|
+
}
|
|
4415
4720
|
const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
|
|
4416
4721
|
const { rows } = await client.query<Record<string, unknown>>(
|
|
4417
4722
|
`
|
|
@@ -4546,6 +4851,44 @@ export async function completeRuntimeWorkReceipts(
|
|
|
4546
4851
|
}
|
|
4547
4852
|
throw error;
|
|
4548
4853
|
}
|
|
4854
|
+
const immutableEntries = receipts
|
|
4855
|
+
.map((receipt, index) => ({ receipt, index }))
|
|
4856
|
+
.filter(
|
|
4857
|
+
({ receipt }) =>
|
|
4858
|
+
receipt.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
4859
|
+
);
|
|
4860
|
+
if (immutableEntries.length > 0) {
|
|
4861
|
+
const leasedEntries = receipts
|
|
4862
|
+
.map((receipt, index) => ({ receipt, index }))
|
|
4863
|
+
.filter(
|
|
4864
|
+
({ receipt }) =>
|
|
4865
|
+
receipt.leaseId !== COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
4866
|
+
);
|
|
4867
|
+
const resolved = Array<WorkReceipt | null>(receipts.length).fill(null);
|
|
4868
|
+
const [immutableResults, leasedResults] = await Promise.all([
|
|
4869
|
+
Promise.all(
|
|
4870
|
+
immutableEntries.map(({ receipt }) =>
|
|
4871
|
+
completeRuntimeWorkReceipt(context, {
|
|
4872
|
+
playName: input.playName,
|
|
4873
|
+
...receipt,
|
|
4874
|
+
}),
|
|
4875
|
+
),
|
|
4876
|
+
),
|
|
4877
|
+
leasedEntries.length > 0
|
|
4878
|
+
? completeRuntimeWorkReceipts(context, {
|
|
4879
|
+
playName: input.playName,
|
|
4880
|
+
receipts: leasedEntries.map(({ receipt }) => receipt),
|
|
4881
|
+
})
|
|
4882
|
+
: Promise.resolve([]),
|
|
4883
|
+
]);
|
|
4884
|
+
immutableEntries.forEach(({ index }, resultIndex) => {
|
|
4885
|
+
resolved[index] = immutableResults[resultIndex] ?? null;
|
|
4886
|
+
});
|
|
4887
|
+
leasedEntries.forEach(({ index }, resultIndex) => {
|
|
4888
|
+
resolved[index] = leasedResults[resultIndex] ?? null;
|
|
4889
|
+
});
|
|
4890
|
+
return resolved;
|
|
4891
|
+
}
|
|
4549
4892
|
const session = await getRuntimeWorkReceiptSessionForKeys(context, {
|
|
4550
4893
|
playName: input.playName,
|
|
4551
4894
|
keys: receipts.map((receipt) => receipt.key),
|
|
@@ -5195,6 +5538,7 @@ export async function startRuntimeSheetDataset(
|
|
|
5195
5538
|
attemptExpiresAt?: string | null;
|
|
5196
5539
|
attemptSeq?: number | null;
|
|
5197
5540
|
attemptLeaseTtlMs?: number | null;
|
|
5541
|
+
writeVersion?: number | null;
|
|
5198
5542
|
inputOffset?: number;
|
|
5199
5543
|
force?: boolean;
|
|
5200
5544
|
},
|
|
@@ -5341,7 +5685,10 @@ export async function startRuntimeSheetDataset(
|
|
|
5341
5685
|
});
|
|
5342
5686
|
const attemptSeq = normalizeRuntimeRunAttempt(input.attemptSeq);
|
|
5343
5687
|
const chunks = chunkValues(rowEntries, DIRECT_POSTGRES_BATCH_SIZE);
|
|
5344
|
-
|
|
5688
|
+
// Scheduling is one admission decision. Even when batching SQL for the
|
|
5689
|
+
// parameter limit, either every requested row receives this intent version
|
|
5690
|
+
// or none do; a run can never leave 42 pending rows after 8 conflicts.
|
|
5691
|
+
const needsTransaction = true;
|
|
5345
5692
|
|
|
5346
5693
|
const result = await withRuntimeSheetQueryClient(
|
|
5347
5694
|
context,
|
|
@@ -5359,6 +5706,11 @@ export async function startRuntimeSheetDataset(
|
|
|
5359
5706
|
client,
|
|
5360
5707
|
input.attemptLeaseTtlMs,
|
|
5361
5708
|
);
|
|
5709
|
+
const writeVersion = await mintRuntimeSheetWriteVersion(
|
|
5710
|
+
client,
|
|
5711
|
+
session,
|
|
5712
|
+
input.writeVersion,
|
|
5713
|
+
);
|
|
5362
5714
|
timings.push({
|
|
5363
5715
|
phase: 'attempt_expiry_mint',
|
|
5364
5716
|
ms: Date.now() - attemptExpiryStartedAt,
|
|
@@ -5374,6 +5726,7 @@ export async function startRuntimeSheetDataset(
|
|
|
5374
5726
|
attemptOwnerRunId,
|
|
5375
5727
|
attemptExpiresAt,
|
|
5376
5728
|
attemptSeq,
|
|
5729
|
+
writeVersion,
|
|
5377
5730
|
physicalInsertColumnsSql,
|
|
5378
5731
|
physicalInsertValuesSql,
|
|
5379
5732
|
physicalRefreshSetSql:
|
|
@@ -5406,6 +5759,7 @@ export async function startRuntimeSheetDataset(
|
|
|
5406
5759
|
attemptOwnerRunId,
|
|
5407
5760
|
attemptExpiresAt,
|
|
5408
5761
|
attemptSeq,
|
|
5762
|
+
writeVersion,
|
|
5409
5763
|
timings,
|
|
5410
5764
|
force: input.force === true,
|
|
5411
5765
|
...prepared,
|
|
@@ -5423,6 +5777,7 @@ export async function startRuntimeSheetDataset(
|
|
|
5423
5777
|
...built,
|
|
5424
5778
|
attemptExpiresAt,
|
|
5425
5779
|
attemptSeq,
|
|
5780
|
+
writeVersion,
|
|
5426
5781
|
};
|
|
5427
5782
|
},
|
|
5428
5783
|
);
|
|
@@ -5442,6 +5797,7 @@ export async function startRuntimeSheetDataset(
|
|
|
5442
5797
|
attemptOwnerRunId,
|
|
5443
5798
|
attemptExpiresAt: result.attemptExpiresAt,
|
|
5444
5799
|
attemptSeq,
|
|
5800
|
+
writeVersion: result.writeVersion,
|
|
5445
5801
|
timings,
|
|
5446
5802
|
};
|
|
5447
5803
|
}
|
|
@@ -5751,6 +6107,7 @@ type CompleteRuntimeMapRowChunksInput = {
|
|
|
5751
6107
|
attemptOwnerRunId: string | null;
|
|
5752
6108
|
attemptExpiresAt: string | null;
|
|
5753
6109
|
attemptSeq: number;
|
|
6110
|
+
writeVersion: number | null;
|
|
5754
6111
|
normalizedPlayName: string;
|
|
5755
6112
|
normalizedTableNamespace: string;
|
|
5756
6113
|
outputFields: string[];
|
|
@@ -5759,6 +6116,8 @@ type CompleteRuntimeMapRowChunksInput = {
|
|
|
5759
6116
|
export type RuntimeMapRowsWriteResult = {
|
|
5760
6117
|
updated: number;
|
|
5761
6118
|
fencedKeys: string[];
|
|
6119
|
+
staleDropped?: number;
|
|
6120
|
+
staleDroppedKeys?: string[];
|
|
5762
6121
|
};
|
|
5763
6122
|
|
|
5764
6123
|
function writableEnrichedRuntimeSheetAttemptSql(
|
|
@@ -5847,6 +6206,8 @@ async function completeRuntimeMapRowChunks(
|
|
|
5847
6206
|
UPDATE ${sheetTable(session)} AS target
|
|
5848
6207
|
SET _status = 'enriched',
|
|
5849
6208
|
_run_id = $5::text,
|
|
6209
|
+
_writer_run_id = $5::text,
|
|
6210
|
+
_write_version = COALESCE($13::bigint, target._write_version),
|
|
5850
6211
|
_error = NULL,
|
|
5851
6212
|
_attempt_id = $9::text,
|
|
5852
6213
|
_attempt_owner_run_id = $10::text,
|
|
@@ -5857,6 +6218,8 @@ async function completeRuntimeMapRowChunks(
|
|
|
5857
6218
|
_cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
|
|
5858
6219
|
FROM matched_updates AS updates
|
|
5859
6220
|
WHERE target._key = updates.matched_key
|
|
6221
|
+
AND ($13::bigint IS NULL OR target._write_version = $13::bigint)
|
|
6222
|
+
AND ($13::bigint IS NOT NULL OR target._status <> 'enriched')
|
|
5860
6223
|
AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
|
|
5861
6224
|
AND NOT (${sameOwnerTerminalEpochSql})
|
|
5862
6225
|
AND (
|
|
@@ -5893,6 +6256,9 @@ async function completeRuntimeMapRowChunks(
|
|
|
5893
6256
|
SELECT 1 FROM applied_rows WHERE applied_rows._key = updates.matched_key
|
|
5894
6257
|
)
|
|
5895
6258
|
AND (
|
|
6259
|
+
($13::bigint IS NOT NULL AND target._write_version <> $13::bigint)
|
|
6260
|
+
OR ($13::bigint IS NULL AND target._status = 'enriched')
|
|
6261
|
+
OR
|
|
5896
6262
|
(${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
|
|
5897
6263
|
OR NOT (
|
|
5898
6264
|
($9::text IS NULL AND target._run_id = $5::text)
|
|
@@ -6002,12 +6368,18 @@ async function completeRuntimeMapRowChunks(
|
|
|
6002
6368
|
input.attemptOwnerRunId,
|
|
6003
6369
|
input.attemptExpiresAt,
|
|
6004
6370
|
input.attemptSeq,
|
|
6371
|
+
input.writeVersion,
|
|
6005
6372
|
],
|
|
6006
6373
|
);
|
|
6007
|
-
updated += Number(rows[0]?.updated ?? 0);
|
|
6008
|
-
fencedKeys.push(...(rows[0]?.fenced_keys ?? []));
|
|
6009
|
-
|
|
6010
6374
|
const matchedKeys = new Set(rows[0]?.matched_keys ?? []);
|
|
6375
|
+
const chunkFencedKeys = rows[0]?.fenced_keys ?? [];
|
|
6376
|
+
// An exact-version replay that already produced the same terminal value is
|
|
6377
|
+
// accepted idempotently even when SQL has no physical mutation to apply.
|
|
6378
|
+
// Stale versions remain excluded through fenced_keys.
|
|
6379
|
+
const appliedCount = Number(rows[0]?.updated ?? 0);
|
|
6380
|
+
updated += appliedCount;
|
|
6381
|
+
fencedKeys.push(...chunkFencedKeys);
|
|
6382
|
+
|
|
6011
6383
|
if (matchedKeys.size === chunk.length) {
|
|
6012
6384
|
continue;
|
|
6013
6385
|
}
|
|
@@ -6444,6 +6816,7 @@ async function failRuntimeMapRowChunks(
|
|
|
6444
6816
|
attemptOwnerRunId: string | null;
|
|
6445
6817
|
attemptExpiresAt: string | null;
|
|
6446
6818
|
attemptSeq: number;
|
|
6819
|
+
writeVersion: number | null;
|
|
6447
6820
|
normalizedPlayName: string;
|
|
6448
6821
|
normalizedTableNamespace: string;
|
|
6449
6822
|
},
|
|
@@ -6518,6 +6891,8 @@ async function failRuntimeMapRowChunks(
|
|
|
6518
6891
|
UPDATE ${sheetTable(session)} AS target
|
|
6519
6892
|
SET _status = 'failed',
|
|
6520
6893
|
_run_id = $6::text,
|
|
6894
|
+
_writer_run_id = $6::text,
|
|
6895
|
+
_write_version = COALESCE($16::bigint, target._write_version),
|
|
6521
6896
|
_error = updates.error,
|
|
6522
6897
|
_attempt_id = $11::text,
|
|
6523
6898
|
_attempt_owner_run_id = $12::text,
|
|
@@ -6528,6 +6903,7 @@ async function failRuntimeMapRowChunks(
|
|
|
6528
6903
|
_cell_meta = ${mergeRuntimeCellMetaPatchSql('target._cell_meta', 'updates.cell_meta_patch')}${input.physicalUpdateSetSql}
|
|
6529
6904
|
FROM matched_updates AS updates, ${sheetTable(session)} AS prev
|
|
6530
6905
|
WHERE target._key = updates.matched_key
|
|
6906
|
+
AND ($16::bigint IS NULL OR target._write_version = $16::bigint)
|
|
6531
6907
|
AND prev._key = target._key
|
|
6532
6908
|
AND ($15::boolean OR target._status <> 'enriched')
|
|
6533
6909
|
AND NOT (${newerTerminalRuntimeSheetRowSql('target', '$13::timestamptz', '$14::integer', '$11::text', '$12::text')})
|
|
@@ -6553,6 +6929,8 @@ async function failRuntimeMapRowChunks(
|
|
|
6553
6929
|
SELECT 1 FROM applied_rows WHERE applied_rows._key = updates.matched_key
|
|
6554
6930
|
)
|
|
6555
6931
|
AND (
|
|
6932
|
+
($16::bigint IS NOT NULL AND target._write_version <> $16::bigint)
|
|
6933
|
+
OR
|
|
6556
6934
|
(NOT $15::boolean AND target._status = 'enriched')
|
|
6557
6935
|
OR (${newerTerminalRuntimeSheetRowSql('target', '$13::timestamptz', '$14::integer', '$11::text', '$12::text')})
|
|
6558
6936
|
OR NOT (
|
|
@@ -6639,6 +7017,7 @@ async function failRuntimeMapRowChunks(
|
|
|
6639
7017
|
input.attemptExpiresAt,
|
|
6640
7018
|
input.attemptSeq,
|
|
6641
7019
|
input.forceTerminal === true,
|
|
7020
|
+
input.writeVersion,
|
|
6642
7021
|
],
|
|
6643
7022
|
);
|
|
6644
7023
|
updated += Number(rows[0]?.updated ?? 0);
|
|
@@ -6649,9 +7028,8 @@ async function failRuntimeMapRowChunks(
|
|
|
6649
7028
|
|
|
6650
7029
|
/**
|
|
6651
7030
|
* Mark map rows terminal in the per-run scoped Postgres sheet table by key.
|
|
6652
|
-
*
|
|
6653
|
-
*
|
|
6654
|
-
* columns, _status flipped to 'enriched', and the sheet cursor advanced.
|
|
7031
|
+
* Writes enriched output values into materialized physical columns, flips
|
|
7032
|
+
* _status to 'enriched', and advances the sheet cursor.
|
|
6655
7033
|
*
|
|
6656
7034
|
* Rows with `status: 'failed'` instead persist a row-isolated failure
|
|
6657
7035
|
* (`_status='failed'`, `_error`, per-cell failure meta) while keeping the
|
|
@@ -6674,6 +7052,7 @@ export async function completeRuntimeMapRows(
|
|
|
6674
7052
|
attemptOwnerRunId?: string | null;
|
|
6675
7053
|
attemptExpiresAt?: string | null;
|
|
6676
7054
|
attemptSeq?: number | null;
|
|
7055
|
+
writeVersion?: number | null;
|
|
6677
7056
|
forceFailedRows?: boolean;
|
|
6678
7057
|
},
|
|
6679
7058
|
): Promise<RuntimeMapRowsWriteResult> {
|
|
@@ -6757,6 +7136,15 @@ export async function completeRuntimeMapRows(
|
|
|
6757
7136
|
input.attemptOwnerRunId?.trim() || (attemptId ? input.runId : null);
|
|
6758
7137
|
const attemptExpiresAt = input.attemptExpiresAt?.trim() || null;
|
|
6759
7138
|
const attemptSeq = normalizeRuntimeRunAttempt(input.attemptSeq);
|
|
7139
|
+
const writeVersion = input.writeVersion == null ? null : input.writeVersion;
|
|
7140
|
+
if (
|
|
7141
|
+
writeVersion !== null &&
|
|
7142
|
+
(!Number.isSafeInteger(writeVersion) || writeVersion <= 0)
|
|
7143
|
+
) {
|
|
7144
|
+
throw new Error(
|
|
7145
|
+
'Runtime sheet writeVersion must be a positive safe integer.',
|
|
7146
|
+
);
|
|
7147
|
+
}
|
|
6760
7148
|
|
|
6761
7149
|
return await withRuntimeSheetQueryClient(
|
|
6762
7150
|
context,
|
|
@@ -6780,6 +7168,7 @@ export async function completeRuntimeMapRows(
|
|
|
6780
7168
|
attemptOwnerRunId,
|
|
6781
7169
|
attemptExpiresAt,
|
|
6782
7170
|
attemptSeq,
|
|
7171
|
+
writeVersion,
|
|
6783
7172
|
normalizedPlayName: normalizePlayNameForSheet(session.playName),
|
|
6784
7173
|
normalizedTableNamespace: normalizeTableNamespace(
|
|
6785
7174
|
input.tableNamespace,
|
|
@@ -6813,6 +7202,7 @@ export async function completeRuntimeMapRows(
|
|
|
6813
7202
|
attemptOwnerRunId,
|
|
6814
7203
|
attemptExpiresAt,
|
|
6815
7204
|
attemptSeq,
|
|
7205
|
+
writeVersion,
|
|
6816
7206
|
normalizedPlayName: normalizePlayNameForSheet(session.playName),
|
|
6817
7207
|
normalizedTableNamespace: normalizeTableNamespace(
|
|
6818
7208
|
input.tableNamespace,
|
|
@@ -6822,6 +7212,8 @@ export async function completeRuntimeMapRows(
|
|
|
6822
7212
|
return {
|
|
6823
7213
|
updated: completed.updated + failed.updated,
|
|
6824
7214
|
fencedKeys: [...completed.fencedKeys, ...failed.fencedKeys],
|
|
7215
|
+
staleDropped: completed.fencedKeys.length + failed.fencedKeys.length,
|
|
7216
|
+
staleDroppedKeys: [...completed.fencedKeys, ...failed.fencedKeys],
|
|
6825
7217
|
};
|
|
6826
7218
|
},
|
|
6827
7219
|
);
|