deepline 0.1.199 → 0.1.201
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/coordinator-entry.ts +5 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +67 -18
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
- package/dist/bundling-sources/sdk/src/http.ts +5 -2
- package/dist/bundling-sources/sdk/src/play.ts +1 -1
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
- package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
- package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
- package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +45 -3
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +561 -105
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
- package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
- package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
- package/dist/cli/index.js +167 -54
- package/dist/cli/index.mjs +167 -54
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/index.mjs +8 -8
- package/dist/plays/bundle-play-file.mjs +15 -20
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
- package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
- package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
- package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
|
@@ -66,10 +66,10 @@ import {
|
|
|
66
66
|
import { RECEIPT_STATUS_CODE, receiptStatusFromCode } from './receipt-status';
|
|
67
67
|
import {
|
|
68
68
|
workReceiptClaimableStatusCodes,
|
|
69
|
+
workReceiptActiveOwnerPredicateSql,
|
|
69
70
|
workReceiptClaimConflictPredicateSql,
|
|
70
71
|
workReceiptHeartbeatPredicateSql,
|
|
71
72
|
workReceiptReleaseOwnerPredicateSql,
|
|
72
|
-
workReceiptRunningOwnerPredicateSql,
|
|
73
73
|
} from './receipt-sql';
|
|
74
74
|
import {
|
|
75
75
|
activeRuntimeSheetAttemptFenceSql,
|
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
PLAY_RUNTIME_TEST_FAULT_HEADER,
|
|
106
106
|
type RuntimeTestFaultName,
|
|
107
107
|
} from './test-runtime-seams';
|
|
108
|
+
import { vercelProtectionBypassHeader } from './vercel-protection';
|
|
108
109
|
|
|
109
110
|
type RuntimeApiContext = {
|
|
110
111
|
baseUrl?: string | null;
|
|
@@ -302,10 +303,11 @@ const RUNTIME_DB_SESSION_RENEWAL_WINDOW_MS = 60_000;
|
|
|
302
303
|
const RUNTIME_API_RETRY_DELAYS_MS = [
|
|
303
304
|
250, 500, 1_000, 2_000, 4_000, 8_000, 8_000,
|
|
304
305
|
] as const;
|
|
305
|
-
const
|
|
306
|
+
const RUNTIME_API_RESOLVE_PLAY_RETRY_DELAYS_MS = [
|
|
307
|
+
250, 500, 1_000, 2_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000,
|
|
308
|
+
] as const;
|
|
306
309
|
const RUNTIME_API_DEFAULT_RETRY_AFTER_MS = 2_000;
|
|
307
310
|
const RUNTIME_API_REQUEST_TIMEOUT_MS = 30_000;
|
|
308
|
-
const vercelProtectionCookieCache = new Map<string, Promise<string | null>>();
|
|
309
311
|
const RUNTIME_POSTGRES_PREWARM_MAX_ATTEMPTS = 4;
|
|
310
312
|
const RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS = [250, 750, 1_500] as const;
|
|
311
313
|
const RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS = 4;
|
|
@@ -315,11 +317,12 @@ const RUNTIME_WORK_RECEIPT_QUERY_RETRY_DELAYS_MS = [250, 750] as const;
|
|
|
315
317
|
// Runtime DB sessions are minted against the pooled tenant endpoint. A healthy
|
|
316
318
|
// connect is sub-second; spending minutes on one sandbox dial only hides a
|
|
317
319
|
// broken route and stalls the whole play. Keep retries bounded and loud.
|
|
318
|
-
const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS =
|
|
320
|
+
const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS = 10_000;
|
|
319
321
|
// Daytona executes one play per runner process. This is the bounded data-plane
|
|
320
322
|
// budget for that one run's sheet reads/writes, receipts, and terminal/export
|
|
321
323
|
// reads over the PgBouncer URL; it is not a scheduler/control-plane fan-out.
|
|
322
324
|
const RUNTIME_POSTGRES_POOL_MAX_CONNECTIONS = 8;
|
|
325
|
+
const RECEIPT_STATUS_QUEUED = RECEIPT_STATUS_CODE.queued;
|
|
323
326
|
const RECEIPT_STATUS_PENDING = RECEIPT_STATUS_CODE.pending;
|
|
324
327
|
const RECEIPT_STATUS_RUNNING = RECEIPT_STATUS_CODE.running;
|
|
325
328
|
const RECEIPT_STATUS_COMPLETED = RECEIPT_STATUS_CODE.completed;
|
|
@@ -456,20 +459,12 @@ function resolveRuntimeApiUrl(context: RuntimeApiContext): string {
|
|
|
456
459
|
if (!baseUrl) {
|
|
457
460
|
throw new Error('Runner runtime API requires a baseUrl.');
|
|
458
461
|
}
|
|
459
|
-
|
|
460
|
-
`${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`,
|
|
461
|
-
);
|
|
462
|
-
const bypassToken = context.vercelProtectionBypassToken?.trim();
|
|
463
|
-
if (bypassToken) {
|
|
464
|
-
url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
|
|
465
|
-
url.searchParams.set('x-vercel-protection-bypass', bypassToken);
|
|
466
|
-
}
|
|
467
|
-
return url.toString();
|
|
462
|
+
return `${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`;
|
|
468
463
|
}
|
|
469
464
|
|
|
470
465
|
function resolveRuntimeApiHeaders(
|
|
471
466
|
context: RuntimeApiContext,
|
|
472
|
-
|
|
467
|
+
vercelHeaders: Record<string, string>,
|
|
473
468
|
): Record<string, string> {
|
|
474
469
|
const token = context.executorToken?.trim();
|
|
475
470
|
if (!token) {
|
|
@@ -479,61 +474,16 @@ function resolveRuntimeApiHeaders(
|
|
|
479
474
|
'content-type': 'application/json',
|
|
480
475
|
authorization: `Bearer ${token}`,
|
|
481
476
|
[PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
|
|
482
|
-
...
|
|
483
|
-
? { 'x-vercel-protection-bypass': context.vercelProtectionBypassToken }
|
|
484
|
-
: {}),
|
|
477
|
+
...vercelHeaders,
|
|
485
478
|
...(context.runtimeTestFaultHeader?.trim()
|
|
486
479
|
? {
|
|
487
480
|
[PLAY_RUNTIME_TEST_FAULT_HEADER]:
|
|
488
481
|
context.runtimeTestFaultHeader.trim(),
|
|
489
482
|
}
|
|
490
483
|
: {}),
|
|
491
|
-
...(vercelProtectionCookie ? { cookie: vercelProtectionCookie } : {}),
|
|
492
484
|
};
|
|
493
485
|
}
|
|
494
486
|
|
|
495
|
-
function setCookieHeaders(headers: Headers): string[] {
|
|
496
|
-
const getter = (headers as Headers & { getSetCookie?: () => string[] })
|
|
497
|
-
.getSetCookie;
|
|
498
|
-
if (typeof getter === 'function') {
|
|
499
|
-
return getter.call(headers);
|
|
500
|
-
}
|
|
501
|
-
const single = headers.get('set-cookie');
|
|
502
|
-
return single ? [single] : [];
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function cookieHeaderFromSetCookie(headers: Headers): string | null {
|
|
506
|
-
const cookies = setCookieHeaders(headers)
|
|
507
|
-
.flatMap((header) => header.split(/,(?=\s*[^;,=]+=[^;,]+)/g))
|
|
508
|
-
.map((header) => header.split(';', 1)[0]?.trim() ?? '')
|
|
509
|
-
.filter(Boolean);
|
|
510
|
-
return cookies.length > 0 ? cookies.join('; ') : null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function resolveVercelProtectionCookie(
|
|
514
|
-
context: RuntimeApiContext,
|
|
515
|
-
): Promise<string | null> {
|
|
516
|
-
const bypassToken = context.vercelProtectionBypassToken?.trim();
|
|
517
|
-
const baseUrl = context.baseUrl?.trim().replace(/\/$/, '');
|
|
518
|
-
if (!bypassToken || !baseUrl) return Promise.resolve(null);
|
|
519
|
-
|
|
520
|
-
const cacheKey = `${baseUrl}\n${bypassToken}`;
|
|
521
|
-
const cached = vercelProtectionCookieCache.get(cacheKey);
|
|
522
|
-
if (cached) return cached;
|
|
523
|
-
|
|
524
|
-
const promise = (async () => {
|
|
525
|
-
const url = new URL(`${baseUrl}/api/v2/health`);
|
|
526
|
-
url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
|
|
527
|
-
url.searchParams.set('x-vercel-protection-bypass', bypassToken);
|
|
528
|
-
const response = await fetch(url.toString(), {
|
|
529
|
-
headers: { 'x-vercel-protection-bypass': bypassToken },
|
|
530
|
-
}).catch(() => null);
|
|
531
|
-
return response ? cookieHeaderFromSetCookie(response.headers) : null;
|
|
532
|
-
})();
|
|
533
|
-
vercelProtectionCookieCache.set(cacheKey, promise);
|
|
534
|
-
return promise;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
487
|
function normalizeRuntimeUserEmail(
|
|
538
488
|
value: string | null | undefined,
|
|
539
489
|
): string | null {
|
|
@@ -546,8 +496,11 @@ async function postRuntimeApi<TResponse>(
|
|
|
546
496
|
body: RuntimeApiActionRequest,
|
|
547
497
|
): Promise<TResponse> {
|
|
548
498
|
const url = resolveRuntimeApiUrl(context);
|
|
549
|
-
const
|
|
550
|
-
|
|
499
|
+
const vercelHeaders = vercelProtectionBypassHeader(
|
|
500
|
+
context.vercelProtectionBypassToken,
|
|
501
|
+
);
|
|
502
|
+
const maxAttempts = runtimeApiMaxAttempts(body.action);
|
|
503
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
551
504
|
let response: Response;
|
|
552
505
|
const abortController = new AbortController();
|
|
553
506
|
const timeout = setTimeout(
|
|
@@ -557,13 +510,13 @@ async function postRuntimeApi<TResponse>(
|
|
|
557
510
|
try {
|
|
558
511
|
response = await fetch(url, {
|
|
559
512
|
method: 'POST',
|
|
560
|
-
headers: resolveRuntimeApiHeaders(context,
|
|
513
|
+
headers: resolveRuntimeApiHeaders(context, vercelHeaders),
|
|
561
514
|
body: JSON.stringify(body),
|
|
562
515
|
signal: abortController.signal,
|
|
563
516
|
});
|
|
564
517
|
} catch (error) {
|
|
565
|
-
if (attempt <
|
|
566
|
-
await sleepRuntimeApiRetry(attempt);
|
|
518
|
+
if (attempt < maxAttempts) {
|
|
519
|
+
await sleepRuntimeApiRetry(body.action, attempt);
|
|
567
520
|
continue;
|
|
568
521
|
}
|
|
569
522
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -605,8 +558,8 @@ async function postRuntimeApi<TResponse>(
|
|
|
605
558
|
isTransientRuntimeApiErrorMessage(
|
|
606
559
|
`${typeof parsed?.error === 'string' ? parsed.error : ''}\n${details ?? ''}`,
|
|
607
560
|
));
|
|
608
|
-
if (shouldRetryRuntimeResponse && attempt <
|
|
609
|
-
await sleepRuntimeApiRetry(attempt, retryAfterMs);
|
|
561
|
+
if (shouldRetryRuntimeResponse && attempt < maxAttempts) {
|
|
562
|
+
await sleepRuntimeApiRetry(body.action, attempt, retryAfterMs);
|
|
610
563
|
continue;
|
|
611
564
|
}
|
|
612
565
|
|
|
@@ -625,11 +578,12 @@ async function postRuntimeApi<TResponse>(
|
|
|
625
578
|
}
|
|
626
579
|
|
|
627
580
|
function runtimeApiRetryDelayMs(
|
|
581
|
+
action: RuntimeApiActionRequest['action'],
|
|
628
582
|
attempt: number,
|
|
629
583
|
retryAfterMs?: number,
|
|
630
584
|
): number {
|
|
631
585
|
const configuredDelay =
|
|
632
|
-
|
|
586
|
+
runtimeApiRetryDelaysForAction(action)[attempt - 1] ??
|
|
633
587
|
RUNTIME_API_DEFAULT_RETRY_AFTER_MS;
|
|
634
588
|
return retryAfterMs === undefined
|
|
635
589
|
? configuredDelay
|
|
@@ -637,14 +591,29 @@ function runtimeApiRetryDelayMs(
|
|
|
637
591
|
}
|
|
638
592
|
|
|
639
593
|
async function sleepRuntimeApiRetry(
|
|
594
|
+
action: RuntimeApiActionRequest['action'],
|
|
640
595
|
attempt: number,
|
|
641
596
|
retryAfterMs?: number,
|
|
642
597
|
): Promise<void> {
|
|
643
598
|
await new Promise((resolve) =>
|
|
644
|
-
setTimeout(resolve, runtimeApiRetryDelayMs(attempt, retryAfterMs)),
|
|
599
|
+
setTimeout(resolve, runtimeApiRetryDelayMs(action, attempt, retryAfterMs)),
|
|
645
600
|
);
|
|
646
601
|
}
|
|
647
602
|
|
|
603
|
+
function runtimeApiRetryDelaysForAction(
|
|
604
|
+
action: RuntimeApiActionRequest['action'],
|
|
605
|
+
): readonly number[] {
|
|
606
|
+
return action === 'resolve_play'
|
|
607
|
+
? RUNTIME_API_RESOLVE_PLAY_RETRY_DELAYS_MS
|
|
608
|
+
: RUNTIME_API_RETRY_DELAYS_MS;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function runtimeApiMaxAttempts(
|
|
612
|
+
action: RuntimeApiActionRequest['action'],
|
|
613
|
+
): number {
|
|
614
|
+
return runtimeApiRetryDelaysForAction(action).length + 1;
|
|
615
|
+
}
|
|
616
|
+
|
|
648
617
|
function isTransientRuntimeApiErrorMessage(message: string): boolean {
|
|
649
618
|
return /timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|requested endpoint could not be found, or you don't have access/i.test(
|
|
650
619
|
message,
|
|
@@ -757,6 +726,75 @@ async function isRuntimeSheetSchemaReady(
|
|
|
757
726
|
}
|
|
758
727
|
}
|
|
759
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Report exactly what the scoped runtime role can see for the sheet table, from
|
|
731
|
+
* the same connection the readiness probe uses. When ensure_sheet reports the
|
|
732
|
+
* table as provisioned but the runner probe still returns not-ready, the cause
|
|
733
|
+
* is almost always that the runtime role lacks a privilege on the freshly
|
|
734
|
+
* created table (so information_schema.columns hides the attempt columns) or a
|
|
735
|
+
* schema/table-name mismatch. This turns the previously blind "still not ready"
|
|
736
|
+
* error into an actionable one that names the concrete missing piece.
|
|
737
|
+
*/
|
|
738
|
+
async function readRuntimeSheetSchemaDiagnostic(
|
|
739
|
+
session: RuntimePostgresSession,
|
|
740
|
+
): Promise<Record<string, unknown>> {
|
|
741
|
+
const query = async (
|
|
742
|
+
client: RuntimeQueryClient,
|
|
743
|
+
): Promise<Record<string, unknown>> => {
|
|
744
|
+
const visibleColumns = await client.query<{ column_name: string }>(
|
|
745
|
+
`
|
|
746
|
+
SELECT column_name
|
|
747
|
+
FROM information_schema.columns
|
|
748
|
+
WHERE table_schema = $1
|
|
749
|
+
AND table_name = $2
|
|
750
|
+
ORDER BY ordinal_position
|
|
751
|
+
`,
|
|
752
|
+
[session.postgres.schema, session.postgres.sheetTable],
|
|
753
|
+
);
|
|
754
|
+
const privileges = await client.query<Record<string, unknown>>(
|
|
755
|
+
`
|
|
756
|
+
SELECT
|
|
757
|
+
current_user AS probe_role,
|
|
758
|
+
to_regclass($3) IS NOT NULL AS regclass_visible,
|
|
759
|
+
has_table_privilege(current_user, $3, 'SELECT') AS can_select,
|
|
760
|
+
has_table_privilege(current_user, $3, 'INSERT') AS can_insert,
|
|
761
|
+
has_table_privilege(current_user, $3, 'UPDATE') AS can_update
|
|
762
|
+
`,
|
|
763
|
+
[
|
|
764
|
+
session.postgres.schema,
|
|
765
|
+
session.postgres.sheetTable,
|
|
766
|
+
`${quoteIdentifier(session.postgres.schema)}.${quoteIdentifier(session.postgres.sheetTable)}`,
|
|
767
|
+
],
|
|
768
|
+
);
|
|
769
|
+
const visible = new Set(visibleColumns.rows.map((row) => row.column_name));
|
|
770
|
+
return {
|
|
771
|
+
schema: session.postgres.schema,
|
|
772
|
+
table: session.postgres.sheetTable,
|
|
773
|
+
probeRole: privileges.rows[0]?.probe_role ?? null,
|
|
774
|
+
regclassVisible: privileges.rows[0]?.regclass_visible ?? null,
|
|
775
|
+
canSelect: privileges.rows[0]?.can_select ?? null,
|
|
776
|
+
canInsert: privileges.rows[0]?.can_insert ?? null,
|
|
777
|
+
canUpdate: privileges.rows[0]?.can_update ?? null,
|
|
778
|
+
visibleColumnCount: visible.size,
|
|
779
|
+
missingAttemptColumns: RUNTIME_SHEET_ATTEMPT_COLUMNS.filter(
|
|
780
|
+
(column) => !visible.has(column),
|
|
781
|
+
),
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
try {
|
|
785
|
+
if (isRuntimeOneShotQueryFactoryRegistered()) {
|
|
786
|
+
return await withRuntimeOneShotPostgres(session, query);
|
|
787
|
+
}
|
|
788
|
+
return await withRuntimePostgres(session, query);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
return {
|
|
791
|
+
schema: session.postgres.schema,
|
|
792
|
+
table: session.postgres.sheetTable,
|
|
793
|
+
diagnosticError: error instanceof Error ? error.message : String(error),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
760
798
|
async function waitForRuntimeSheetSchemaReadyAfterEnsure(
|
|
761
799
|
session: RuntimePostgresSession,
|
|
762
800
|
input: {
|
|
@@ -870,7 +908,7 @@ async function ensureRuntimeSheetForPreloadedSession(
|
|
|
870
908
|
phase: 'ensure_sheet_for_preloaded_session',
|
|
871
909
|
ms: Date.now() - ensureStartedAt,
|
|
872
910
|
});
|
|
873
|
-
|
|
911
|
+
let readyAfterEnsure = await waitForRuntimeSheetSchemaReadyAfterEnsure(
|
|
874
912
|
input.session,
|
|
875
913
|
{
|
|
876
914
|
sheetContract: input.sheetContract,
|
|
@@ -878,8 +916,39 @@ async function ensureRuntimeSheetForPreloadedSession(
|
|
|
878
916
|
},
|
|
879
917
|
);
|
|
880
918
|
if (!readyAfterEnsure) {
|
|
919
|
+
// ensure_sheet provisions the sheet as the tenant admin role, but the
|
|
920
|
+
// preloaded session probes as the scoped runtime role. When the tenant
|
|
921
|
+
// role contract was only partially applied (e.g. role-membership grants
|
|
922
|
+
// skipped on a data plane where the owner cannot administer roles), the
|
|
923
|
+
// runtime role never inherits access to the freshly created table, so its
|
|
924
|
+
// information_schema view hides the attempt columns and the probe stays
|
|
925
|
+
// not-ready even though the table exists. Re-apply the storage grants
|
|
926
|
+
// through the same self-heal the pre-ensure probe uses, then re-check
|
|
927
|
+
// once. This is a grant repair, not a blind retry: if the runtime role
|
|
928
|
+
// still cannot see the sheet, we fail loudly below with the concrete
|
|
929
|
+
// runtime-role view.
|
|
930
|
+
const repairStartedAt = Date.now();
|
|
931
|
+
await repairRuntimeStorageGrants(context, {
|
|
932
|
+
playName: context.playName,
|
|
933
|
+
});
|
|
934
|
+
input.timings?.push({
|
|
935
|
+
phase: 'repair_storage_grants_after_preloaded_session_ensure',
|
|
936
|
+
ms: Date.now() - repairStartedAt,
|
|
937
|
+
retried: true,
|
|
938
|
+
});
|
|
939
|
+
readyAfterEnsure = await waitForRuntimeSheetSchemaReadyAfterEnsure(
|
|
940
|
+
input.session,
|
|
941
|
+
{
|
|
942
|
+
sheetContract: input.sheetContract,
|
|
943
|
+
timings: input.timings,
|
|
944
|
+
},
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
if (!readyAfterEnsure) {
|
|
948
|
+
const diagnostic = await readRuntimeSheetSchemaDiagnostic(input.session);
|
|
881
949
|
throw new Error(
|
|
882
|
-
`Runtime sheet schema for ctx.dataset("${input.tableNamespace}") is still not ready after ensure_sheet
|
|
950
|
+
`Runtime sheet schema for ctx.dataset("${input.tableNamespace}") is still not ready after ensure_sheet. ` +
|
|
951
|
+
`Runtime-role view: ${JSON.stringify(diagnostic)}`,
|
|
883
952
|
);
|
|
884
953
|
}
|
|
885
954
|
})();
|
|
@@ -1303,9 +1372,13 @@ async function prewarmRuntimePostgresSession(
|
|
|
1303
1372
|
attempt += 1
|
|
1304
1373
|
) {
|
|
1305
1374
|
try {
|
|
1306
|
-
await withRuntimePostgres(
|
|
1307
|
-
|
|
1308
|
-
|
|
1375
|
+
await withRuntimePostgres(
|
|
1376
|
+
session,
|
|
1377
|
+
async (client) => {
|
|
1378
|
+
await client.query('SELECT 1');
|
|
1379
|
+
},
|
|
1380
|
+
{ maxConnectAttempts: 1 },
|
|
1381
|
+
);
|
|
1309
1382
|
return;
|
|
1310
1383
|
} catch (error) {
|
|
1311
1384
|
if (
|
|
@@ -1454,17 +1527,17 @@ async function resetRuntimePostgresPool(postgresUrl: string): Promise<void> {
|
|
|
1454
1527
|
async function withRuntimePostgres<T>(
|
|
1455
1528
|
session: RuntimePostgresSession,
|
|
1456
1529
|
fn: (client: RuntimePoolClient) => Promise<T>,
|
|
1457
|
-
options: { cachePool?: boolean } = {},
|
|
1530
|
+
options: { cachePool?: boolean; maxConnectAttempts?: number } = {},
|
|
1458
1531
|
): Promise<T> {
|
|
1459
1532
|
let client: RuntimePoolClient | null = null;
|
|
1460
1533
|
let requestLocalPool: RuntimePool | null = null;
|
|
1461
1534
|
const cachePool =
|
|
1462
1535
|
(options.cachePool ?? true) && canReuseRuntimePostgresPoolsAcrossRequests();
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
) {
|
|
1536
|
+
const maxConnectAttempts =
|
|
1537
|
+
options.maxConnectAttempts === undefined
|
|
1538
|
+
? RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS
|
|
1539
|
+
: Math.max(1, Math.floor(options.maxConnectAttempts));
|
|
1540
|
+
for (let attempt = 1; attempt <= maxConnectAttempts; attempt += 1) {
|
|
1468
1541
|
try {
|
|
1469
1542
|
const pool = getPostgresPool(session.postgresUrl, cachePool);
|
|
1470
1543
|
if (!cachePool) {
|
|
@@ -1484,7 +1557,7 @@ async function withRuntimePostgres<T>(
|
|
|
1484
1557
|
requestLocalPool = null;
|
|
1485
1558
|
}
|
|
1486
1559
|
if (
|
|
1487
|
-
attempt >=
|
|
1560
|
+
attempt >= maxConnectAttempts ||
|
|
1488
1561
|
!isTransientRuntimePostgresConnectionError(error)
|
|
1489
1562
|
) {
|
|
1490
1563
|
throw error;
|
|
@@ -2658,6 +2731,8 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2658
2731
|
'$9::timestamptz',
|
|
2659
2732
|
'$10::integer',
|
|
2660
2733
|
);
|
|
2734
|
+
const targetOlderForeignAttemptOwnerSql =
|
|
2735
|
+
'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $8::text AND COALESCE(target._attempt_seq, 0) < $10::integer';
|
|
2661
2736
|
const existingAttemptFenceSql = activeRuntimeSheetAttemptFenceSql(
|
|
2662
2737
|
'existing',
|
|
2663
2738
|
'$7::text',
|
|
@@ -2665,6 +2740,8 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2665
2740
|
'$9::timestamptz',
|
|
2666
2741
|
'$10::integer',
|
|
2667
2742
|
);
|
|
2743
|
+
const existingForeignAttemptOwnerSql =
|
|
2744
|
+
'coalesce(existing._attempt_owner_run_id, existing._run_id) IS DISTINCT FROM $8::text';
|
|
2668
2745
|
const targetNewerTerminalRowSql = newerTerminalRuntimeSheetRowSql(
|
|
2669
2746
|
'target',
|
|
2670
2747
|
'$9::timestamptz',
|
|
@@ -2776,10 +2853,14 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2776
2853
|
WHERE target._key = input_rows._key
|
|
2777
2854
|
AND target._status IN ('pending', 'running', 'failed')
|
|
2778
2855
|
AND NOT (${targetNewerTerminalRowSql})
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2856
|
+
AND (
|
|
2857
|
+
${targetAttemptFenceSql}
|
|
2858
|
+
OR ${targetOlderForeignAttemptOwnerSql}
|
|
2859
|
+
OR (
|
|
2860
|
+
$11::boolean
|
|
2861
|
+
AND target._status = 'failed'
|
|
2862
|
+
)
|
|
2863
|
+
)
|
|
2783
2864
|
AND (
|
|
2784
2865
|
target._run_id IS DISTINCT FROM $4::text
|
|
2785
2866
|
OR target._input_index IS DISTINCT FROM input_rows._input_index
|
|
@@ -2808,10 +2889,17 @@ async function prepareRuntimeSheetDatasetRows(
|
|
|
2808
2889
|
WHERE (
|
|
2809
2890
|
existing._status IN ('pending', 'running', 'failed')
|
|
2810
2891
|
AND NOT (${existingNewerTerminalRowSql})
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2892
|
+
AND (
|
|
2893
|
+
${existingAttemptFenceSql}
|
|
2894
|
+
OR (
|
|
2895
|
+
${existingForeignAttemptOwnerSql}
|
|
2896
|
+
AND COALESCE(existing._attempt_seq, 0) < $10::integer
|
|
2897
|
+
)
|
|
2898
|
+
OR (
|
|
2899
|
+
$11::boolean
|
|
2900
|
+
AND existing._status = 'failed'
|
|
2901
|
+
)
|
|
2902
|
+
)
|
|
2815
2903
|
)
|
|
2816
2904
|
OR (
|
|
2817
2905
|
existing._status = 'enriched'
|
|
@@ -3110,6 +3198,8 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3110
3198
|
'$7::timestamptz',
|
|
3111
3199
|
'$8::integer',
|
|
3112
3200
|
);
|
|
3201
|
+
const targetOlderForeignAttemptOwnerSql =
|
|
3202
|
+
'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $6::text AND COALESCE(target._attempt_seq, 0) < $8::integer';
|
|
3113
3203
|
const targetWritableEnrichedAttemptSql =
|
|
3114
3204
|
writableEnrichedRuntimeSheetAttemptSql(
|
|
3115
3205
|
'target',
|
|
@@ -3138,7 +3228,18 @@ async function markRuntimeRowsPendingForRecompute(
|
|
|
3138
3228
|
FROM target_rows
|
|
3139
3229
|
WHERE target._key = target_rows._key
|
|
3140
3230
|
AND (
|
|
3141
|
-
|
|
3231
|
+
(
|
|
3232
|
+
target._status IN ('pending', 'running', 'failed')
|
|
3233
|
+
AND (
|
|
3234
|
+
${targetAttemptFenceSql}
|
|
3235
|
+
OR ${targetOlderForeignAttemptOwnerSql}
|
|
3236
|
+
OR (
|
|
3237
|
+
$10::boolean
|
|
3238
|
+
AND target._status = 'failed'
|
|
3239
|
+
)
|
|
3240
|
+
)
|
|
3241
|
+
)
|
|
3242
|
+
OR ${targetAttemptFenceSql}
|
|
3142
3243
|
OR (
|
|
3143
3244
|
$10::boolean
|
|
3144
3245
|
AND target._status = 'enriched'
|
|
@@ -3450,6 +3551,10 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3450
3551
|
input.leaseTtlMs,
|
|
3451
3552
|
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
3452
3553
|
);
|
|
3554
|
+
const claimStatus =
|
|
3555
|
+
input.leaseAware === true
|
|
3556
|
+
? RECEIPT_STATUS_QUEUED
|
|
3557
|
+
: RECEIPT_STATUS_RUNNING;
|
|
3453
3558
|
const claimableStatuses = workReceiptClaimableStatusCodes({
|
|
3454
3559
|
forceRefresh: input.forceRefresh,
|
|
3455
3560
|
forceFailedRefresh: input.forceFailedRefresh,
|
|
@@ -3457,7 +3562,13 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3457
3562
|
});
|
|
3458
3563
|
const { rows } = await client.query<Record<string, unknown>>(
|
|
3459
3564
|
`
|
|
3460
|
-
WITH
|
|
3565
|
+
WITH existing AS MATERIALIZED (
|
|
3566
|
+
SELECT status
|
|
3567
|
+
FROM ${workReceiptTable(session)}
|
|
3568
|
+
WHERE k = decode($1, 'hex')
|
|
3569
|
+
FOR UPDATE
|
|
3570
|
+
),
|
|
3571
|
+
claimed AS (
|
|
3461
3572
|
INSERT INTO ${workReceiptTable(session)} (
|
|
3462
3573
|
k,
|
|
3463
3574
|
status,
|
|
@@ -3500,6 +3611,7 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3500
3611
|
claimableStatusesSql: '$5',
|
|
3501
3612
|
claimantRunIdSql: '$3',
|
|
3502
3613
|
claimantRunAttemptSql: '$9',
|
|
3614
|
+
reclaimRunningSql: '$11',
|
|
3503
3615
|
forceRefreshSql: '$8',
|
|
3504
3616
|
forceFailedRefreshSql: '$10',
|
|
3505
3617
|
})}
|
|
@@ -3525,25 +3637,39 @@ export async function claimRuntimeWorkReceipt(
|
|
|
3525
3637
|
lease_owner_run_id,
|
|
3526
3638
|
lease_owner_attempt,
|
|
3527
3639
|
lease_expires_at,
|
|
3528
|
-
updated_at
|
|
3640
|
+
updated_at,
|
|
3641
|
+
EXISTS (
|
|
3642
|
+
SELECT 1
|
|
3643
|
+
FROM existing
|
|
3644
|
+
WHERE status = $12::smallint
|
|
3645
|
+
) AS refreshed_failed_receipt
|
|
3529
3646
|
FROM claimed
|
|
3530
3647
|
`,
|
|
3531
3648
|
[
|
|
3532
3649
|
workReceiptKeyHex(input.key),
|
|
3533
|
-
|
|
3650
|
+
claimStatus,
|
|
3534
3651
|
input.runId,
|
|
3535
|
-
|
|
3652
|
+
claimStatus,
|
|
3536
3653
|
claimableStatuses,
|
|
3537
3654
|
leaseId,
|
|
3538
3655
|
leaseTtlMs,
|
|
3539
3656
|
input.forceRefresh === true,
|
|
3540
3657
|
runAttempt,
|
|
3541
3658
|
input.forceFailedRefresh === true,
|
|
3659
|
+
input.reclaimRunning === true,
|
|
3660
|
+
RECEIPT_STATUS_CODE.failed,
|
|
3542
3661
|
],
|
|
3543
3662
|
);
|
|
3544
|
-
const
|
|
3663
|
+
const claimedRow = rows[0] ?? null;
|
|
3664
|
+
const claimed = claimedRow ? mapRuntimeWorkReceiptRow(claimedRow) : null;
|
|
3545
3665
|
if (claimed) {
|
|
3546
|
-
return {
|
|
3666
|
+
return {
|
|
3667
|
+
disposition: 'claimed',
|
|
3668
|
+
receipt: claimed,
|
|
3669
|
+
wasFailed:
|
|
3670
|
+
claimedRow.refreshed_failed_receipt === true ||
|
|
3671
|
+
claimedRow.refreshed_failed_receipt === 'true',
|
|
3672
|
+
};
|
|
3547
3673
|
}
|
|
3548
3674
|
|
|
3549
3675
|
const latest = await readRuntimeWorkReceipt(client, session, input.key);
|
|
@@ -3577,7 +3703,11 @@ function runtimeWorkReceiptClaimDispositionForBlockedReceipt(input: {
|
|
|
3577
3703
|
if (receipt.status === 'failed') {
|
|
3578
3704
|
return { disposition: 'failed', receipt };
|
|
3579
3705
|
}
|
|
3580
|
-
if (
|
|
3706
|
+
if (
|
|
3707
|
+
receipt.status === 'queued' ||
|
|
3708
|
+
receipt.status === 'pending' ||
|
|
3709
|
+
receipt.status === 'running'
|
|
3710
|
+
) {
|
|
3581
3711
|
const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
|
|
3582
3712
|
const leaseExpiresAtMs =
|
|
3583
3713
|
typeof receipt.leaseExpiresAt === 'string'
|
|
@@ -3628,6 +3758,10 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3628
3758
|
const leaseIds = keys.map(() =>
|
|
3629
3759
|
input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null,
|
|
3630
3760
|
);
|
|
3761
|
+
const claimStatus =
|
|
3762
|
+
input.leaseAware === true
|
|
3763
|
+
? RECEIPT_STATUS_QUEUED
|
|
3764
|
+
: RECEIPT_STATUS_RUNNING;
|
|
3631
3765
|
const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
|
|
3632
3766
|
const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
|
|
3633
3767
|
input.leaseTtlMs,
|
|
@@ -3697,6 +3831,7 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3697
3831
|
claimableStatusesSql: '$6',
|
|
3698
3832
|
claimantRunIdSql: '$4::text',
|
|
3699
3833
|
claimantRunAttemptSql: '$9',
|
|
3834
|
+
reclaimRunningSql: '$11',
|
|
3700
3835
|
forceRefreshSql: '$8',
|
|
3701
3836
|
forceFailedRefreshSql: '$10',
|
|
3702
3837
|
})}
|
|
@@ -3783,14 +3918,15 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3783
3918
|
[
|
|
3784
3919
|
keyHexes,
|
|
3785
3920
|
leaseIds,
|
|
3786
|
-
|
|
3921
|
+
claimStatus,
|
|
3787
3922
|
input.runId,
|
|
3788
|
-
|
|
3923
|
+
claimStatus,
|
|
3789
3924
|
reclaimStatuses,
|
|
3790
3925
|
leaseTtlMs,
|
|
3791
3926
|
input.forceRefresh === true,
|
|
3792
3927
|
runAttempt,
|
|
3793
3928
|
input.forceFailedRefresh === true,
|
|
3929
|
+
input.reclaimRunning === true,
|
|
3794
3930
|
],
|
|
3795
3931
|
);
|
|
3796
3932
|
return rows.map((row) => {
|
|
@@ -3811,6 +3947,317 @@ export async function claimRuntimeWorkReceipts(
|
|
|
3811
3947
|
);
|
|
3812
3948
|
}
|
|
3813
3949
|
|
|
3950
|
+
export async function markRuntimeWorkReceiptRunning(
|
|
3951
|
+
context: RuntimeApiContext,
|
|
3952
|
+
input: {
|
|
3953
|
+
playName: string;
|
|
3954
|
+
runId: string;
|
|
3955
|
+
key: string;
|
|
3956
|
+
runAttempt?: number | null;
|
|
3957
|
+
leaseId?: string | null;
|
|
3958
|
+
leaseTtlMs?: number | null;
|
|
3959
|
+
},
|
|
3960
|
+
): Promise<WorkReceipt | null> {
|
|
3961
|
+
const session = await getRuntimeWorkReceiptSession(context, {
|
|
3962
|
+
playName: input.playName,
|
|
3963
|
+
key: input.key,
|
|
3964
|
+
});
|
|
3965
|
+
return await withRuntimeWorkReceiptClient(
|
|
3966
|
+
context,
|
|
3967
|
+
session,
|
|
3968
|
+
async (client) => {
|
|
3969
|
+
const leaseId = input.leaseId?.trim() || null;
|
|
3970
|
+
const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
|
|
3971
|
+
const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
|
|
3972
|
+
input.leaseTtlMs,
|
|
3973
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
3974
|
+
);
|
|
3975
|
+
const { rows } = await client.query<Record<string, unknown>>(
|
|
3976
|
+
`
|
|
3977
|
+
WITH running AS (
|
|
3978
|
+
UPDATE ${workReceiptTable(session)}
|
|
3979
|
+
SET status = $2::smallint,
|
|
3980
|
+
lease_expires_at = CASE
|
|
3981
|
+
WHEN lease_id IS NULL THEN NULL
|
|
3982
|
+
ELSE now() + ($6::double precision * interval '1 millisecond')
|
|
3983
|
+
END,
|
|
3984
|
+
updated_at = now()
|
|
3985
|
+
WHERE k = decode($1, 'hex')
|
|
3986
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
3987
|
+
receiptTable: workReceiptTable(session),
|
|
3988
|
+
ownerRunIdSql: '$3',
|
|
3989
|
+
ownerRunAttemptSql: '$5',
|
|
3990
|
+
leaseIdSql: '$4::text',
|
|
3991
|
+
})}
|
|
3992
|
+
RETURNING convert_from(k, 'UTF8') AS k,
|
|
3993
|
+
status,
|
|
3994
|
+
output,
|
|
3995
|
+
error,
|
|
3996
|
+
failure_kind,
|
|
3997
|
+
run_id,
|
|
3998
|
+
lease_id,
|
|
3999
|
+
lease_owner_run_id,
|
|
4000
|
+
lease_owner_attempt,
|
|
4001
|
+
lease_expires_at,
|
|
4002
|
+
updated_at
|
|
4003
|
+
)
|
|
4004
|
+
SELECT k, status, output, error, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at, updated_at
|
|
4005
|
+
FROM running
|
|
4006
|
+
`,
|
|
4007
|
+
[
|
|
4008
|
+
workReceiptKeyHex(input.key),
|
|
4009
|
+
RECEIPT_STATUS_RUNNING,
|
|
4010
|
+
input.runId,
|
|
4011
|
+
leaseId,
|
|
4012
|
+
runAttempt,
|
|
4013
|
+
leaseTtlMs,
|
|
4014
|
+
],
|
|
4015
|
+
);
|
|
4016
|
+
return rows[0] ? mapRuntimeWorkReceiptRow(rows[0]) : null;
|
|
4017
|
+
},
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
export async function markRuntimeWorkReceiptsRunning(
|
|
4022
|
+
context: RuntimeApiContext,
|
|
4023
|
+
input: {
|
|
4024
|
+
playName: string;
|
|
4025
|
+
receipts: Array<{
|
|
4026
|
+
runId: string;
|
|
4027
|
+
key: string;
|
|
4028
|
+
leaseId?: string | null;
|
|
4029
|
+
runAttempt?: number | null;
|
|
4030
|
+
}>;
|
|
4031
|
+
leaseTtlMs?: number | null;
|
|
4032
|
+
},
|
|
4033
|
+
): Promise<Array<WorkReceipt | null>> {
|
|
4034
|
+
const receipts = input.receipts.filter((receipt) => receipt.key.trim());
|
|
4035
|
+
if (receipts.length === 0) return [];
|
|
4036
|
+
const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
|
|
4037
|
+
input.leaseTtlMs,
|
|
4038
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
4039
|
+
);
|
|
4040
|
+
const session = await getRuntimeWorkReceiptSessionForKeys(context, {
|
|
4041
|
+
playName: input.playName,
|
|
4042
|
+
keys: receipts.map((receipt) => receipt.key),
|
|
4043
|
+
});
|
|
4044
|
+
return await withRuntimeWorkReceiptClient(
|
|
4045
|
+
context,
|
|
4046
|
+
session,
|
|
4047
|
+
async (client) => {
|
|
4048
|
+
const { rows } = await client.query<Record<string, unknown>>(
|
|
4049
|
+
`
|
|
4050
|
+
WITH inputs AS (
|
|
4051
|
+
SELECT key_values.key_hex,
|
|
4052
|
+
run_values.run_id,
|
|
4053
|
+
lease_values.lease_id,
|
|
4054
|
+
attempt_values.run_attempt,
|
|
4055
|
+
key_values.ord
|
|
4056
|
+
FROM unnest($1::text[]) WITH ORDINALITY AS key_values(key_hex, ord)
|
|
4057
|
+
JOIN unnest($2::text[]) WITH ORDINALITY AS run_values(run_id, ord)
|
|
4058
|
+
ON run_values.ord = key_values.ord
|
|
4059
|
+
JOIN unnest($3::text[]) WITH ORDINALITY AS lease_values(lease_id, ord)
|
|
4060
|
+
ON lease_values.ord = key_values.ord
|
|
4061
|
+
JOIN unnest($4::integer[]) WITH ORDINALITY AS attempt_values(run_attempt, ord)
|
|
4062
|
+
ON attempt_values.ord = key_values.ord
|
|
4063
|
+
),
|
|
4064
|
+
running AS (
|
|
4065
|
+
UPDATE ${workReceiptTable(session)} AS target
|
|
4066
|
+
SET status = $5::smallint,
|
|
4067
|
+
lease_expires_at = CASE
|
|
4068
|
+
WHEN target.lease_id IS NULL THEN NULL
|
|
4069
|
+
ELSE now() + ($6::double precision * interval '1 millisecond')
|
|
4070
|
+
END,
|
|
4071
|
+
updated_at = now()
|
|
4072
|
+
FROM inputs
|
|
4073
|
+
WHERE target.k = decode(inputs.key_hex, 'hex')
|
|
4074
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4075
|
+
receiptTable: 'target',
|
|
4076
|
+
ownerRunIdSql: 'inputs.run_id',
|
|
4077
|
+
ownerRunAttemptSql: 'inputs.run_attempt',
|
|
4078
|
+
leaseIdSql: 'inputs.lease_id',
|
|
4079
|
+
})}
|
|
4080
|
+
RETURNING target.k,
|
|
4081
|
+
target.status,
|
|
4082
|
+
target.output,
|
|
4083
|
+
target.error,
|
|
4084
|
+
target.failure_kind,
|
|
4085
|
+
target.run_id,
|
|
4086
|
+
target.lease_id,
|
|
4087
|
+
target.lease_owner_run_id,
|
|
4088
|
+
target.lease_owner_attempt,
|
|
4089
|
+
target.lease_expires_at,
|
|
4090
|
+
target.updated_at,
|
|
4091
|
+
inputs.ord
|
|
4092
|
+
),
|
|
4093
|
+
returned AS (
|
|
4094
|
+
SELECT running.k,
|
|
4095
|
+
running.status,
|
|
4096
|
+
running.output,
|
|
4097
|
+
running.error,
|
|
4098
|
+
running.failure_kind,
|
|
4099
|
+
running.run_id,
|
|
4100
|
+
running.lease_id,
|
|
4101
|
+
running.lease_owner_run_id,
|
|
4102
|
+
running.lease_owner_attempt,
|
|
4103
|
+
running.lease_expires_at,
|
|
4104
|
+
running.updated_at,
|
|
4105
|
+
inputs.ord
|
|
4106
|
+
FROM inputs
|
|
4107
|
+
LEFT JOIN running ON running.ord = inputs.ord
|
|
4108
|
+
)
|
|
4109
|
+
SELECT convert_from(returned.k, 'UTF8') AS k,
|
|
4110
|
+
returned.status,
|
|
4111
|
+
returned.output,
|
|
4112
|
+
returned.error,
|
|
4113
|
+
returned.failure_kind,
|
|
4114
|
+
returned.run_id,
|
|
4115
|
+
returned.lease_id,
|
|
4116
|
+
returned.lease_owner_run_id,
|
|
4117
|
+
returned.lease_owner_attempt,
|
|
4118
|
+
returned.lease_expires_at,
|
|
4119
|
+
returned.updated_at
|
|
4120
|
+
FROM returned
|
|
4121
|
+
ORDER BY returned.ord
|
|
4122
|
+
`,
|
|
4123
|
+
[
|
|
4124
|
+
receipts.map((receipt) => workReceiptKeyHex(receipt.key)),
|
|
4125
|
+
receipts.map((receipt) => receipt.runId),
|
|
4126
|
+
receipts.map((receipt) => receipt.leaseId?.trim() || null),
|
|
4127
|
+
receipts.map((receipt) =>
|
|
4128
|
+
normalizeRuntimeRunAttempt(receipt.runAttempt),
|
|
4129
|
+
),
|
|
4130
|
+
RECEIPT_STATUS_RUNNING,
|
|
4131
|
+
leaseTtlMs,
|
|
4132
|
+
],
|
|
4133
|
+
);
|
|
4134
|
+
return rows.map((row) =>
|
|
4135
|
+
row.k == null ? null : mapRuntimeWorkReceiptRow(row),
|
|
4136
|
+
);
|
|
4137
|
+
},
|
|
4138
|
+
);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
export async function markRuntimeWorkReceiptsQueued(
|
|
4142
|
+
context: RuntimeApiContext,
|
|
4143
|
+
input: {
|
|
4144
|
+
playName: string;
|
|
4145
|
+
receipts: Array<{
|
|
4146
|
+
runId: string;
|
|
4147
|
+
key: string;
|
|
4148
|
+
leaseId?: string | null;
|
|
4149
|
+
runAttempt?: number | null;
|
|
4150
|
+
}>;
|
|
4151
|
+
leaseTtlMs?: number | null;
|
|
4152
|
+
},
|
|
4153
|
+
): Promise<Array<WorkReceipt | null>> {
|
|
4154
|
+
const receipts = input.receipts.filter((receipt) => receipt.key.trim());
|
|
4155
|
+
if (receipts.length === 0) return [];
|
|
4156
|
+
const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
|
|
4157
|
+
input.leaseTtlMs,
|
|
4158
|
+
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
4159
|
+
);
|
|
4160
|
+
const session = await getRuntimeWorkReceiptSessionForKeys(context, {
|
|
4161
|
+
playName: input.playName,
|
|
4162
|
+
keys: receipts.map((receipt) => receipt.key),
|
|
4163
|
+
});
|
|
4164
|
+
return await withRuntimeWorkReceiptClient(
|
|
4165
|
+
context,
|
|
4166
|
+
session,
|
|
4167
|
+
async (client) => {
|
|
4168
|
+
const { rows } = await client.query<Record<string, unknown>>(
|
|
4169
|
+
`
|
|
4170
|
+
WITH inputs AS (
|
|
4171
|
+
SELECT key_values.key_hex,
|
|
4172
|
+
run_values.run_id,
|
|
4173
|
+
lease_values.lease_id,
|
|
4174
|
+
attempt_values.run_attempt,
|
|
4175
|
+
key_values.ord
|
|
4176
|
+
FROM unnest($1::text[]) WITH ORDINALITY AS key_values(key_hex, ord)
|
|
4177
|
+
JOIN unnest($2::text[]) WITH ORDINALITY AS run_values(run_id, ord)
|
|
4178
|
+
ON run_values.ord = key_values.ord
|
|
4179
|
+
JOIN unnest($3::text[]) WITH ORDINALITY AS lease_values(lease_id, ord)
|
|
4180
|
+
ON lease_values.ord = key_values.ord
|
|
4181
|
+
JOIN unnest($4::integer[]) WITH ORDINALITY AS attempt_values(run_attempt, ord)
|
|
4182
|
+
ON attempt_values.ord = key_values.ord
|
|
4183
|
+
),
|
|
4184
|
+
queued AS (
|
|
4185
|
+
UPDATE ${workReceiptTable(session)} AS target
|
|
4186
|
+
SET status = $5::smallint,
|
|
4187
|
+
lease_expires_at = CASE
|
|
4188
|
+
WHEN target.lease_id IS NULL THEN NULL
|
|
4189
|
+
ELSE now() + ($6::double precision * interval '1 millisecond')
|
|
4190
|
+
END,
|
|
4191
|
+
updated_at = now()
|
|
4192
|
+
FROM inputs
|
|
4193
|
+
WHERE target.k = decode(inputs.key_hex, 'hex')
|
|
4194
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4195
|
+
receiptTable: 'target',
|
|
4196
|
+
ownerRunIdSql: 'inputs.run_id',
|
|
4197
|
+
ownerRunAttemptSql: 'inputs.run_attempt',
|
|
4198
|
+
leaseIdSql: 'inputs.lease_id',
|
|
4199
|
+
})}
|
|
4200
|
+
RETURNING target.k,
|
|
4201
|
+
target.status,
|
|
4202
|
+
target.output,
|
|
4203
|
+
target.error,
|
|
4204
|
+
target.failure_kind,
|
|
4205
|
+
target.run_id,
|
|
4206
|
+
target.lease_id,
|
|
4207
|
+
target.lease_owner_run_id,
|
|
4208
|
+
target.lease_owner_attempt,
|
|
4209
|
+
target.lease_expires_at,
|
|
4210
|
+
target.updated_at,
|
|
4211
|
+
inputs.ord
|
|
4212
|
+
),
|
|
4213
|
+
returned AS (
|
|
4214
|
+
SELECT queued.k,
|
|
4215
|
+
queued.status,
|
|
4216
|
+
queued.output,
|
|
4217
|
+
queued.error,
|
|
4218
|
+
queued.failure_kind,
|
|
4219
|
+
queued.run_id,
|
|
4220
|
+
queued.lease_id,
|
|
4221
|
+
queued.lease_owner_run_id,
|
|
4222
|
+
queued.lease_owner_attempt,
|
|
4223
|
+
queued.lease_expires_at,
|
|
4224
|
+
queued.updated_at,
|
|
4225
|
+
inputs.ord
|
|
4226
|
+
FROM inputs
|
|
4227
|
+
LEFT JOIN queued ON queued.ord = inputs.ord
|
|
4228
|
+
)
|
|
4229
|
+
SELECT convert_from(returned.k, 'UTF8') AS k,
|
|
4230
|
+
returned.status,
|
|
4231
|
+
returned.output,
|
|
4232
|
+
returned.error,
|
|
4233
|
+
returned.failure_kind,
|
|
4234
|
+
returned.run_id,
|
|
4235
|
+
returned.lease_id,
|
|
4236
|
+
returned.lease_owner_run_id,
|
|
4237
|
+
returned.lease_owner_attempt,
|
|
4238
|
+
returned.lease_expires_at,
|
|
4239
|
+
returned.updated_at
|
|
4240
|
+
FROM returned
|
|
4241
|
+
ORDER BY returned.ord
|
|
4242
|
+
`,
|
|
4243
|
+
[
|
|
4244
|
+
receipts.map((receipt) => workReceiptKeyHex(receipt.key)),
|
|
4245
|
+
receipts.map((receipt) => receipt.runId),
|
|
4246
|
+
receipts.map((receipt) => receipt.leaseId?.trim() || null),
|
|
4247
|
+
receipts.map((receipt) =>
|
|
4248
|
+
normalizeRuntimeRunAttempt(receipt.runAttempt),
|
|
4249
|
+
),
|
|
4250
|
+
RECEIPT_STATUS_QUEUED,
|
|
4251
|
+
leaseTtlMs,
|
|
4252
|
+
],
|
|
4253
|
+
);
|
|
4254
|
+
return rows.map((row) =>
|
|
4255
|
+
row.k == null ? null : mapRuntimeWorkReceiptRow(row),
|
|
4256
|
+
);
|
|
4257
|
+
},
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
|
|
3814
4261
|
export async function completeRuntimeWorkReceipt(
|
|
3815
4262
|
context: RuntimeApiContext,
|
|
3816
4263
|
input: {
|
|
@@ -3876,7 +4323,7 @@ export async function completeRuntimeWorkReceipt(
|
|
|
3876
4323
|
lease_expires_at = NULL,
|
|
3877
4324
|
updated_at = now()
|
|
3878
4325
|
WHERE k = decode($1, 'hex')
|
|
3879
|
-
AND ${
|
|
4326
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
3880
4327
|
receiptTable: workReceiptTable(session),
|
|
3881
4328
|
ownerRunIdSql: '$4',
|
|
3882
4329
|
ownerRunAttemptSql: '$6',
|
|
@@ -4012,7 +4459,7 @@ export async function completeRuntimeWorkReceipts(
|
|
|
4012
4459
|
updated_at = now()
|
|
4013
4460
|
FROM inputs
|
|
4014
4461
|
WHERE target.k = decode(inputs.key_hex, 'hex')
|
|
4015
|
-
AND ${
|
|
4462
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4016
4463
|
receiptTable: 'target',
|
|
4017
4464
|
ownerRunIdSql: 'inputs.run_id',
|
|
4018
4465
|
ownerRunAttemptSql: 'inputs.run_attempt',
|
|
@@ -4120,7 +4567,7 @@ export async function failRuntimeWorkReceipt(
|
|
|
4120
4567
|
lease_expires_at = NULL,
|
|
4121
4568
|
updated_at = now()
|
|
4122
4569
|
WHERE k = decode($1, 'hex')
|
|
4123
|
-
AND ${
|
|
4570
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4124
4571
|
receiptTable: workReceiptTable(session),
|
|
4125
4572
|
ownerRunIdSql: '$4',
|
|
4126
4573
|
ownerRunAttemptSql: '$7',
|
|
@@ -4190,7 +4637,7 @@ export async function releaseRuntimeWorkReceipt(
|
|
|
4190
4637
|
lease_expires_at = NULL,
|
|
4191
4638
|
updated_at = now()
|
|
4192
4639
|
WHERE k = decode($1, 'hex')
|
|
4193
|
-
AND ${
|
|
4640
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4194
4641
|
receiptTable: workReceiptTable(session),
|
|
4195
4642
|
ownerRunIdSql: '$3',
|
|
4196
4643
|
ownerRunAttemptSql: '$5',
|
|
@@ -4283,7 +4730,7 @@ export async function failRuntimeWorkReceipts(
|
|
|
4283
4730
|
updated_at = now()
|
|
4284
4731
|
FROM inputs
|
|
4285
4732
|
WHERE target.k = decode(inputs.key_hex, 'hex')
|
|
4286
|
-
AND ${
|
|
4733
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4287
4734
|
receiptTable: 'target',
|
|
4288
4735
|
ownerRunIdSql: 'inputs.run_id',
|
|
4289
4736
|
ownerRunAttemptSql: 'inputs.run_attempt',
|
|
@@ -4493,7 +4940,7 @@ export async function skipRuntimeWorkReceipt(
|
|
|
4493
4940
|
lease_expires_at = NULL,
|
|
4494
4941
|
updated_at = now()
|
|
4495
4942
|
WHERE k = decode($1, 'hex')
|
|
4496
|
-
AND ${
|
|
4943
|
+
AND ${workReceiptActiveOwnerPredicateSql({
|
|
4497
4944
|
receiptTable: workReceiptTable(session),
|
|
4498
4945
|
ownerRunIdSql: '$4',
|
|
4499
4946
|
ownerRunAttemptSql: '$6',
|
|
@@ -5122,6 +5569,13 @@ async function completeRuntimeMapRowChunks(
|
|
|
5122
5569
|
'$11::timestamptz',
|
|
5123
5570
|
'$12',
|
|
5124
5571
|
);
|
|
5572
|
+
const writableNonTerminalAttemptSql = `(
|
|
5573
|
+
target._status <> 'enriched'
|
|
5574
|
+
AND (
|
|
5575
|
+
${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')}
|
|
5576
|
+
OR coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $10::text
|
|
5577
|
+
)
|
|
5578
|
+
)`;
|
|
5125
5579
|
const sameOwnerTerminalEpochSql = sameOwnerTerminalAttemptEpochSql(
|
|
5126
5580
|
'target',
|
|
5127
5581
|
'$10::text',
|
|
@@ -5174,6 +5628,7 @@ async function completeRuntimeMapRowChunks(
|
|
|
5174
5628
|
AND NOT (${sameOwnerTerminalEpochSql})
|
|
5175
5629
|
AND (
|
|
5176
5630
|
($9::text IS NULL AND target._run_id = $5::text)
|
|
5631
|
+
OR ($9::text IS NOT NULL AND ${writableNonTerminalAttemptSql})
|
|
5177
5632
|
OR ($9::text IS NOT NULL AND ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')})
|
|
5178
5633
|
OR ($9::text IS NOT NULL AND ${writableEnrichedAttemptSql})
|
|
5179
5634
|
)
|
|
@@ -5208,6 +5663,7 @@ async function completeRuntimeMapRowChunks(
|
|
|
5208
5663
|
(${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
|
|
5209
5664
|
OR NOT (
|
|
5210
5665
|
($9::text IS NULL AND target._run_id = $5::text)
|
|
5666
|
+
OR ($9::text IS NOT NULL AND ${writableNonTerminalAttemptSql})
|
|
5211
5667
|
OR ($9::text IS NOT NULL AND ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')})
|
|
5212
5668
|
OR ($9::text IS NOT NULL AND ${writableEnrichedAttemptSql})
|
|
5213
5669
|
)
|