deepline 0.3.174 → 0.3.176
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/sdk/src/client.ts +3 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/integration-contracts/waterfall/execution-policy.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/runtime-sheet-orm.ts +489 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/runtime-sheet-storage-contract.ts +4 -13
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +143 -110
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +73 -151
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +113 -7
- package/dist/cli/index.js +13 -1
- package/dist/cli/index.mjs +13 -1
- package/dist/index.js +13 -1
- package/dist/index.mjs +13 -1
- package/dist/install-integrity.json +2 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -25,6 +25,13 @@ import {
|
|
|
25
25
|
type PhysicalSheetColumnProjection,
|
|
26
26
|
} from '../play-data-plane/sheet-contract';
|
|
27
27
|
import { RUNTIME_SHEET_CELL_META_COLUMN } from '../play-data-plane/runtime-sheet-storage-contract';
|
|
28
|
+
import {
|
|
29
|
+
createRuntimeSheetOrm,
|
|
30
|
+
isRuntimeSheetOwnerMetadataConstraintError,
|
|
31
|
+
RUNTIME_SHEET_ATTEMPT_COLUMNS,
|
|
32
|
+
RUNTIME_SHEET_TIMESTAMP_COLUMNS,
|
|
33
|
+
RUNTIME_SHEET_VERSION_SEQUENCE,
|
|
34
|
+
} from '../play-data-plane/runtime-sheet-orm';
|
|
28
35
|
import type {
|
|
29
36
|
CreateDbSessionResponse,
|
|
30
37
|
DbLogicalTable,
|
|
@@ -316,13 +323,6 @@ const APPEND_KEY_SUFFIX_LENGTH = 12;
|
|
|
316
323
|
// 60-min alignment fixed. A shorter TTL only causes periodic re-mints (cheap;
|
|
317
324
|
// the tenant runtime role is long-lived), not run failures.
|
|
318
325
|
const RUNTIME_DB_SESSION_TTL_SECONDS = 10 * 60;
|
|
319
|
-
const RUNTIME_SHEET_ATTEMPT_COLUMNS = [
|
|
320
|
-
'_attempt_id',
|
|
321
|
-
'_attempt_owner_run_id',
|
|
322
|
-
'_attempt_seq',
|
|
323
|
-
'_attempt_expires_at',
|
|
324
|
-
] as const;
|
|
325
|
-
const RUNTIME_SHEET_TIMESTAMP_COLUMNS = ['_created_at', '_updated_at'] as const;
|
|
326
326
|
const RUNTIME_SHEET_ENSURE_CACHE_TTL_MS = 10 * 60_000;
|
|
327
327
|
const RUNTIME_SHEET_READY_AFTER_ENSURE_RETRY_DELAYS_MS = [
|
|
328
328
|
100, 250, 500, 1_000, 2_000,
|
|
@@ -519,6 +519,7 @@ type RuntimeApiActionRequest =
|
|
|
519
519
|
tableNamespace: string;
|
|
520
520
|
sheetContract?: PlaySheetContract | null;
|
|
521
521
|
userEmail?: string | null;
|
|
522
|
+
forceRepair?: boolean;
|
|
522
523
|
}
|
|
523
524
|
| {
|
|
524
525
|
action: 'create_db_session';
|
|
@@ -1135,62 +1136,21 @@ async function isRuntimeSheetSchemaReady(
|
|
|
1135
1136
|
FROM ${sheetTable(session)}
|
|
1136
1137
|
LIMIT 0`,
|
|
1137
1138
|
);
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
[
|
|
1150
|
-
|
|
1151
|
-
);
|
|
1152
|
-
|
|
1153
|
-
`
|
|
1154
|
-
SELECT count(*)::int AS ready_count
|
|
1155
|
-
FROM information_schema.columns
|
|
1156
|
-
WHERE table_schema = $1
|
|
1157
|
-
AND table_name = $2
|
|
1158
|
-
AND column_name = ANY($3::text[])
|
|
1159
|
-
AND data_type = 'timestamp with time zone'
|
|
1160
|
-
AND is_nullable = 'NO'
|
|
1161
|
-
AND column_default IS NOT NULL
|
|
1162
|
-
`,
|
|
1163
|
-
[
|
|
1164
|
-
session.postgres.schema,
|
|
1165
|
-
session.postgres.sheetTable,
|
|
1166
|
-
[...RUNTIME_SHEET_TIMESTAMP_COLUMNS],
|
|
1167
|
-
],
|
|
1168
|
-
);
|
|
1169
|
-
const cellMetaColumn = await client.query<Record<string, unknown>>(
|
|
1170
|
-
`
|
|
1171
|
-
SELECT count(*)::int AS ready_count
|
|
1172
|
-
FROM information_schema.columns
|
|
1173
|
-
WHERE table_schema = $1
|
|
1174
|
-
AND table_name = $2
|
|
1175
|
-
AND column_name = $3
|
|
1176
|
-
AND data_type = $4
|
|
1177
|
-
AND is_nullable = 'NO'
|
|
1178
|
-
AND column_default IS NOT NULL
|
|
1179
|
-
`,
|
|
1180
|
-
[
|
|
1181
|
-
session.postgres.schema,
|
|
1182
|
-
session.postgres.sheetTable,
|
|
1183
|
-
RUNTIME_SHEET_CELL_META_COLUMN.name,
|
|
1184
|
-
RUNTIME_SHEET_CELL_META_COLUMN.dataType,
|
|
1185
|
-
],
|
|
1186
|
-
);
|
|
1187
|
-
return (
|
|
1188
|
-
Number(attemptColumns.rows[0]?.present_count ?? 0) ===
|
|
1189
|
-
RUNTIME_SHEET_ATTEMPT_COLUMNS.length &&
|
|
1190
|
-
Number(timestampColumns.rows[0]?.ready_count ?? 0) ===
|
|
1191
|
-
RUNTIME_SHEET_TIMESTAMP_COLUMNS.length &&
|
|
1192
|
-
Number(cellMetaColumn.rows[0]?.ready_count ?? 0) === 1
|
|
1193
|
-
);
|
|
1139
|
+
const orm = createRuntimeSheetOrm({
|
|
1140
|
+
schema: session.postgres.schema,
|
|
1141
|
+
table: session.postgres.sheetTable,
|
|
1142
|
+
query: async <Row = Record<string, unknown>>(
|
|
1143
|
+
sql: string,
|
|
1144
|
+
params?: readonly unknown[],
|
|
1145
|
+
): Promise<{ rows: Row[] }> => {
|
|
1146
|
+
const result = await client.query<Row & Record<string, unknown>>(
|
|
1147
|
+
sql,
|
|
1148
|
+
params === undefined ? undefined : [...params],
|
|
1149
|
+
);
|
|
1150
|
+
return { rows: result.rows as Row[] };
|
|
1151
|
+
},
|
|
1152
|
+
});
|
|
1153
|
+
return (await orm.readiness()).ready;
|
|
1194
1154
|
};
|
|
1195
1155
|
try {
|
|
1196
1156
|
if (isRuntimeOneShotQueryFactoryRegistered()) {
|
|
@@ -1250,15 +1210,31 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1250
1210
|
`${quoteIdentifier(session.postgres.schema)}.${quoteIdentifier(session.postgres.sheetTable)}`,
|
|
1251
1211
|
],
|
|
1252
1212
|
);
|
|
1213
|
+
const orm = createRuntimeSheetOrm({
|
|
1214
|
+
schema: session.postgres.schema,
|
|
1215
|
+
table: session.postgres.sheetTable,
|
|
1216
|
+
query: async <Row = Record<string, unknown>>(
|
|
1217
|
+
sql: string,
|
|
1218
|
+
params?: readonly unknown[],
|
|
1219
|
+
): Promise<{ rows: Row[] }> => {
|
|
1220
|
+
const result = await client.query<Row & Record<string, unknown>>(
|
|
1221
|
+
sql,
|
|
1222
|
+
params === undefined ? undefined : [...params],
|
|
1223
|
+
);
|
|
1224
|
+
return { rows: result.rows as Row[] };
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
const ownerSchema = await orm.inspect();
|
|
1228
|
+
const ownerReadiness = await orm.readiness(ownerSchema);
|
|
1253
1229
|
const visible = new Set(visibleColumns.rows.map((row) => row.column_name));
|
|
1254
1230
|
const timestampColumns = new Map(
|
|
1255
|
-
|
|
1256
|
-
.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1231
|
+
[...ownerSchema.columns.entries()].filter(([column]) =>
|
|
1232
|
+
RUNTIME_SHEET_TIMESTAMP_COLUMNS.includes(column),
|
|
1233
|
+
),
|
|
1234
|
+
);
|
|
1235
|
+
const versionColumn = ownerSchema.columns.get('_version');
|
|
1236
|
+
const cellMetaColumn = ownerSchema.columns.get(
|
|
1237
|
+
RUNTIME_SHEET_CELL_META_COLUMN.name,
|
|
1262
1238
|
);
|
|
1263
1239
|
return {
|
|
1264
1240
|
schema: session.postgres.schema,
|
|
@@ -1268,6 +1244,16 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1268
1244
|
canSelect: privileges.rows[0]?.can_select ?? null,
|
|
1269
1245
|
canInsert: privileges.rows[0]?.can_insert ?? null,
|
|
1270
1246
|
canUpdate: privileges.rows[0]?.can_update ?? null,
|
|
1247
|
+
ownerSchemaReady: ownerReadiness.ready,
|
|
1248
|
+
ownerSchemaMissingColumns: ownerReadiness.missingColumns,
|
|
1249
|
+
ownerSchemaIncompatibleColumns: ownerReadiness.incompatibleColumns,
|
|
1250
|
+
ownerSchemaMissingDefaults: ownerReadiness.missingDefaults,
|
|
1251
|
+
versionSequenceExists: ownerSchema.sequenceExists,
|
|
1252
|
+
versionColumnState: {
|
|
1253
|
+
dataType: versionColumn?.data_type ?? null,
|
|
1254
|
+
isNullable: versionColumn?.is_nullable ?? null,
|
|
1255
|
+
hasDefault: versionColumn?.column_default != null,
|
|
1256
|
+
},
|
|
1271
1257
|
visibleColumnCount: visible.size,
|
|
1272
1258
|
missingAttemptColumns: RUNTIME_SHEET_ATTEMPT_COLUMNS.filter(
|
|
1273
1259
|
(column) => !visible.has(column),
|
|
@@ -1282,14 +1268,10 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1282
1268
|
};
|
|
1283
1269
|
}),
|
|
1284
1270
|
cellMetaColumnState: (() => {
|
|
1285
|
-
const state = visibleColumns.rows.find(
|
|
1286
|
-
(column) =>
|
|
1287
|
-
column.column_name === RUNTIME_SHEET_CELL_META_COLUMN.name,
|
|
1288
|
-
);
|
|
1289
1271
|
return {
|
|
1290
|
-
dataType:
|
|
1291
|
-
isNullable:
|
|
1292
|
-
hasDefault:
|
|
1272
|
+
dataType: cellMetaColumn?.data_type ?? null,
|
|
1273
|
+
isNullable: cellMetaColumn?.is_nullable ?? null,
|
|
1274
|
+
hasDefault: cellMetaColumn?.column_default != null,
|
|
1293
1275
|
};
|
|
1294
1276
|
})(),
|
|
1295
1277
|
};
|
|
@@ -2483,6 +2465,7 @@ async function withRuntimeSheetProvisioningRetry<T>(
|
|
|
2483
2465
|
playName: string;
|
|
2484
2466
|
tableNamespace: string;
|
|
2485
2467
|
sheetContract: PlaySheetContract;
|
|
2468
|
+
session: RuntimePostgresSession;
|
|
2486
2469
|
timings?: RuntimeSheetTiming[];
|
|
2487
2470
|
},
|
|
2488
2471
|
operation: () => Promise<T>,
|
|
@@ -2505,12 +2488,33 @@ async function withRuntimeSheetProvisioningRetry<T>(
|
|
|
2505
2488
|
});
|
|
2506
2489
|
if (isMissingRelationError(error)) {
|
|
2507
2490
|
const ensureStartedAt = Date.now();
|
|
2508
|
-
await ensureRuntimeSheet(context,
|
|
2491
|
+
await ensureRuntimeSheet(context, {
|
|
2492
|
+
playName: input.playName,
|
|
2493
|
+
tableNamespace: input.tableNamespace,
|
|
2494
|
+
sheetContract: input.sheetContract,
|
|
2495
|
+
});
|
|
2509
2496
|
input.timings?.push({
|
|
2510
2497
|
phase: 'ensure_sheet_after_provisioning_error',
|
|
2511
2498
|
ms: Date.now() - ensureStartedAt,
|
|
2512
2499
|
retried: true,
|
|
2513
2500
|
});
|
|
2501
|
+
} else if (
|
|
2502
|
+
isRuntimeSheetOwnerMetadataConstraintError(error, {
|
|
2503
|
+
table: input.session.postgres.sheetTable,
|
|
2504
|
+
})
|
|
2505
|
+
) {
|
|
2506
|
+
const ensureStartedAt = Date.now();
|
|
2507
|
+
await ensureRuntimeSheet(context, {
|
|
2508
|
+
playName: input.playName,
|
|
2509
|
+
tableNamespace: input.tableNamespace,
|
|
2510
|
+
sheetContract: input.sheetContract,
|
|
2511
|
+
forceRepair: true,
|
|
2512
|
+
});
|
|
2513
|
+
input.timings?.push({
|
|
2514
|
+
phase: 'force_repair_sheet_after_metadata_constraint_error',
|
|
2515
|
+
ms: Date.now() - ensureStartedAt,
|
|
2516
|
+
retried: true,
|
|
2517
|
+
});
|
|
2514
2518
|
} else if (isPostgresPermissionDeniedError(error)) {
|
|
2515
2519
|
const repairStartedAt = Date.now();
|
|
2516
2520
|
await repairRuntimeStorageGrants(context, {
|
|
@@ -2551,7 +2555,7 @@ async function withRuntimeSheetQueryClient<T>(
|
|
|
2551
2555
|
const totalStartedAt = Date.now();
|
|
2552
2556
|
const result = await withRuntimeSheetProvisioningRetry(
|
|
2553
2557
|
context,
|
|
2554
|
-
input,
|
|
2558
|
+
{ ...input, session },
|
|
2555
2559
|
async () => {
|
|
2556
2560
|
if (
|
|
2557
2561
|
!input.transactional &&
|
|
@@ -2865,12 +2869,10 @@ async function withRuntimeWorkReceiptClient<T>(
|
|
|
2865
2869
|
throw new Error('Runtime work receipt query failed after retries.');
|
|
2866
2870
|
}
|
|
2867
2871
|
|
|
2868
|
-
const PLAY_INTERNAL_SHEET_VERSION_SEQUENCE = '_deepline_sheet_version_seq';
|
|
2869
|
-
|
|
2870
2872
|
function nextRuntimeSheetVersionExpression(
|
|
2871
2873
|
session: RuntimePostgresSession,
|
|
2872
2874
|
): string {
|
|
2873
|
-
return `nextval(${quoteLiteral(`${session.postgres.schema}.${
|
|
2875
|
+
return `nextval(${quoteLiteral(`${session.postgres.schema}.${RUNTIME_SHEET_VERSION_SEQUENCE}`)}::regclass)`;
|
|
2874
2876
|
}
|
|
2875
2877
|
|
|
2876
2878
|
function missingOutputCellSql(
|
|
@@ -3476,6 +3478,23 @@ async function writeRuntimeRows(
|
|
|
3476
3478
|
});
|
|
3477
3479
|
}
|
|
3478
3480
|
|
|
3481
|
+
async function writeRuntimeRowsWithProvisioningRetry(
|
|
3482
|
+
context: RuntimeApiContext,
|
|
3483
|
+
session: RuntimePostgresSession,
|
|
3484
|
+
input: Parameters<typeof writeRuntimeRows>[1],
|
|
3485
|
+
): Promise<RowsWriteResponse> {
|
|
3486
|
+
return await withRuntimeSheetProvisioningRetry(
|
|
3487
|
+
context,
|
|
3488
|
+
{
|
|
3489
|
+
playName: session.playName,
|
|
3490
|
+
tableNamespace: input.tableNamespace,
|
|
3491
|
+
sheetContract: input.sheetContract,
|
|
3492
|
+
session,
|
|
3493
|
+
},
|
|
3494
|
+
() => writeRuntimeRows(session, input),
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3479
3498
|
export async function resolveRuntimeReferencedPlay(
|
|
3480
3499
|
context: RuntimeApiContext,
|
|
3481
3500
|
playRef: string,
|
|
@@ -3509,6 +3528,7 @@ export async function ensureRuntimeSheet(
|
|
|
3509
3528
|
playName: string;
|
|
3510
3529
|
tableNamespace: string;
|
|
3511
3530
|
sheetContract: PlaySheetContract;
|
|
3531
|
+
forceRepair?: boolean;
|
|
3512
3532
|
},
|
|
3513
3533
|
): Promise<void> {
|
|
3514
3534
|
const request = {
|
|
@@ -3516,6 +3536,7 @@ export async function ensureRuntimeSheet(
|
|
|
3516
3536
|
...input,
|
|
3517
3537
|
runId: context.runId ?? null,
|
|
3518
3538
|
userEmail: normalizeRuntimeUserEmail(context.userEmail),
|
|
3539
|
+
...(input.forceRepair === true ? { forceRepair: true } : {}),
|
|
3519
3540
|
};
|
|
3520
3541
|
try {
|
|
3521
3542
|
await postRuntimeApi<{ ok: true }>(context, request);
|
|
@@ -9124,18 +9145,22 @@ export async function upsertRuntimeRows(
|
|
|
9124
9145
|
},
|
|
9125
9146
|
);
|
|
9126
9147
|
return rowsWriteResponseSchema.parse(
|
|
9127
|
-
await
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
.
|
|
9134
|
-
|
|
9135
|
-
)
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9148
|
+
await writeRuntimeRowsWithProvisioningRetry(
|
|
9149
|
+
context,
|
|
9150
|
+
requireRuntimePostgresSession(session),
|
|
9151
|
+
{
|
|
9152
|
+
tableNamespace: input.tableNamespace,
|
|
9153
|
+
rows: input.rows,
|
|
9154
|
+
runId: input.runId,
|
|
9155
|
+
sheetContract: input.sheetContract,
|
|
9156
|
+
idempotencyKey: createHash('sha1')
|
|
9157
|
+
.update(
|
|
9158
|
+
`${input.playName}:${input.tableNamespace}:${input.runId}:${JSON.stringify(input.rows)}`,
|
|
9159
|
+
)
|
|
9160
|
+
.digest('hex'),
|
|
9161
|
+
mode: 'upsert',
|
|
9162
|
+
},
|
|
9163
|
+
),
|
|
9139
9164
|
);
|
|
9140
9165
|
}
|
|
9141
9166
|
|
|
@@ -9170,14 +9195,18 @@ export async function appendRuntimeRows(
|
|
|
9170
9195
|
},
|
|
9171
9196
|
);
|
|
9172
9197
|
return rowsWriteResponseSchema.parse(
|
|
9173
|
-
await
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9198
|
+
await writeRuntimeRowsWithProvisioningRetry(
|
|
9199
|
+
context,
|
|
9200
|
+
requireRuntimePostgresSession(session),
|
|
9201
|
+
{
|
|
9202
|
+
tableNamespace: input.tableNamespace,
|
|
9203
|
+
rows: input.rows,
|
|
9204
|
+
runId: input.runId,
|
|
9205
|
+
sheetContract: input.sheetContract,
|
|
9206
|
+
idempotencyKey: input.idempotencyKey,
|
|
9207
|
+
mode: 'append',
|
|
9208
|
+
},
|
|
9209
|
+
),
|
|
9181
9210
|
);
|
|
9182
9211
|
}
|
|
9183
9212
|
|
|
@@ -9212,14 +9241,18 @@ export async function replaceRuntimeRows(
|
|
|
9212
9241
|
},
|
|
9213
9242
|
);
|
|
9214
9243
|
return rowsWriteResponseSchema.parse(
|
|
9215
|
-
await
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9244
|
+
await writeRuntimeRowsWithProvisioningRetry(
|
|
9245
|
+
context,
|
|
9246
|
+
requireRuntimePostgresSession(session),
|
|
9247
|
+
{
|
|
9248
|
+
tableNamespace: input.tableNamespace,
|
|
9249
|
+
rows: input.rows,
|
|
9250
|
+
runId: input.runId,
|
|
9251
|
+
sheetContract: input.sheetContract,
|
|
9252
|
+
idempotencyKey: input.idempotencyKey,
|
|
9253
|
+
mode: 'replace',
|
|
9254
|
+
},
|
|
9255
|
+
),
|
|
9223
9256
|
);
|
|
9224
9257
|
}
|
|
9225
9258
|
|
|
@@ -14,33 +14,27 @@
|
|
|
14
14
|
* Persist the absurd task id
|
|
15
15
|
* on the run row so a later cancel can target it.
|
|
16
16
|
* observe/result → reuse the postgres LISTEN/NOTIFY run handle.
|
|
17
|
-
* cancel →
|
|
18
|
-
*
|
|
17
|
+
* cancel → one bounded transaction cancels the Absurd task and terminalizes
|
|
18
|
+
* the postgres scheduler run. Success means both states committed.
|
|
19
19
|
* signal → durable signal row (authoritative payload) + absurd emitEvent
|
|
20
20
|
* (the wakeup only; payload is re-read from the signal row).
|
|
21
21
|
*/
|
|
22
22
|
import { recordPerfTrace } from '../perf-trace';
|
|
23
23
|
import { CTX_RUN_PLAY_INLINE_ONLY } from '../child-execution-strategy';
|
|
24
24
|
import { PLAY_ADMISSION_MAX_ACTIVE_LIFECYCLES } from '../admission-transport-policy';
|
|
25
|
-
import {
|
|
26
|
-
defaultRuntimeSandboxReconciliation,
|
|
27
|
-
runtimeSandboxDeleteSucceeded,
|
|
28
|
-
runtimeSandboxRefIdentityKey,
|
|
29
|
-
runtimeSandboxRefFromResource,
|
|
30
|
-
type RuntimeSandboxRef,
|
|
31
|
-
} from '../runner-backends/runtime-sandbox-reconciliation';
|
|
32
25
|
import {
|
|
33
26
|
PLAY_SCHEDULER_BACKENDS,
|
|
34
|
-
RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE,
|
|
35
27
|
type PlaySchedulerBackend,
|
|
36
28
|
type PlaySchedulerRunHandle,
|
|
37
29
|
type PlaySchedulerSubmitInput,
|
|
38
30
|
} from '../scheduler-backend';
|
|
39
31
|
import {
|
|
32
|
+
cancelPostgresSchedulerRun,
|
|
33
|
+
postgresSchedulerCancellationOutboxLease,
|
|
40
34
|
postgresSchedulerRunHandle,
|
|
41
35
|
readPostgresSchedulerLaunch,
|
|
42
36
|
readPostgresSchedulerAbsurdTaskId,
|
|
43
|
-
|
|
37
|
+
readPostgresSchedulerResult,
|
|
44
38
|
recordPostgresSchedulerSignal,
|
|
45
39
|
} from './postgres';
|
|
46
40
|
import {
|
|
@@ -57,7 +51,6 @@ import { withRuntimeSchedulerSubmitAdmission } from './submit-admission';
|
|
|
57
51
|
import {
|
|
58
52
|
ABSURD_PLAY_RUN_MAX_ATTEMPTS,
|
|
59
53
|
ABSURD_PLAY_RUN_RETRY_STRATEGY,
|
|
60
|
-
ABSURD_DAYTONA_SANDBOX_CLEANUP_TASK,
|
|
61
54
|
absurdIntegrationEventName,
|
|
62
55
|
absurdQueueNameForRelease,
|
|
63
56
|
absurdReleaseIdForLaunch,
|
|
@@ -67,7 +60,6 @@ import {
|
|
|
67
60
|
normalizeAbsurdSchedulerOptions,
|
|
68
61
|
resolveAbsurdPoolOptions,
|
|
69
62
|
serverlessAdmissionSubmitMaxActive,
|
|
70
|
-
type AbsurdDaytonaSandboxCleanupTaskParams,
|
|
71
63
|
type AbsurdSchedulerOptions,
|
|
72
64
|
} from './absurd-shared';
|
|
73
65
|
|
|
@@ -162,168 +154,98 @@ function absurdRunHandle(input: {
|
|
|
162
154
|
const runsQueue = absurdQueueNameForRelease(releaseId);
|
|
163
155
|
const cancellationKey = `${poolOptions.url}::${effectiveOptions.schema ?? ''}::${input.runId}`;
|
|
164
156
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
readPostgresSchedulerAbsurdTaskId(client, input.runId, effectiveOptions),
|
|
168
|
-
);
|
|
169
|
-
if (!taskId) return;
|
|
170
|
-
// Keep cancellation on the same bounded control-plane contract as the
|
|
171
|
-
// scheduler terminal write. This is a durable lane transition, not an
|
|
172
|
-
// unbounded best-effort query which can pile up behind a worker lock.
|
|
173
|
-
await withRuntimeSchedulerTransaction(
|
|
157
|
+
const cancelAbsurdRunSynchronously = async (reason?: string) =>
|
|
158
|
+
withRuntimeSchedulerTransaction(
|
|
174
159
|
poolOptions,
|
|
175
160
|
{
|
|
176
161
|
lockTimeoutMs: RUNTIME_SCHEDULER_CANCEL_LOCK_TIMEOUT_MS,
|
|
177
162
|
statementTimeoutMs: RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS,
|
|
178
163
|
},
|
|
179
164
|
async (client) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
165
|
+
// Resolve the immutable release lane inside the same checkout and
|
|
166
|
+
// transaction that performs cancellation. The stop endpoint therefore
|
|
167
|
+
// has no preliminary lane lookup that can succeed or fail separately.
|
|
168
|
+
const launch = await readPostgresSchedulerLaunch(
|
|
169
|
+
client,
|
|
170
|
+
input.runId,
|
|
171
|
+
effectiveOptions,
|
|
172
|
+
{ caller: 'play_run_control' },
|
|
173
|
+
);
|
|
174
|
+
if (!launch) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Cannot cancel play run "${input.runId}": scheduler launch is missing.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const cancellationReleaseId = absurdReleaseIdForLaunch(launch);
|
|
180
|
+
const cancellationQueue = absurdQueueNameForRelease(
|
|
181
|
+
cancellationReleaseId,
|
|
182
|
+
);
|
|
183
|
+
const taskId = await readPostgresSchedulerAbsurdTaskId(
|
|
184
|
+
client,
|
|
185
|
+
input.runId,
|
|
186
|
+
effectiveOptions,
|
|
187
|
+
);
|
|
188
|
+
if (taskId) {
|
|
189
|
+
await client.query(`SELECT absurd.cancel_task($1, $2::uuid)`, [
|
|
190
|
+
cancellationQueue,
|
|
191
|
+
taskId,
|
|
192
|
+
]);
|
|
193
|
+
// Older v4 Absurd functions do not materialize a manual cancellation
|
|
194
|
+
// deadline or reason. Stamp both in the adapter so existing queues
|
|
195
|
+
// participate in the same durable reconciliation lane without a
|
|
196
|
+
// schema cutover. The explicit marker keeps a user cancel distinct
|
|
197
|
+
// from a task cancelled by its configured max_delay sweep.
|
|
198
|
+
await client.query(
|
|
199
|
+
`UPDATE absurd.${quotePostgresIdentifier(`t_${cancellationQueue}`)}
|
|
191
200
|
SET cancel_at = COALESCE(cancel_at, clock_timestamp()),
|
|
192
201
|
cancellation = COALESCE(cancellation, '{}'::jsonb)
|
|
193
202
|
|| jsonb_build_object('manual', true)
|
|
194
203
|
WHERE task_id = $1::uuid
|
|
195
204
|
AND state = 'cancelled'`,
|
|
196
|
-
|
|
205
|
+
[taskId],
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const cancelled = await cancelPostgresSchedulerRun(
|
|
209
|
+
client,
|
|
210
|
+
{
|
|
211
|
+
runId: input.runId,
|
|
212
|
+
reason,
|
|
213
|
+
outboxLease: postgresSchedulerCancellationOutboxLease(input.runId),
|
|
214
|
+
},
|
|
215
|
+
effectiveOptions,
|
|
197
216
|
);
|
|
217
|
+
// The update returning a row is proof that this transaction wrote the
|
|
218
|
+
// terminal state and outbox event. Only the idempotent retry/race path
|
|
219
|
+
// needs one read to prove another terminal writer already won.
|
|
220
|
+
if (!cancelled) {
|
|
221
|
+
const terminal = await readPostgresSchedulerResult(
|
|
222
|
+
client,
|
|
223
|
+
input.runId,
|
|
224
|
+
effectiveOptions,
|
|
225
|
+
);
|
|
226
|
+
if (!terminal) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Cancellation did not terminalize scheduler run "${input.runId}".`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
198
232
|
},
|
|
199
233
|
{
|
|
200
234
|
runId: input.runId,
|
|
201
|
-
phase: '
|
|
235
|
+
phase: 'cancel_run_synchronously',
|
|
202
236
|
priority: 'critical',
|
|
203
237
|
},
|
|
204
|
-
{ maxAttempts: 1 },
|
|
205
238
|
);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const cancellationPendingError = (cause: unknown): Error => {
|
|
209
|
-
const error = new Error(
|
|
210
|
-
`Run cancellation was durably requested, but scheduler terminalization is pending for ${input.runId}.`,
|
|
211
|
-
);
|
|
212
|
-
Object.assign(error, {
|
|
213
|
-
code: RUNTIME_SCHEDULER_CANCELLATION_PENDING_CODE,
|
|
214
|
-
cause,
|
|
215
|
-
});
|
|
216
|
-
return error;
|
|
217
|
-
};
|
|
218
239
|
|
|
219
240
|
const requestCancellation = async (reason?: string) => {
|
|
220
241
|
await runCancellationSingleFlight(cancellationKey, async () => {
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
await
|
|
225
|
-
try {
|
|
226
|
-
await delegate.cancel(reason);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
throw cancellationPendingError(error);
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
await cleanupPersistedRuntimeSandboxes();
|
|
232
|
-
} catch (error) {
|
|
233
|
-
// The scheduler terminal transaction already won. Its cleanup jobs are
|
|
234
|
-
// durable, so a best-effort eager cleanup failure must not turn a real
|
|
235
|
-
// cancellation into a false 502 or cause the caller to retry the
|
|
236
|
-
// terminal write.
|
|
237
|
-
console.warn('[absurd.runtime_sandbox_cleanup_deferred]', {
|
|
238
|
-
runId: input.runId,
|
|
239
|
-
error: error instanceof Error ? error.message : String(error),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
+
// Engine cancellation and the scheduler terminal write commit together.
|
|
243
|
+
// A timeout rolls both back, so returning means the run is terminal and
|
|
244
|
+
// throwing means the caller can safely retry one unchanged operation.
|
|
245
|
+
await cancelAbsurdRunSynchronously(reason);
|
|
242
246
|
});
|
|
243
247
|
};
|
|
244
248
|
|
|
245
|
-
const cleanupPersistedRuntimeSandboxes = async () => {
|
|
246
|
-
const resources = await withRuntimeSchedulerClient(poolOptions, (client) =>
|
|
247
|
-
readPostgresSchedulerRuntimeResourcesForRun(
|
|
248
|
-
client,
|
|
249
|
-
{ runId: input.runId },
|
|
250
|
-
effectiveOptions,
|
|
251
|
-
),
|
|
252
|
-
);
|
|
253
|
-
const productionRefs = new Map<string, RuntimeSandboxRef>();
|
|
254
|
-
const workerOwnedRefs = new Map<string, RuntimeSandboxRef>();
|
|
255
|
-
for (const resource of resources) {
|
|
256
|
-
if (
|
|
257
|
-
!resource ||
|
|
258
|
-
typeof resource !== 'object' ||
|
|
259
|
-
Array.isArray(resource)
|
|
260
|
-
) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const record = resource as Record<string, unknown>;
|
|
264
|
-
const ref = runtimeSandboxRefFromResource(record);
|
|
265
|
-
if (!ref) continue;
|
|
266
|
-
const refKey = runtimeSandboxRefIdentityKey(ref);
|
|
267
|
-
// Modal credentials and all preview provider credentials are worker-only.
|
|
268
|
-
// This explicit task keeps deletion on the owning release lane. Legacy
|
|
269
|
-
// Daytona records predate runtimeEnvironment, so retain their marker.
|
|
270
|
-
const runtimeEnvironment =
|
|
271
|
-
record.runtimeEnvironment === 'preview' ||
|
|
272
|
-
record.daytonaEnvironment === 'preview'
|
|
273
|
-
? 'preview'
|
|
274
|
-
: 'production';
|
|
275
|
-
if (runtimeEnvironment === 'preview' || ref.provider === 'modal') {
|
|
276
|
-
workerOwnedRefs.set(refKey, ref);
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
productionRefs.set(refKey, ref);
|
|
280
|
-
}
|
|
281
|
-
if (workerOwnedRefs.size > 0) {
|
|
282
|
-
const absurd = createAbsurdRuntimeClient(poolOptions, runsQueue);
|
|
283
|
-
await Promise.all(
|
|
284
|
-
[...workerOwnedRefs.values()].map((runtimeSandboxRef) => {
|
|
285
|
-
// sandboxId remains in the payload for workers deployed before the
|
|
286
|
-
// provider-scoped cleanup cutover. New workers use the versioned ref.
|
|
287
|
-
const params: AbsurdDaytonaSandboxCleanupTaskParams & {
|
|
288
|
-
runtimeSandboxRef: RuntimeSandboxRef;
|
|
289
|
-
} = {
|
|
290
|
-
runId: input.runId,
|
|
291
|
-
sandboxId: runtimeSandboxRef.resourceId,
|
|
292
|
-
runtimeSandboxRef,
|
|
293
|
-
};
|
|
294
|
-
return absurd.spawn(ABSURD_DAYTONA_SANDBOX_CLEANUP_TASK, params, {
|
|
295
|
-
queue: runsQueue,
|
|
296
|
-
idempotencyKey: `sandbox-cleanup:${input.runId}:${runtimeSandboxRefIdentityKey(runtimeSandboxRef)}`,
|
|
297
|
-
maxAttempts: ABSURD_PLAY_RUN_MAX_ATTEMPTS,
|
|
298
|
-
retryStrategy: ABSURD_PLAY_RUN_RETRY_STRATEGY,
|
|
299
|
-
});
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
if (productionRefs.size === 0) return;
|
|
304
|
-
const cleanupResults = await Promise.allSettled(
|
|
305
|
-
[...productionRefs.values()].map(async (ref) => {
|
|
306
|
-
const outcome = await defaultRuntimeSandboxReconciliation.delete(ref);
|
|
307
|
-
if (!runtimeSandboxDeleteSucceeded(outcome)) {
|
|
308
|
-
throw new Error(
|
|
309
|
-
`${ref.provider} sandbox cleanup did not delete ${ref.resourceId}.`,
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
for (const result of cleanupResults) {
|
|
315
|
-
if (result.status === 'rejected') {
|
|
316
|
-
console.warn('[absurd.runtime_sandbox_cleanup_failed]', {
|
|
317
|
-
runId: input.runId,
|
|
318
|
-
error:
|
|
319
|
-
result.reason instanceof Error
|
|
320
|
-
? result.reason.message
|
|
321
|
-
: String(result.reason),
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
|
|
327
249
|
return {
|
|
328
250
|
...delegate,
|
|
329
251
|
runId: input.runId,
|