deepline 0.1.211 → 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 +317 -340
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +1 -1
- package/dist/bundling-sources/sdk/src/client.ts +118 -3
- 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/bundle-play-file.ts +1 -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/sdk/src/types.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +302 -30
- 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 +164 -121
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
- package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +163 -7
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
- 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-sql.ts +21 -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/run-ledger.ts +128 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
- 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 +91 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +540 -92
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
- package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
- package/dist/cli/index.js +637 -229
- package/dist/cli/index.mjs +637 -229
- 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 +84 -11
- package/dist/index.d.ts +84 -11
- package/dist/index.js +225 -43
- package/dist/index.mjs +225 -43
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +65 -4
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ import type { PreviousCell } from './cell-staleness';
|
|
|
24
24
|
import type { MapRowOutcome } from './durability-store';
|
|
25
25
|
import type { WorkReceiptFailureKind } from './work-receipts';
|
|
26
26
|
import type { RunExecutionScope } from './run-execution-scope';
|
|
27
|
+
import type { PlayCallExecution } from './play-call-execution';
|
|
27
28
|
|
|
28
29
|
export interface RowState {
|
|
29
30
|
results: Map<string, unknown>;
|
|
@@ -69,12 +70,26 @@ export interface RuntimeStepReceipt {
|
|
|
69
70
|
claimState?: 'claimed' | 'existing';
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export type AcquireRuntimeReceiptExecutionLockInput = {
|
|
74
|
+
key: string;
|
|
75
|
+
runId: string;
|
|
76
|
+
ownerExecutionId: string;
|
|
77
|
+
ttlMs: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ReleaseRuntimeReceiptExecutionLockInput = {
|
|
81
|
+
key: string;
|
|
82
|
+
runId: string;
|
|
83
|
+
ownerExecutionId: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
72
86
|
export interface GetRuntimeStepReceiptInput {
|
|
73
87
|
key: string;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
export interface ClaimRuntimeStepReceiptInput {
|
|
77
91
|
key: string;
|
|
92
|
+
leaseId?: string;
|
|
78
93
|
runId: string;
|
|
79
94
|
runAttempt?: number | null;
|
|
80
95
|
leaseAware?: boolean;
|
|
@@ -128,6 +143,7 @@ export interface GetRuntimeStepReceiptsInput {
|
|
|
128
143
|
|
|
129
144
|
export interface ClaimRuntimeStepReceiptsInput {
|
|
130
145
|
keys: string[];
|
|
146
|
+
leaseIds?: string[];
|
|
131
147
|
runId: string;
|
|
132
148
|
runAttempt?: number | null;
|
|
133
149
|
leaseAware?: boolean;
|
|
@@ -281,6 +297,7 @@ export type CustomerDbQueryHandler = (
|
|
|
281
297
|
|
|
282
298
|
export interface PlayCallOptions {
|
|
283
299
|
description?: string;
|
|
300
|
+
execution?: PlayCallExecution;
|
|
284
301
|
}
|
|
285
302
|
|
|
286
303
|
export interface StepOptions {
|
|
@@ -380,6 +397,18 @@ export interface MapExecutionScope {
|
|
|
380
397
|
}
|
|
381
398
|
|
|
382
399
|
export type PlayExecutionEvent =
|
|
400
|
+
| {
|
|
401
|
+
type: 'dataset.lifecycle';
|
|
402
|
+
datasetId: string;
|
|
403
|
+
path: string;
|
|
404
|
+
tableNamespace: string;
|
|
405
|
+
phase: 'registered' | 'available' | 'failed';
|
|
406
|
+
persistedRows?: number;
|
|
407
|
+
succeededRows?: number;
|
|
408
|
+
failedRows?: number;
|
|
409
|
+
complete?: boolean;
|
|
410
|
+
at: number;
|
|
411
|
+
}
|
|
383
412
|
| {
|
|
384
413
|
type: 'map.preparing';
|
|
385
414
|
mapInvocationId: string;
|
|
@@ -585,7 +614,10 @@ export interface ContextOptions {
|
|
|
585
614
|
rows: MapRowOutcome[];
|
|
586
615
|
outputFields: string[];
|
|
587
616
|
staticPipeline?: PlayStaticPipeline | null;
|
|
588
|
-
}) => Promise<void
|
|
617
|
+
}) => Promise<void | {
|
|
618
|
+
updated: number;
|
|
619
|
+
staleDroppedKeys?: string[];
|
|
620
|
+
}>;
|
|
589
621
|
playId?: string;
|
|
590
622
|
runId?: string;
|
|
591
623
|
/** Physical executor run that owns leases for an inline child context. */
|
|
@@ -622,6 +654,7 @@ export interface ContextOptions {
|
|
|
622
654
|
getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
|
|
623
655
|
getToolRetryPolicy?: (toolId: string) => Promise<{
|
|
624
656
|
retrySafeTransientHttp?: boolean;
|
|
657
|
+
requiresExecutionFence?: boolean;
|
|
625
658
|
} | null>;
|
|
626
659
|
getToolActionCacheVersion?: (toolId: string) => Promise<string> | string;
|
|
627
660
|
getToolAuthScopeDigest?: (toolId: string) => Promise<string> | string;
|
|
@@ -660,6 +693,12 @@ export interface ContextOptions {
|
|
|
660
693
|
getRuntimeStepReceipt?: (
|
|
661
694
|
input: GetRuntimeStepReceiptInput,
|
|
662
695
|
) => Promise<RuntimeStepReceipt | null> | RuntimeStepReceipt | null;
|
|
696
|
+
acquireRuntimeReceiptExecutionLock?: (
|
|
697
|
+
input: AcquireRuntimeReceiptExecutionLockInput,
|
|
698
|
+
) => Promise<{ ownerExecutionId: string; expiresAt: string } | null>;
|
|
699
|
+
releaseRuntimeReceiptExecutionLock?: (
|
|
700
|
+
input: ReleaseRuntimeReceiptExecutionLockInput,
|
|
701
|
+
) => Promise<boolean>;
|
|
663
702
|
getRuntimeStepReceipts?: (
|
|
664
703
|
input: GetRuntimeStepReceiptsInput,
|
|
665
704
|
) =>
|
|
@@ -920,6 +959,7 @@ export type PlayStep =
|
|
|
920
959
|
| {
|
|
921
960
|
type: 'play_call';
|
|
922
961
|
playId: string;
|
|
962
|
+
execution?: PlayCallExecution;
|
|
923
963
|
results?: PlayStepRowResult[];
|
|
924
964
|
nestedSteps: PlayStep[];
|
|
925
965
|
description?: string;
|
|
@@ -959,6 +999,7 @@ export type PlayDatasetSubstep =
|
|
|
959
999
|
| {
|
|
960
1000
|
type: 'play_call';
|
|
961
1001
|
playId: string;
|
|
1002
|
+
execution?: PlayCallExecution;
|
|
962
1003
|
results?: PlayStepRowResult[];
|
|
963
1004
|
nestedSteps: PlayStep[];
|
|
964
1005
|
description?: string;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
normalizePlayNameForSheet,
|
|
3
|
+
normalizeTableNamespace,
|
|
4
|
+
sha256Hex,
|
|
5
|
+
} from '../plays/row-identity';
|
|
2
6
|
|
|
3
7
|
export function createRuntimeDatasetId(
|
|
4
8
|
playName: string,
|
|
5
9
|
tableNamespace: string,
|
|
6
10
|
): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const normalizedPlayName = normalizePlayNameForSheet(playName);
|
|
12
|
+
const normalizedTableNamespace = normalizeTableNamespace(tableNamespace);
|
|
13
|
+
return sha256Hex(
|
|
14
|
+
`${normalizedPlayName}:${normalizedTableNamespace}`,
|
|
15
|
+
).slice(0, 40);
|
|
10
16
|
}
|
|
@@ -56,6 +56,7 @@ export type CreateDbSessionRequest = {
|
|
|
56
56
|
|
|
57
57
|
export type CreateDbSessionResponse = {
|
|
58
58
|
sessionId: string;
|
|
59
|
+
executionRole?: string;
|
|
59
60
|
postgresUrl?: string;
|
|
60
61
|
encryptedPostgresUrl?: EncryptedPostgresUrl;
|
|
61
62
|
postgres?: {
|
|
@@ -398,6 +399,14 @@ export const createDbSessionResponseSchema =
|
|
|
398
399
|
}
|
|
399
400
|
return {
|
|
400
401
|
sessionId: parseString(value.sessionId, 'sessionId'),
|
|
402
|
+
...(value.executionRole !== undefined
|
|
403
|
+
? {
|
|
404
|
+
executionRole: parseOptionalString(
|
|
405
|
+
value.executionRole,
|
|
406
|
+
'executionRole',
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
: {}),
|
|
401
410
|
...(value.postgresUrl !== undefined
|
|
402
411
|
? { postgresUrl: parseOptionalString(value.postgresUrl, 'postgresUrl') }
|
|
403
412
|
: {}),
|
|
@@ -25,6 +25,9 @@ const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
|
|
|
25
25
|
const TOOL_RECEIPT_DEFAULT_WAIT_MS = 300_000;
|
|
26
26
|
const TOOL_RECEIPT_COMPLETION_BUFFER_MS = 30_000;
|
|
27
27
|
const TOOL_RECEIPT_MAX_WAIT_MS = 30 * 60_000;
|
|
28
|
+
const DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS = 15_000;
|
|
29
|
+
export const COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID =
|
|
30
|
+
'__deepline_completed_cache_insert__';
|
|
28
31
|
|
|
29
32
|
export class RuntimeReceiptWaitTimeoutError extends Error {
|
|
30
33
|
constructor(key: string) {
|
|
@@ -179,6 +182,15 @@ export type DurableReceiptExecutionStore = {
|
|
|
179
182
|
): Promise<RuntimeStepReceipt | null>;
|
|
180
183
|
canPersistFailure: boolean;
|
|
181
184
|
canPersistCompletion: boolean;
|
|
185
|
+
acquireExecutionLock?: (input: {
|
|
186
|
+
receiptKey: string;
|
|
187
|
+
ownerExecutionId: string;
|
|
188
|
+
ttlMs: number;
|
|
189
|
+
}) => Promise<{ ownerExecutionId: string; expiresAt: string } | null>;
|
|
190
|
+
releaseExecutionLock?: (input: {
|
|
191
|
+
receiptKey: string;
|
|
192
|
+
ownerExecutionId: string;
|
|
193
|
+
}) => Promise<boolean>;
|
|
182
194
|
};
|
|
183
195
|
|
|
184
196
|
async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
|
|
@@ -243,7 +255,7 @@ async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
|
|
|
243
255
|
rejectLeaseLost(error as RuntimeReceiptLeaseLostError),
|
|
244
256
|
});
|
|
245
257
|
|
|
246
|
-
//
|
|
258
|
+
// The claim is the authoritative pre-dispatch ownership fence. Renewal is
|
|
247
259
|
// only needed when the lease approaches expiry; an immediate read/renewal
|
|
248
260
|
// adds a round trip to every short tool call without extending ownership.
|
|
249
261
|
// Completion remains conditional on this lease, and long work still renews.
|
|
@@ -390,6 +402,11 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
390
402
|
shouldPersistFailure?: (error: unknown) => boolean;
|
|
391
403
|
heartbeatIntervalMs?: number;
|
|
392
404
|
markRunningBeforeExecute?: boolean;
|
|
405
|
+
/** New receipt-cache path: read completed facts, execute without creating an
|
|
406
|
+
* in-flight receipt, then atomically insert the completed fact. */
|
|
407
|
+
completedCacheOnly?: boolean;
|
|
408
|
+
requiresExecutionLock?: boolean;
|
|
409
|
+
executionLockTtlMs?: number;
|
|
393
410
|
formatError: (error: unknown) => string;
|
|
394
411
|
log: (message: string) => void;
|
|
395
412
|
execute: (context: { leaseId: string | null }) => Promise<T>;
|
|
@@ -412,9 +429,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
412
429
|
receipt: RuntimeStepReceipt,
|
|
413
430
|
source: DurableReceiptRecoverySource = 'cache',
|
|
414
431
|
): Promise<T> => {
|
|
415
|
-
input.log(
|
|
416
|
-
`ctx.${input.operation}(${input.id}): recovered result from receipt`,
|
|
417
|
-
);
|
|
432
|
+
input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
|
|
418
433
|
if (receipt.output === undefined) {
|
|
419
434
|
return receipt.output as T;
|
|
420
435
|
}
|
|
@@ -428,6 +443,143 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
428
443
|
return recovered;
|
|
429
444
|
};
|
|
430
445
|
|
|
446
|
+
if (input.completedCacheOnly === true) {
|
|
447
|
+
if (input.force !== true) {
|
|
448
|
+
const cached = await input.store.get(input.receiptKey);
|
|
449
|
+
if (cached?.status === 'completed' || cached?.status === 'skipped') {
|
|
450
|
+
return await recoverCompletedReceipt(cached);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const needsLock = input.requiresExecutionLock === true;
|
|
454
|
+
const ownerExecutionId = `${input.runId}:${crypto.randomUUID()}`;
|
|
455
|
+
let ownsLock = false;
|
|
456
|
+
let lockSupervisor: ReturnType<
|
|
457
|
+
typeof createRuntimeReceiptHeartbeatSupervisor
|
|
458
|
+
> | null = null;
|
|
459
|
+
if (needsLock) {
|
|
460
|
+
if (
|
|
461
|
+
!input.store.acquireExecutionLock ||
|
|
462
|
+
!input.store.releaseExecutionLock
|
|
463
|
+
) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`ctx.${input.operation}(${input.id}): non-idempotent provider requires the receipt execution-lock backend.`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
|
|
469
|
+
while (!ownsLock) {
|
|
470
|
+
const lock = await input.store.acquireExecutionLock({
|
|
471
|
+
receiptKey: input.receiptKey,
|
|
472
|
+
ownerExecutionId,
|
|
473
|
+
ttlMs: input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS,
|
|
474
|
+
});
|
|
475
|
+
ownsLock = lock?.ownerExecutionId === ownerExecutionId;
|
|
476
|
+
if (ownsLock) break;
|
|
477
|
+
const winner = await input.store.get(input.receiptKey);
|
|
478
|
+
if (
|
|
479
|
+
input.force !== true &&
|
|
480
|
+
(winner?.status === 'completed' || winner?.status === 'skipped')
|
|
481
|
+
) {
|
|
482
|
+
return await recoverCompletedReceipt(winner, 'in_flight');
|
|
483
|
+
}
|
|
484
|
+
if (Date.now() >= deadline) {
|
|
485
|
+
throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
|
|
486
|
+
}
|
|
487
|
+
await sleepReceiptWait(DURABLE_RECEIPT_WAIT_DELAY_MS);
|
|
488
|
+
}
|
|
489
|
+
const afterLock = await input.store.get(input.receiptKey);
|
|
490
|
+
if (
|
|
491
|
+
input.force !== true &&
|
|
492
|
+
(afterLock?.status === 'completed' ||
|
|
493
|
+
afterLock?.status === 'skipped')
|
|
494
|
+
) {
|
|
495
|
+
await input.store.releaseExecutionLock({
|
|
496
|
+
receiptKey: input.receiptKey,
|
|
497
|
+
ownerExecutionId,
|
|
498
|
+
});
|
|
499
|
+
return await recoverCompletedReceipt(afterLock, 'in_flight');
|
|
500
|
+
}
|
|
501
|
+
const lockTtlMs =
|
|
502
|
+
input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS;
|
|
503
|
+
lockSupervisor = createRuntimeReceiptHeartbeatSupervisor({
|
|
504
|
+
intervalMs: Math.max(1_000, Math.floor(lockTtlMs / 3)),
|
|
505
|
+
heartbeat: async () => {
|
|
506
|
+
const renewed = await input.store.acquireExecutionLock!({
|
|
507
|
+
receiptKey: input.receiptKey,
|
|
508
|
+
ownerExecutionId,
|
|
509
|
+
ttlMs: lockTtlMs,
|
|
510
|
+
});
|
|
511
|
+
if (renewed?.ownerExecutionId !== ownerExecutionId) {
|
|
512
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
513
|
+
receiptKey: input.receiptKey,
|
|
514
|
+
runId: input.runId,
|
|
515
|
+
leaseId: ownerExecutionId,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return 'active';
|
|
519
|
+
},
|
|
520
|
+
isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
|
|
521
|
+
onLeaseLost: (error) => {
|
|
522
|
+
input.log(
|
|
523
|
+
`ctx.${input.operation}(${input.id}): dangerous side-effect execution fence lost: ${error instanceof Error ? error.message : String(error)}`,
|
|
524
|
+
);
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
lockSupervisor.start();
|
|
528
|
+
}
|
|
529
|
+
const releaseOwnLock = async (): Promise<void> => {
|
|
530
|
+
lockSupervisor?.stop();
|
|
531
|
+
lockSupervisor = null;
|
|
532
|
+
if (ownsLock) {
|
|
533
|
+
ownsLock = false;
|
|
534
|
+
await input.store.releaseExecutionLock!({
|
|
535
|
+
receiptKey: input.receiptKey,
|
|
536
|
+
ownerExecutionId,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
let executed: T;
|
|
541
|
+
try {
|
|
542
|
+
executed = await input.execute({ leaseId: null });
|
|
543
|
+
} catch (error) {
|
|
544
|
+
await releaseOwnLock();
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
const result = input.onClaimedResult
|
|
549
|
+
? input.onClaimedResult(executed, input.receiptKey)
|
|
550
|
+
: executed;
|
|
551
|
+
assertNoSecretTaint(result, `ctx.${input.operation} result`);
|
|
552
|
+
// Force bypasses the immutable cache. It does not grant permission to
|
|
553
|
+
// replace the completed fact already published under this semantic key.
|
|
554
|
+
if (input.force === true) {
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
const completed = await input.store.complete(
|
|
558
|
+
input.receiptKey,
|
|
559
|
+
input.runId,
|
|
560
|
+
isToolExecuteResult(result)
|
|
561
|
+
? serializeToolExecuteResult(result)
|
|
562
|
+
: result,
|
|
563
|
+
COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
|
|
564
|
+
);
|
|
565
|
+
if (
|
|
566
|
+
completed?.status === 'completed' ||
|
|
567
|
+
completed?.status === 'skipped'
|
|
568
|
+
) {
|
|
569
|
+
return await recoverCompletedReceipt(completed, 'owner');
|
|
570
|
+
}
|
|
571
|
+
const winner = await input.store.get(input.receiptKey);
|
|
572
|
+
if (winner?.status === 'completed' || winner?.status === 'skipped') {
|
|
573
|
+
return await recoverCompletedReceipt(winner, 'owner');
|
|
574
|
+
}
|
|
575
|
+
throw new Error(
|
|
576
|
+
`ctx.${input.operation}(${input.id}): completed receipt cache insert failed.`,
|
|
577
|
+
);
|
|
578
|
+
} finally {
|
|
579
|
+
await releaseOwnLock();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
431
583
|
let ownedLeaseId: string | null = null;
|
|
432
584
|
let ownedLeaseExpiresAt: string | null = null;
|
|
433
585
|
|
|
@@ -447,7 +599,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
447
599
|
}
|
|
448
600
|
if (reclaimed?.status === 'failed') {
|
|
449
601
|
throw new Error(
|
|
450
|
-
`ctx.${input.operation}(${input.id}):
|
|
602
|
+
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${reclaimed.error ?? 'unknown error'}.`,
|
|
451
603
|
);
|
|
452
604
|
}
|
|
453
605
|
if (
|
|
@@ -509,7 +661,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
509
661
|
receipt: RuntimeStepReceipt,
|
|
510
662
|
): Promise<never> => {
|
|
511
663
|
throw new Error(
|
|
512
|
-
`ctx.${input.operation}(${input.id}):
|
|
664
|
+
`ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${receipt.error ?? 'unknown error'}.`,
|
|
513
665
|
);
|
|
514
666
|
};
|
|
515
667
|
|
|
@@ -556,7 +708,11 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
|
|
|
556
708
|
|
|
557
709
|
let result: T;
|
|
558
710
|
try {
|
|
559
|
-
|
|
711
|
+
// New runners treat a claimed, nonterminal receipt as active. Keep the
|
|
712
|
+
// mark-running transition as an explicit compatibility option for older
|
|
713
|
+
// stores during their migration window, but do not pay the extra durable
|
|
714
|
+
// mutation on the normal claim -> terminal path.
|
|
715
|
+
if (input.markRunningBeforeExecute === true && input.store.markRunning) {
|
|
560
716
|
const markRunningStartedAt = Date.now();
|
|
561
717
|
const running = await input.store.markRunning(
|
|
562
718
|
input.receiptKey,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { createSingleFlight } from './single-flight';
|
|
4
|
+
|
|
5
|
+
export type GatewayAuthSession<TAuth, TLaunch> = {
|
|
6
|
+
auth: TAuth;
|
|
7
|
+
launch: TLaunch;
|
|
8
|
+
source: 'hit' | 'miss' | 'coalesced';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type StoredSession<TAuth, TLaunch> = {
|
|
12
|
+
auth: TAuth;
|
|
13
|
+
launch: TLaunch;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
runId: string;
|
|
16
|
+
runAttempt: number | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Briefly reuse an authenticated immutable launch, bounded by token identity. */
|
|
20
|
+
export function createGatewayAuthSessionCache<TAuth, TLaunch>(options: {
|
|
21
|
+
ttlMs: number;
|
|
22
|
+
maxEntries: number;
|
|
23
|
+
identity: (auth: TAuth) => {
|
|
24
|
+
runId: string | null;
|
|
25
|
+
runAttempt: number | null;
|
|
26
|
+
};
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}) {
|
|
29
|
+
const sessions = new Map<string, StoredSession<TAuth, TLaunch>>();
|
|
30
|
+
const flights = createSingleFlight<string, StoredSession<TAuth, TLaunch>>();
|
|
31
|
+
const now = options.now ?? Date.now;
|
|
32
|
+
const fingerprint = (token: string) =>
|
|
33
|
+
createHash('sha256').update(token).digest('base64url');
|
|
34
|
+
|
|
35
|
+
const prune = (at: number) => {
|
|
36
|
+
for (const [key, session] of sessions) {
|
|
37
|
+
if (session.expiresAt <= at) sessions.delete(key);
|
|
38
|
+
}
|
|
39
|
+
while (sessions.size >= options.maxEntries) {
|
|
40
|
+
const oldest = sessions.keys().next().value;
|
|
41
|
+
if (!oldest) break;
|
|
42
|
+
sessions.delete(oldest);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
async getOrEstablish(input: {
|
|
48
|
+
token: string;
|
|
49
|
+
tokenExpiresAt: number;
|
|
50
|
+
establish: () => Promise<{ auth: TAuth; launch: TLaunch }>;
|
|
51
|
+
}): Promise<GatewayAuthSession<TAuth, TLaunch>> {
|
|
52
|
+
const key = fingerprint(input.token);
|
|
53
|
+
const at = now();
|
|
54
|
+
const cached = sessions.get(key);
|
|
55
|
+
if (cached && cached.expiresAt > at) {
|
|
56
|
+
const identity = options.identity(cached.auth);
|
|
57
|
+
if (
|
|
58
|
+
identity.runId === cached.runId &&
|
|
59
|
+
identity.runAttempt === cached.runAttempt
|
|
60
|
+
) {
|
|
61
|
+
return { ...cached, source: 'hit' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (cached) sessions.delete(key);
|
|
65
|
+
|
|
66
|
+
const coalesced = flights.has(key);
|
|
67
|
+
const session = await flights.run(key, async () => {
|
|
68
|
+
const established = await input.establish();
|
|
69
|
+
const identity = options.identity(established.auth);
|
|
70
|
+
if (!identity.runId) {
|
|
71
|
+
throw new Error('Gateway authentication produced no run identity.');
|
|
72
|
+
}
|
|
73
|
+
const establishedAt = now();
|
|
74
|
+
const stored: StoredSession<TAuth, TLaunch> = {
|
|
75
|
+
...established,
|
|
76
|
+
expiresAt: Math.min(
|
|
77
|
+
establishedAt + options.ttlMs,
|
|
78
|
+
input.tokenExpiresAt,
|
|
79
|
+
),
|
|
80
|
+
runId: identity.runId,
|
|
81
|
+
runAttempt: identity.runAttempt,
|
|
82
|
+
};
|
|
83
|
+
if (stored.expiresAt > establishedAt) {
|
|
84
|
+
prune(establishedAt);
|
|
85
|
+
sessions.set(key, stored);
|
|
86
|
+
}
|
|
87
|
+
return stored;
|
|
88
|
+
});
|
|
89
|
+
return { ...session, source: coalesced ? 'coalesced' : 'miss' };
|
|
90
|
+
},
|
|
91
|
+
size: () => sessions.size,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createSecretRedactionContext } from './secret-redaction';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertRuntimeReceiptOutputWithinLimit,
|
|
4
|
+
TERMINAL_RUN_RESULT_MAX_BYTES,
|
|
5
|
+
} from './output-size-limits';
|
|
3
6
|
|
|
4
|
-
export const MAX_LEDGER_RESULT_PAYLOAD_BYTES =
|
|
7
|
+
export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = TERMINAL_RUN_RESULT_MAX_BYTES;
|
|
5
8
|
export const MAX_LEDGER_LOG_LINES_PER_EVENT = 500;
|
|
6
9
|
export const MAX_LEDGER_LOG_LINE_LENGTH = 2_000;
|
|
7
10
|
|
|
@@ -21,6 +24,15 @@ export function sanitizeLedgerPayload(value: unknown): unknown {
|
|
|
21
24
|
return ledgerIngressRedactor.redact(value);
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
/** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
|
|
28
|
+
export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
|
|
29
|
+
assertRuntimeReceiptOutputWithinLimit({
|
|
30
|
+
output: value,
|
|
31
|
+
path: 'runtime receipt output',
|
|
32
|
+
});
|
|
33
|
+
return ledgerIngressRedactor.redact(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export function sanitizeLedgerLogLines(value: unknown): string[] {
|
|
25
37
|
if (!Array.isArray(value)) return [];
|
|
26
38
|
return value
|
|
@@ -3,8 +3,10 @@ const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
|
|
|
3
3
|
export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
|
|
4
4
|
export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
|
|
5
5
|
export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
|
|
6
|
-
|
|
7
|
-
export const
|
|
6
|
+
/** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
|
|
7
|
+
export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
/** One completion request may contain multiple receipts up to 32 MiB total. */
|
|
9
|
+
export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
8
10
|
|
|
9
11
|
export class OutputTooLargeError extends Error {
|
|
10
12
|
readonly code = 'OUTPUT_TOO_LARGE';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type PlayCallExecution = 'inline' | 'child-workflow';
|
|
@@ -7,6 +7,7 @@ export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_SIZE = 200;
|
|
|
7
7
|
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_FLUSH_MS = 10;
|
|
8
8
|
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES =
|
|
9
9
|
RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES;
|
|
10
|
+
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_BYTES = 1_000_000;
|
|
10
11
|
export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES =
|
|
11
12
|
RUNTIME_RECEIPT_OUTPUT_MAX_BYTES;
|
|
12
13
|
|
|
@@ -22,6 +23,8 @@ export interface RuntimeReceiptCompletionSinkOptions<TInput, TOutput> {
|
|
|
22
23
|
maxBatchSize?: number;
|
|
23
24
|
maxFlushMs?: number;
|
|
24
25
|
maxBufferedBytes?: number;
|
|
26
|
+
/** Flush trigger and maximum ordinary transport batch size. */
|
|
27
|
+
maxBatchBytes?: number;
|
|
25
28
|
maxInputBytes?: number;
|
|
26
29
|
estimateBytes?: (input: TInput) => number;
|
|
27
30
|
completeMany: (inputs: TInput[]) => Promise<TOutput[]>;
|
|
@@ -83,12 +86,14 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
|
83
86
|
readonly maxBatchSize: number;
|
|
84
87
|
readonly maxFlushMs: number;
|
|
85
88
|
readonly maxBufferedBytes: number;
|
|
89
|
+
readonly maxBatchBytes: number;
|
|
86
90
|
readonly maxInputBytes: number;
|
|
87
91
|
|
|
88
92
|
#pending: Array<PendingReceiptCompletion<TInput, TOutput>> = [];
|
|
89
93
|
#pendingHead = 0;
|
|
90
94
|
/** Pending plus in-flight bytes. Released only after transport settles. */
|
|
91
95
|
#residentBytes = 0;
|
|
96
|
+
#pendingBytes = 0;
|
|
92
97
|
#capacityWaiters: CapacityWaiter[] = [];
|
|
93
98
|
#flushTimer: ReceiptCompletionSinkTimer | null = null;
|
|
94
99
|
#flushPromise: Promise<void> | null = null;
|
|
@@ -116,6 +121,10 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
|
116
121
|
options.maxBufferedBytes,
|
|
117
122
|
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES,
|
|
118
123
|
);
|
|
124
|
+
this.maxBatchBytes = normalizePositiveInteger(
|
|
125
|
+
options.maxBatchBytes,
|
|
126
|
+
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_BYTES,
|
|
127
|
+
);
|
|
119
128
|
this.maxInputBytes = normalizePositiveInteger(
|
|
120
129
|
options.maxInputBytes,
|
|
121
130
|
DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES,
|
|
@@ -194,12 +203,16 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
|
194
203
|
}
|
|
195
204
|
this.#throwIfUnavailable();
|
|
196
205
|
this.#residentBytes += bytes;
|
|
206
|
+
this.#pendingBytes += bytes;
|
|
197
207
|
const completion = new Promise<TOutput>((resolve, reject) => {
|
|
198
208
|
this.#pending.push({ input, bytes, resolve, reject });
|
|
199
209
|
if (this.#pendingCount() >= this.maxBatchSize) {
|
|
200
210
|
void this.flush().catch(() => undefined);
|
|
201
211
|
}
|
|
202
212
|
});
|
|
213
|
+
if (this.#pendingBytes >= this.maxBatchBytes) {
|
|
214
|
+
void this.flush().catch(() => undefined);
|
|
215
|
+
}
|
|
203
216
|
this.#scheduleFlush();
|
|
204
217
|
return await completion;
|
|
205
218
|
}
|
|
@@ -237,12 +250,13 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
|
237
250
|
let batchBytes = 0;
|
|
238
251
|
while (this.#pendingCount() > 0 && batch.length < this.maxBatchSize) {
|
|
239
252
|
const next = this.#pending[this.#pendingHead]!;
|
|
240
|
-
if (batch.length > 0 && batchBytes + next.bytes > this.
|
|
253
|
+
if (batch.length > 0 && batchBytes + next.bytes > this.maxBatchBytes) {
|
|
241
254
|
break;
|
|
242
255
|
}
|
|
243
256
|
this.#pendingHead += 1;
|
|
244
257
|
batch.push(next);
|
|
245
258
|
batchBytes += next.bytes;
|
|
259
|
+
this.#pendingBytes = Math.max(0, this.#pendingBytes - next.bytes);
|
|
246
260
|
}
|
|
247
261
|
if (
|
|
248
262
|
this.#pendingHead > 1_024 &&
|
|
@@ -321,6 +335,7 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
|
|
|
321
335
|
while (this.#pendingCount() > 0) {
|
|
322
336
|
const pending = this.#pending[this.#pendingHead++]!;
|
|
323
337
|
this.#residentBytes = Math.max(0, this.#residentBytes - pending.bytes);
|
|
338
|
+
this.#pendingBytes = Math.max(0, this.#pendingBytes - pending.bytes);
|
|
324
339
|
pending.reject(this.#failure);
|
|
325
340
|
}
|
|
326
341
|
this.#wakeCapacityWaiters();
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { RECEIPT_STATUS_CODE } from './receipt-status';
|
|
2
2
|
import { workReceiptFailureKindCode } from './work-receipts';
|
|
3
3
|
|
|
4
|
+
export function workReceiptRepairableFailurePredicateSql(
|
|
5
|
+
receiptTable: string,
|
|
6
|
+
): string {
|
|
7
|
+
return `(
|
|
8
|
+
${receiptTable}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
|
|
9
|
+
OR (
|
|
10
|
+
${receiptTable}.failure_kind = ${workReceiptFailureKindCode('terminal')}::smallint
|
|
11
|
+
AND ${receiptTable}.error IS NOT NULL
|
|
12
|
+
AND lower(${receiptTable}.error) NOT LIKE '%billing cap%'
|
|
13
|
+
AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
|
|
14
|
+
AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
|
|
15
|
+
AND (
|
|
16
|
+
${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
|
|
17
|
+
OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
|
|
18
|
+
OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
export function workReceiptClaimableStatusCodes(input: {
|
|
5
25
|
forceRefresh?: boolean;
|
|
6
26
|
forceFailedRefresh?: boolean;
|
|
@@ -47,7 +67,7 @@ export function workReceiptClaimConflictPredicateSql(input: {
|
|
|
47
67
|
)
|
|
48
68
|
OR ${table}.status <> ${RECEIPT_STATUS_CODE.failed}::smallint
|
|
49
69
|
OR (
|
|
50
|
-
${table
|
|
70
|
+
${workReceiptRepairableFailurePredicateSql(table)}
|
|
51
71
|
AND (
|
|
52
72
|
${table}.run_id IS DISTINCT FROM ${input.claimantRunIdSql}
|
|
53
73
|
OR COALESCE(${table}.lease_owner_attempt, 0) < ${claimantRunAttemptSql}::integer
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { RuntimeReceiptCompletionSink } from './receipt-completion-sink';
|
|
2
2
|
|
|
3
|
+
/** Claims/state transitions are small; flush once a 512 KB SQL payload forms. */
|
|
4
|
+
export const DEFAULT_RECEIPT_STATE_SINK_MAX_BATCH_BYTES = 512_000;
|
|
5
|
+
|
|
3
6
|
export interface RuntimeReceiptStateSinkOptions<TInput, TOutput> {
|
|
4
7
|
maxBatchSize?: number;
|
|
5
8
|
maxFlushMs?: number;
|
|
9
|
+
maxBatchBytes?: number;
|
|
10
|
+
maxBufferedBytes?: number;
|
|
11
|
+
maxInputBytes?: number;
|
|
6
12
|
estimateBytes?: (input: TInput) => number;
|
|
7
13
|
applyMany: (inputs: TInput[]) => Promise<TOutput[]>;
|
|
8
14
|
onFlushStart?: (event: { count: number; bytes: number }) => void;
|
|
@@ -33,6 +39,10 @@ export class RuntimeReceiptStateSink<TInput, TOutput> {
|
|
|
33
39
|
this.#sink = new RuntimeReceiptCompletionSink<TInput, TOutput>({
|
|
34
40
|
maxBatchSize: options.maxBatchSize,
|
|
35
41
|
maxFlushMs: options.maxFlushMs,
|
|
42
|
+
maxBatchBytes:
|
|
43
|
+
options.maxBatchBytes ?? DEFAULT_RECEIPT_STATE_SINK_MAX_BATCH_BYTES,
|
|
44
|
+
maxBufferedBytes: options.maxBufferedBytes,
|
|
45
|
+
maxInputBytes: options.maxInputBytes,
|
|
36
46
|
estimateBytes: options.estimateBytes,
|
|
37
47
|
completeMany: options.applyMany,
|
|
38
48
|
onFlushStart: options.onFlushStart,
|