deepline 0.3.200 → 0.3.202
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 +62 -1
- package/dist/bundling-sources/sdk/src/http.ts +55 -6
- package/dist/bundling-sources/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +10 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +160 -58
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +111 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +51 -46
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +304 -33
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +133 -5
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
- package/dist/cli/index.js +364 -39
- package/dist/cli/index.mjs +367 -41
- package/dist/index.d.mts +131 -3
- package/dist/index.d.ts +131 -3
- package/dist/index.js +64 -7
- package/dist/index.mjs +64 -7
- package/dist/install-integrity.json +3 -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
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
type PlayRunLedgerIngressEvent,
|
|
85
85
|
} from '../run-ledger';
|
|
86
86
|
import { stableStringify } from '../../plays/row-identity';
|
|
87
|
+
import { runtimeWorkCountersFromResources } from '../runtime-work-counters';
|
|
87
88
|
import {
|
|
88
89
|
isCtxFetchHttpFailureDiagnostic,
|
|
89
90
|
parseCtxFetchHttpFailureDiagnostic,
|
|
@@ -8774,13 +8775,29 @@ export async function resolvePostgresSchedulerAmbiguousModalCreateIntentFromProv
|
|
|
8774
8775
|
): Promise<boolean> {
|
|
8775
8776
|
const resolved = await client.query<{ run_id: string }>(
|
|
8776
8777
|
`
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8778
|
+
WITH resolved AS (
|
|
8779
|
+
UPDATE ${tableName('ambiguous_modal_creates', options)}
|
|
8780
|
+
SET status = 'resolved', resolved_at = clock_timestamp()
|
|
8781
|
+
WHERE run_id = $1
|
|
8782
|
+
AND attempt = $2
|
|
8783
|
+
AND modal_app_id = $3
|
|
8784
|
+
AND status = 'pending'
|
|
8785
|
+
RETURNING run_id, sandbox_capacity_lease_id
|
|
8786
|
+
), abandoned AS (
|
|
8787
|
+
UPDATE ${tableName('runtime_sandbox_operations', options)} AS operation
|
|
8788
|
+
SET state = 'abandoned', updated_at = clock_timestamp()
|
|
8789
|
+
FROM resolved
|
|
8790
|
+
WHERE operation.operation_key = resolved.sandbox_capacity_lease_id
|
|
8791
|
+
AND operation.state = 'reserved'
|
|
8792
|
+
RETURNING operation.operation_key
|
|
8793
|
+
), released AS (
|
|
8794
|
+
UPDATE ${tableName('runtime_sandbox_capacity_leases', options)} AS lease
|
|
8795
|
+
SET released_at = clock_timestamp()
|
|
8796
|
+
FROM abandoned
|
|
8797
|
+
WHERE lease.lease_id = abandoned.operation_key
|
|
8798
|
+
AND lease.released_at IS NULL
|
|
8799
|
+
)
|
|
8800
|
+
SELECT run_id FROM resolved
|
|
8784
8801
|
`,
|
|
8785
8802
|
[input.runId, input.attempt, input.modalAppId],
|
|
8786
8803
|
);
|
|
@@ -10587,6 +10604,131 @@ export async function enqueuePostgresSchedulerSandboxCleanupForAttempt(
|
|
|
10587
10604
|
return enqueued.rows.length;
|
|
10588
10605
|
}
|
|
10589
10606
|
|
|
10607
|
+
/** Queue every known sandbox before a requested cancellation can terminalize. */
|
|
10608
|
+
export async function enqueuePostgresSchedulerSandboxCleanupForRun(
|
|
10609
|
+
client: PostgresSchedulerQueryClient,
|
|
10610
|
+
input: { runId: string },
|
|
10611
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
10612
|
+
): Promise<void> {
|
|
10613
|
+
await client.query(
|
|
10614
|
+
`
|
|
10615
|
+
INSERT INTO ${tableName('sandbox_cleanup_jobs_v2', options)} (
|
|
10616
|
+
provider, sandbox_id, run_id, attempt, environment, routing_domain
|
|
10617
|
+
)
|
|
10618
|
+
SELECT DISTINCT
|
|
10619
|
+
CASE resource->>'kind'
|
|
10620
|
+
WHEN 'modal_sandbox' THEN 'modal'
|
|
10621
|
+
ELSE 'daytona'
|
|
10622
|
+
END,
|
|
10623
|
+
resource->>'sandboxId',
|
|
10624
|
+
attempt.run_id,
|
|
10625
|
+
attempt.attempt,
|
|
10626
|
+
COALESCE(
|
|
10627
|
+
NULLIF(resource->>'runtimeEnvironment', ''),
|
|
10628
|
+
NULLIF(resource->>'daytonaEnvironment', ''),
|
|
10629
|
+
'production'
|
|
10630
|
+
),
|
|
10631
|
+
CASE resource->>'kind'
|
|
10632
|
+
WHEN 'modal_sandbox' THEN NULLIF(resource->>'modalAppId', '')
|
|
10633
|
+
ELSE NULLIF(resource->>'daytonaOrganizationId', '')
|
|
10634
|
+
END
|
|
10635
|
+
FROM ${tableName('attempts', options)} AS attempt
|
|
10636
|
+
CROSS JOIN LATERAL jsonb_array_elements(
|
|
10637
|
+
CASE
|
|
10638
|
+
WHEN jsonb_typeof(attempt.resource_json) = 'array'
|
|
10639
|
+
THEN attempt.resource_json
|
|
10640
|
+
ELSE jsonb_build_array(attempt.resource_json)
|
|
10641
|
+
END
|
|
10642
|
+
) AS resource
|
|
10643
|
+
WHERE attempt.run_id = $1
|
|
10644
|
+
AND attempt.resource_json IS NOT NULL
|
|
10645
|
+
AND resource->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
|
|
10646
|
+
AND NULLIF(resource->>'sandboxId', '') IS NOT NULL
|
|
10647
|
+
UNION ALL
|
|
10648
|
+
SELECT operation.provider, operation.sandbox_id, operation.run_id,
|
|
10649
|
+
operation.attempt, operation.environment, operation.routing_domain
|
|
10650
|
+
FROM ${tableName('runtime_sandbox_operations', options)} AS operation
|
|
10651
|
+
WHERE operation.run_id = $1
|
|
10652
|
+
AND operation.state IN ('live', 'deleting')
|
|
10653
|
+
AND operation.sandbox_id IS NOT NULL
|
|
10654
|
+
ON CONFLICT (provider, sandbox_id, routing_domain_key) DO NOTHING
|
|
10655
|
+
`,
|
|
10656
|
+
[input.runId],
|
|
10657
|
+
);
|
|
10658
|
+
}
|
|
10659
|
+
|
|
10660
|
+
/** Modal cannot create without a durable intent; an old reservation with no
|
|
10661
|
+
* pending selector or recorded resource is safe to release after recovery time. */
|
|
10662
|
+
export async function abandonUnstartedModalCancellationReservations(
|
|
10663
|
+
client: PostgresSchedulerQueryClient,
|
|
10664
|
+
input: { runId: string },
|
|
10665
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
10666
|
+
): Promise<void> {
|
|
10667
|
+
await client.query(
|
|
10668
|
+
`WITH abandoned AS (
|
|
10669
|
+
UPDATE ${tableName('runtime_sandbox_operations', options)} AS operation
|
|
10670
|
+
SET state = 'abandoned', updated_at = clock_timestamp()
|
|
10671
|
+
WHERE operation.run_id = $1
|
|
10672
|
+
AND operation.provider = 'modal'
|
|
10673
|
+
AND operation.state = 'reserved'
|
|
10674
|
+
AND operation.created_at <= clock_timestamp()
|
|
10675
|
+
- ($2::double precision * interval '1 millisecond')
|
|
10676
|
+
AND NOT EXISTS (
|
|
10677
|
+
SELECT 1 FROM ${tableName('ambiguous_modal_creates', options)} AS intent
|
|
10678
|
+
WHERE intent.sandbox_capacity_lease_id = operation.operation_key
|
|
10679
|
+
AND intent.status = 'pending'
|
|
10680
|
+
)
|
|
10681
|
+
AND NOT EXISTS (
|
|
10682
|
+
SELECT 1 FROM ${tableName('attempts', options)} AS attempt
|
|
10683
|
+
CROSS JOIN LATERAL jsonb_array_elements(
|
|
10684
|
+
CASE
|
|
10685
|
+
WHEN jsonb_typeof(attempt.resource_json) = 'array'
|
|
10686
|
+
THEN attempt.resource_json
|
|
10687
|
+
WHEN attempt.resource_json IS NULL THEN '[]'::jsonb
|
|
10688
|
+
ELSE jsonb_build_array(attempt.resource_json)
|
|
10689
|
+
END
|
|
10690
|
+
) AS resource
|
|
10691
|
+
WHERE attempt.run_id = operation.run_id
|
|
10692
|
+
AND resource->>'sandboxCapacityLeaseId' = operation.operation_key
|
|
10693
|
+
)
|
|
10694
|
+
RETURNING operation.operation_key
|
|
10695
|
+
)
|
|
10696
|
+
UPDATE ${tableName('runtime_sandbox_capacity_leases', options)} AS lease
|
|
10697
|
+
SET released_at = clock_timestamp()
|
|
10698
|
+
FROM abandoned
|
|
10699
|
+
WHERE lease.lease_id = abandoned.operation_key
|
|
10700
|
+
AND lease.released_at IS NULL`,
|
|
10701
|
+
[input.runId, RUNTIME_RELIABILITY_POLICY.recovery.horizonMs],
|
|
10702
|
+
);
|
|
10703
|
+
}
|
|
10704
|
+
|
|
10705
|
+
/** A reserved create or unconfirmed deletion keeps cancellation pending. */
|
|
10706
|
+
export async function hasPendingPostgresSchedulerSandboxCleanupForRun(
|
|
10707
|
+
client: PostgresSchedulerQueryClient,
|
|
10708
|
+
input: { runId: string },
|
|
10709
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
10710
|
+
): Promise<boolean> {
|
|
10711
|
+
const pending = await client.query<{ pending: boolean }>(
|
|
10712
|
+
`
|
|
10713
|
+
SELECT EXISTS (
|
|
10714
|
+
SELECT 1
|
|
10715
|
+
FROM ${tableName('sandbox_cleanup_jobs_v2', options)}
|
|
10716
|
+
WHERE run_id = $1 AND status <> 'deleted'
|
|
10717
|
+
) OR EXISTS (
|
|
10718
|
+
SELECT 1
|
|
10719
|
+
FROM ${tableName('runtime_sandbox_operations', options)}
|
|
10720
|
+
WHERE run_id = $1 AND state = 'reserved'
|
|
10721
|
+
) OR EXISTS (
|
|
10722
|
+
SELECT 1
|
|
10723
|
+
FROM ${tableName('ambiguous_modal_creates', options)}
|
|
10724
|
+
WHERE run_id = $1 AND status = 'pending'
|
|
10725
|
+
) AS pending
|
|
10726
|
+
`,
|
|
10727
|
+
[input.runId],
|
|
10728
|
+
);
|
|
10729
|
+
return pending.rows[0]?.pending ?? true;
|
|
10730
|
+
}
|
|
10731
|
+
|
|
10590
10732
|
export async function claimPostgresSchedulerSandboxCleanupJobs(
|
|
10591
10733
|
client: PostgresSchedulerQueryClient,
|
|
10592
10734
|
input: {
|
|
@@ -12278,7 +12420,26 @@ export async function cancelPostgresSchedulerRun(
|
|
|
12278
12420
|
WHEN elem->>'kind' IN ('daytona_sandbox', 'modal_sandbox')
|
|
12279
12421
|
AND (elem->'billingEndedAt' IS NULL OR elem->'billingEndedAt' = 'null'::jsonb)
|
|
12280
12422
|
THEN elem || jsonb_build_object(
|
|
12281
|
-
'billingEndedAt',
|
|
12423
|
+
'billingEndedAt', COALESCE(
|
|
12424
|
+
(
|
|
12425
|
+
SELECT extract(epoch FROM cleanup.deleted_at) * 1000
|
|
12426
|
+
FROM ${tableName('sandbox_cleanup_jobs_v2', options)} AS cleanup
|
|
12427
|
+
WHERE cleanup.provider = CASE elem->>'kind'
|
|
12428
|
+
WHEN 'modal_sandbox' THEN 'modal'
|
|
12429
|
+
ELSE 'daytona'
|
|
12430
|
+
END
|
|
12431
|
+
AND cleanup.sandbox_id = elem->>'sandboxId'
|
|
12432
|
+
AND cleanup.routing_domain_key = COALESCE(
|
|
12433
|
+
CASE elem->>'kind'
|
|
12434
|
+
WHEN 'modal_sandbox' THEN NULLIF(elem->>'modalAppId', '')
|
|
12435
|
+
ELSE NULLIF(elem->>'daytonaOrganizationId', '')
|
|
12436
|
+
END,
|
|
12437
|
+
''
|
|
12438
|
+
)
|
|
12439
|
+
AND cleanup.status = 'deleted'
|
|
12440
|
+
),
|
|
12441
|
+
extract(epoch FROM clock_timestamp()) * 1000
|
|
12442
|
+
),
|
|
12282
12443
|
'terminalAt', extract(epoch FROM clock_timestamp()) * 1000,
|
|
12283
12444
|
'terminalReason', 'cancelled'
|
|
12284
12445
|
)
|
|
@@ -12885,7 +13046,7 @@ function terminalLogLinesForOutbox(
|
|
|
12885
13046
|
.slice(-LOG_TAIL_LIMIT);
|
|
12886
13047
|
}
|
|
12887
13048
|
|
|
12888
|
-
function attachRunnerWorkProgressMetadata(
|
|
13049
|
+
export function attachRunnerWorkProgressMetadata(
|
|
12889
13050
|
output: unknown,
|
|
12890
13051
|
runnerResult: unknown,
|
|
12891
13052
|
options: {
|
|
@@ -12937,6 +13098,13 @@ function attachRunnerWorkProgressMetadata(
|
|
|
12937
13098
|
const runtimeResources = metadata?.runtimeResources
|
|
12938
13099
|
? null
|
|
12939
13100
|
: boundedRuntimeResourcesMetadata(runnerResult);
|
|
13101
|
+
// Keep the two scalar counts when the detailed runtimeResources record
|
|
13102
|
+
// exceeds its metadata budget. Compact status must not scan a call log to
|
|
13103
|
+
// reconstruct physical work.
|
|
13104
|
+
const runtimeCounters = runtimeWorkCountersFromResources(
|
|
13105
|
+
(runnerResult as { stats?: { runtimeResources?: unknown } }).stats
|
|
13106
|
+
?.runtimeResources,
|
|
13107
|
+
);
|
|
12940
13108
|
const outputWarnings = Array.isArray(
|
|
12941
13109
|
(runnerResult as { outputWarnings?: unknown }).outputWarnings,
|
|
12942
13110
|
)
|
|
@@ -12979,6 +13147,8 @@ function attachRunnerWorkProgressMetadata(
|
|
|
12979
13147
|
(total === null || skipped === null || metadata?.workProgress) &&
|
|
12980
13148
|
!runLogTail &&
|
|
12981
13149
|
!runtimeResources &&
|
|
13150
|
+
!runtimeCounters &&
|
|
13151
|
+
!metadata?.runtimeCounters &&
|
|
12982
13152
|
!rowOutcomes &&
|
|
12983
13153
|
mergedOutputWarnings.length === 0
|
|
12984
13154
|
) {
|
|
@@ -13010,6 +13180,9 @@ function attachRunnerWorkProgressMetadata(
|
|
|
13010
13180
|
...record,
|
|
13011
13181
|
_metadata: {
|
|
13012
13182
|
...(metadata ?? {}),
|
|
13183
|
+
// A previous attempt's scalar evidence cannot describe this terminal
|
|
13184
|
+
// attempt. Undefined removes it from persisted JSON when unavailable.
|
|
13185
|
+
runtimeCounters: runtimeCounters ?? undefined,
|
|
13013
13186
|
...(workProgress ? { workProgress } : {}),
|
|
13014
13187
|
...(rowOutcomes ? { rowOutcomes } : {}),
|
|
13015
13188
|
...(runLogTail ? { runLogTail } : {}),
|
|
@@ -15507,8 +15680,63 @@ export async function readPostgresSchedulerResult(
|
|
|
15507
15680
|
export async function readPostgresSchedulerRunStatus(
|
|
15508
15681
|
client: PostgresSchedulerQueryClient,
|
|
15509
15682
|
runId: string,
|
|
15510
|
-
options?: Pick<PostgresSchedulerOptions, 'schema'
|
|
15683
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'> & {
|
|
15684
|
+
includeDetails?: boolean;
|
|
15685
|
+
},
|
|
15511
15686
|
): Promise<PostgresSchedulerRunStatusSnapshot | null> {
|
|
15687
|
+
if (options?.includeDetails === false) {
|
|
15688
|
+
// An admission projection gap needs lifecycle truth only. Launch, result,
|
|
15689
|
+
// suspension and error JSON may contain row/call-sized detail: never even
|
|
15690
|
+
// select them for status polling. Missing progress remains unconfirmed.
|
|
15691
|
+
const result = await client.query<
|
|
15692
|
+
Pick<
|
|
15693
|
+
RunStatusRow,
|
|
15694
|
+
| 'run_id'
|
|
15695
|
+
| 'org_id'
|
|
15696
|
+
| 'status'
|
|
15697
|
+
| 'play_name'
|
|
15698
|
+
| 'graph_hash'
|
|
15699
|
+
| 'runtime_profile'
|
|
15700
|
+
| 'created_at'
|
|
15701
|
+
| 'updated_at'
|
|
15702
|
+
| 'started_at'
|
|
15703
|
+
| 'finished_at'
|
|
15704
|
+
>
|
|
15705
|
+
>(
|
|
15706
|
+
`
|
|
15707
|
+
SELECT run_id, org_id, status, play_name, graph_hash, runtime_profile,
|
|
15708
|
+
created_at, updated_at, started_at, finished_at
|
|
15709
|
+
FROM ${tableName('runs', options)}
|
|
15710
|
+
WHERE run_id = $1
|
|
15711
|
+
`,
|
|
15712
|
+
[runId],
|
|
15713
|
+
);
|
|
15714
|
+
const row = result.rows[0];
|
|
15715
|
+
if (!row) return null;
|
|
15716
|
+
return {
|
|
15717
|
+
runId: row.run_id,
|
|
15718
|
+
orgId: row.org_id ?? '',
|
|
15719
|
+
status: row.status,
|
|
15720
|
+
playName: row.play_name,
|
|
15721
|
+
graphHash: row.graph_hash,
|
|
15722
|
+
runtimeBackend: null,
|
|
15723
|
+
schedulerBackend: 'absurd',
|
|
15724
|
+
executionProfile: row.runtime_profile,
|
|
15725
|
+
createdAt: row.created_at?.getTime() ?? null,
|
|
15726
|
+
updatedAt: row.updated_at?.getTime() ?? null,
|
|
15727
|
+
startedAt: row.started_at?.getTime() ?? null,
|
|
15728
|
+
finishedAt: row.finished_at?.getTime() ?? null,
|
|
15729
|
+
durationMs:
|
|
15730
|
+
row.finished_at && row.started_at
|
|
15731
|
+
? Math.max(0, row.finished_at.getTime() - row.started_at.getTime())
|
|
15732
|
+
: null,
|
|
15733
|
+
runtimeTiming: null,
|
|
15734
|
+
result: null,
|
|
15735
|
+
error: null,
|
|
15736
|
+
suspension: null,
|
|
15737
|
+
staticPipeline: null,
|
|
15738
|
+
};
|
|
15739
|
+
}
|
|
15512
15740
|
const result = await client.query<RunStatusRow>(
|
|
15513
15741
|
`
|
|
15514
15742
|
SELECT run_id, org_id, status, play_name, graph_hash, runtime_profile,
|
|
@@ -15723,6 +15951,10 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15723
15951
|
runId: string;
|
|
15724
15952
|
idempotencyKey: string;
|
|
15725
15953
|
leaseOwner?: string;
|
|
15954
|
+
projectedEvents?: readonly AcceptedPlayRunLedgerEvent[];
|
|
15955
|
+
normalizeProjectedEvents?: (
|
|
15956
|
+
events: readonly PlayRunLedgerIngressEvent[],
|
|
15957
|
+
) => readonly AcceptedPlayRunLedgerEvent[];
|
|
15726
15958
|
},
|
|
15727
15959
|
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
15728
15960
|
): Promise<void> {
|
|
@@ -15733,6 +15965,8 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15733
15965
|
lease_live: boolean;
|
|
15734
15966
|
due: boolean;
|
|
15735
15967
|
blocked: boolean;
|
|
15968
|
+
payload_json: unknown;
|
|
15969
|
+
created_at: Date;
|
|
15736
15970
|
}>(
|
|
15737
15971
|
`
|
|
15738
15972
|
SELECT outbox.delivery_key,
|
|
@@ -15740,7 +15974,9 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15740
15974
|
outbox.lease_owner,
|
|
15741
15975
|
outbox.lease_expires_at > now() AS lease_live,
|
|
15742
15976
|
coalesce(outbox.next_attempt_at, outbox.created_at) <= now() AS due,
|
|
15743
|
-
outbox.failed_at IS NOT NULL AS blocked
|
|
15977
|
+
outbox.failed_at IS NOT NULL AS blocked,
|
|
15978
|
+
outbox.payload_json,
|
|
15979
|
+
outbox.created_at
|
|
15744
15980
|
FROM ${tableName('outbox', options)} AS outbox
|
|
15745
15981
|
JOIN ${tableName('runs', options)} AS runs ON runs.run_id = outbox.run_id
|
|
15746
15982
|
WHERE outbox.run_id = $1 AND runs.org_id = $2
|
|
@@ -15755,12 +15991,9 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15755
15991
|
throw new RunLedgerProjectionLeaseLostError();
|
|
15756
15992
|
}
|
|
15757
15993
|
|
|
15758
|
-
//
|
|
15759
|
-
//
|
|
15760
|
-
//
|
|
15761
|
-
// one row at a time. Validate the submitted key against a *contiguous due
|
|
15762
|
-
// prefix*, not every row carrying that owner; otherwise a valid cancellation
|
|
15763
|
-
// head is rejected whenever later facts share the cancellation lease.
|
|
15994
|
+
// Cancellation may own several pending facts at once. Validate the exact
|
|
15995
|
+
// contiguous, due prefix named by the receipt instead of all rows sharing
|
|
15996
|
+
// that cancellation lease.
|
|
15764
15997
|
const prefix: typeof rows = [];
|
|
15765
15998
|
for (const row of rows) {
|
|
15766
15999
|
if (
|
|
@@ -15780,6 +16013,9 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15780
16013
|
candidate.event_type,
|
|
15781
16014
|
),
|
|
15782
16015
|
);
|
|
16016
|
+
const prefixDigest = createHash('sha256')
|
|
16017
|
+
.update(prefix.map((candidate) => candidate.delivery_key).join('\n'))
|
|
16018
|
+
.digest('hex');
|
|
15783
16019
|
let expectedKey: string;
|
|
15784
16020
|
if (terminal) {
|
|
15785
16021
|
expectedKey = buildRunLedgerTerminalIdempotencyKey({
|
|
@@ -15795,29 +16031,58 @@ export async function assertPostgresRunLedgerDelivery(
|
|
|
15795
16031
|
deliveryKey: first.delivery_key,
|
|
15796
16032
|
});
|
|
15797
16033
|
} else {
|
|
15798
|
-
const digest = createHash('sha256')
|
|
15799
|
-
.update(prefix.map((candidate) => candidate.delivery_key).join('\n'))
|
|
15800
|
-
.digest('hex');
|
|
15801
16034
|
expectedKey = buildRunLedgerEventsIdempotencyKey({
|
|
15802
16035
|
runId: input.runId,
|
|
15803
|
-
deliveryDigest:
|
|
16036
|
+
deliveryDigest: prefixDigest,
|
|
15804
16037
|
});
|
|
15805
16038
|
}
|
|
15806
|
-
if (input.idempotencyKey === expectedKey) return;
|
|
15807
16039
|
|
|
15808
|
-
//
|
|
15809
|
-
//
|
|
15810
|
-
|
|
15811
|
-
|
|
16040
|
+
// Older batch dispatchers may use the digest receipt even when the batch
|
|
16041
|
+
// contains a terminal fact. Keep accepting it for this exact prefix.
|
|
16042
|
+
const batchKey = buildRunLedgerEventsIdempotencyKey({
|
|
16043
|
+
runId: input.runId,
|
|
16044
|
+
deliveryDigest: prefixDigest,
|
|
16045
|
+
});
|
|
16046
|
+
|
|
16047
|
+
// Preserve the old singleton key for non-start, nonterminal facts.
|
|
16048
|
+
const legacySingletonKey =
|
|
15812
16049
|
prefix.length === 1 &&
|
|
15813
16050
|
!['run.created', 'run.started'].includes(first.event_type) &&
|
|
15814
16051
|
input.idempotencyKey ===
|
|
15815
16052
|
buildRunLedgerTerminalIdempotencyKey({
|
|
15816
16053
|
runId: input.runId,
|
|
15817
16054
|
deliveryKey: first.delivery_key,
|
|
15818
|
-
})
|
|
16055
|
+
});
|
|
16056
|
+
if (
|
|
16057
|
+
input.idempotencyKey === expectedKey ||
|
|
16058
|
+
input.idempotencyKey === batchKey ||
|
|
16059
|
+
legacySingletonKey
|
|
15819
16060
|
) {
|
|
15820
|
-
return;
|
|
16061
|
+
if (input.projectedEvents === undefined) return;
|
|
16062
|
+
const prefixEvents: PlayRunLedgerIngressEvent[] = [];
|
|
16063
|
+
for (const prefixRow of prefix) {
|
|
16064
|
+
prefixEvents.push(
|
|
16065
|
+
...buildRunLedgerEventsFromPostgresSchedulerOutbox({
|
|
16066
|
+
eventId: prefixRow.delivery_key,
|
|
16067
|
+
runId: input.runId,
|
|
16068
|
+
eventType: prefixRow.event_type,
|
|
16069
|
+
payload: prefixRow.payload_json,
|
|
16070
|
+
createdAt: new Date(prefixRow.created_at),
|
|
16071
|
+
leaseOwner: prefixRow.lease_owner ?? undefined,
|
|
16072
|
+
deliveryKey: prefixRow.delivery_key,
|
|
16073
|
+
}),
|
|
16074
|
+
);
|
|
16075
|
+
}
|
|
16076
|
+
const comparablePrefixEvents = input.normalizeProjectedEvents
|
|
16077
|
+
? input.normalizeProjectedEvents(prefixEvents)
|
|
16078
|
+
: prefixEvents;
|
|
16079
|
+
if (
|
|
16080
|
+
stableStringify(input.projectedEvents) ===
|
|
16081
|
+
stableStringify(comparablePrefixEvents)
|
|
16082
|
+
) {
|
|
16083
|
+
return;
|
|
16084
|
+
}
|
|
16085
|
+
throw new RunLedgerProjectionLeaseLostError();
|
|
15821
16086
|
}
|
|
15822
16087
|
|
|
15823
16088
|
if (
|
|
@@ -15997,7 +16262,7 @@ export async function readPendingPostgresSchedulerOutboxEvents(
|
|
|
15997
16262
|
)
|
|
15998
16263
|
)
|
|
15999
16264
|
)
|
|
16000
|
-
ORDER BY outbox
|
|
16265
|
+
ORDER BY ${runLedgerOrderSql('outbox')}
|
|
16001
16266
|
FOR UPDATE OF outbox
|
|
16002
16267
|
LIMIT $1
|
|
16003
16268
|
), terminal_batch AS (
|
|
@@ -16033,7 +16298,7 @@ export async function readPendingPostgresSchedulerOutboxEvents(
|
|
|
16033
16298
|
SELECT event_id, run_id, event_type, payload_json, created_at, lease_owner,
|
|
16034
16299
|
delivery_key
|
|
16035
16300
|
FROM claimed
|
|
16036
|
-
ORDER BY
|
|
16301
|
+
ORDER BY ${runLedgerOrderSql('claimed')}
|
|
16037
16302
|
`,
|
|
16038
16303
|
[limit, runId, leaseOwner, leaseSeconds],
|
|
16039
16304
|
);
|
|
@@ -16127,7 +16392,9 @@ export async function readPostgresSchedulerOutboxNextDueAt(
|
|
|
16127
16392
|
export async function readPostgresSchedulerOutboxDeliveryStatus(
|
|
16128
16393
|
client: PostgresSchedulerQueryClient,
|
|
16129
16394
|
input: { runId: string; eventId: string },
|
|
16130
|
-
options?: Pick<PostgresSchedulerOptions, 'schema'
|
|
16395
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'> & {
|
|
16396
|
+
includeBlocked?: boolean;
|
|
16397
|
+
},
|
|
16131
16398
|
): Promise<PostgresSchedulerOutboxDeliveryStatus> {
|
|
16132
16399
|
const runId = input.runId.trim();
|
|
16133
16400
|
const eventId = input.eventId.trim();
|
|
@@ -16138,13 +16405,17 @@ export async function readPostgresSchedulerOutboxDeliveryStatus(
|
|
|
16138
16405
|
}
|
|
16139
16406
|
const result = await client.query<{ delivered: boolean; blocked: boolean }>(
|
|
16140
16407
|
`SELECT target.delivered_at IS NOT NULL AS delivered,
|
|
16141
|
-
|
|
16408
|
+
${
|
|
16409
|
+
options?.includeBlocked === false
|
|
16410
|
+
? 'false'
|
|
16411
|
+
: `target.failed_at IS NOT NULL OR EXISTS (
|
|
16142
16412
|
SELECT 1
|
|
16143
16413
|
FROM ${tableName('outbox', options)} predecessor
|
|
16144
16414
|
WHERE predecessor.run_id = target.run_id
|
|
16145
16415
|
AND predecessor.failed_at IS NOT NULL
|
|
16146
16416
|
AND ${runLedgerPrecedesSql('predecessor', 'target', true)}
|
|
16147
|
-
)
|
|
16417
|
+
)`
|
|
16418
|
+
} AS blocked
|
|
16148
16419
|
FROM ${tableName('outbox', options)} target
|
|
16149
16420
|
WHERE target.run_id = $1 AND target.event_id = $2`,
|
|
16150
16421
|
[runId, eventId],
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
createRuntimeReceiptValuePersister,
|
|
67
67
|
createRuntimeReceiptValueReader,
|
|
68
68
|
RUNTIME_RECEIPT_VALUE_INLINE_MAX_BYTES,
|
|
69
|
+
RuntimeValueStoreUnavailableError,
|
|
69
70
|
type RuntimeReceiptValueBlobStore,
|
|
70
71
|
type RuntimeReceiptValuePersister,
|
|
71
72
|
type RuntimeReceiptValueReader,
|
|
@@ -250,6 +251,26 @@ export function receiptGatewayFailureResponse(
|
|
|
250
251
|
},
|
|
251
252
|
);
|
|
252
253
|
}
|
|
254
|
+
if (error instanceof RuntimeValueStoreUnavailableError) {
|
|
255
|
+
return receiptGatewayJsonResponse(
|
|
256
|
+
{
|
|
257
|
+
error: `${operationLabel} object store is temporarily unavailable.`,
|
|
258
|
+
code: 'runtime_value_store_unavailable',
|
|
259
|
+
retryable: true,
|
|
260
|
+
retry_after_ms: 1_000,
|
|
261
|
+
detail: error.message,
|
|
262
|
+
...(context.action ? { action: context.action } : {}),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
status: 503,
|
|
266
|
+
headers: {
|
|
267
|
+
'retry-after': '1',
|
|
268
|
+
'x-deepline-error-code': 'runtime_value_store_unavailable',
|
|
269
|
+
'x-deepline-error-detail': error.message,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
253
274
|
if (isRuntimeSchedulerCapacityError(error)) {
|
|
254
275
|
const retryAfterMs = runtimeSchedulerCapacityRetryAfterMs(error);
|
|
255
276
|
return receiptGatewayJsonResponse(
|
|
@@ -25,6 +25,40 @@ export type RuntimeValueBlobStore = {
|
|
|
25
25
|
/** Backward-compatible receipt-specific name for the shared object store. */
|
|
26
26
|
export type RuntimeReceiptValueBlobStore = RuntimeValueBlobStore;
|
|
27
27
|
|
|
28
|
+
export class RuntimeValueStoreUnavailableError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
readonly operation: 'put' | 'head',
|
|
31
|
+
readonly status: number,
|
|
32
|
+
cause: unknown,
|
|
33
|
+
) {
|
|
34
|
+
super(`Runtime value object store ${operation} returned HTTP ${status}.`, {
|
|
35
|
+
cause,
|
|
36
|
+
});
|
|
37
|
+
this.name = 'RuntimeValueStoreUnavailableError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function transientStoreStatus(error: unknown): number | null {
|
|
42
|
+
if (!error || typeof error !== 'object') return null;
|
|
43
|
+
const metadata = (error as { $metadata?: { httpStatusCode?: unknown } })
|
|
44
|
+
.$metadata;
|
|
45
|
+
const status = metadata?.httpStatusCode;
|
|
46
|
+
return typeof status === 'number' &&
|
|
47
|
+
(status === 408 || status === 429 || status >= 500)
|
|
48
|
+
? status
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function transientStoreError(
|
|
53
|
+
operation: 'put' | 'head',
|
|
54
|
+
error: unknown,
|
|
55
|
+
): RuntimeValueStoreUnavailableError | null {
|
|
56
|
+
const status = transientStoreStatus(error);
|
|
57
|
+
return status === null
|
|
58
|
+
? null
|
|
59
|
+
: new RuntimeValueStoreUnavailableError(operation, status, error);
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
/**
|
|
29
63
|
* Foundation owns value references and bounded I/O admission; the deployment
|
|
30
64
|
* injects its object-store adapter. This keeps R2 credentials and transport
|
|
@@ -185,7 +219,13 @@ export async function persistRuntimeReceiptValue(
|
|
|
185
219
|
// An immutable deterministic object that is observable after a failed PUT
|
|
186
220
|
// is the same attempted publication. Treat it as a recovered write, never
|
|
187
221
|
// as permission to call the provider again.
|
|
188
|
-
|
|
222
|
+
let exists: boolean;
|
|
223
|
+
try {
|
|
224
|
+
exists = await store.exists(outputRef.key);
|
|
225
|
+
} catch (probeError) {
|
|
226
|
+
throw transientStoreError('head', probeError) ?? probeError;
|
|
227
|
+
}
|
|
228
|
+
if (!exists) throw transientStoreError('put', error) ?? error;
|
|
189
229
|
}
|
|
190
230
|
return { kind: 'reference', outputJson: null, outputRef };
|
|
191
231
|
}
|
|
@@ -209,11 +249,10 @@ export function createRuntimeReceiptValuePersister(
|
|
|
209
249
|
serialized: string;
|
|
210
250
|
inlineFields?: RuntimeReceiptValueInlineFields;
|
|
211
251
|
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>({
|
|
252
|
+
type WriteOutcome =
|
|
253
|
+
| { ok: true; value: PersistedRuntimeReceiptValue }
|
|
254
|
+
| { ok: false; error: unknown };
|
|
255
|
+
const writes = new BoundedBatchFunction<Write, WriteOutcome, 'r2'>({
|
|
217
256
|
keyId: () => 'r2',
|
|
218
257
|
estimateBytes: (value) => Buffer.byteLength(value.serialized, 'utf8'),
|
|
219
258
|
maxBatchItems: 4,
|
|
@@ -224,16 +263,24 @@ export function createRuntimeReceiptValuePersister(
|
|
|
224
263
|
maxRetainedUnits: 32,
|
|
225
264
|
maxInFlightBatches: 2,
|
|
226
265
|
flushMs: 0,
|
|
266
|
+
// R2 has no multi-PUT. One failed object must not reject the other
|
|
267
|
+
// independent receipts admitted alongside it.
|
|
227
268
|
execute: async ({ values }) =>
|
|
228
269
|
await Promise.all(
|
|
229
|
-
values.map(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
270
|
+
values.map(async (value): Promise<WriteOutcome> => {
|
|
271
|
+
try {
|
|
272
|
+
return {
|
|
273
|
+
ok: true,
|
|
274
|
+
value: await persistRuntimeReceiptValue({
|
|
275
|
+
...value,
|
|
276
|
+
inlineMaxBytes,
|
|
277
|
+
...(input.store ? { store: input.store } : {}),
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { ok: false, error };
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
237
284
|
),
|
|
238
285
|
});
|
|
239
286
|
return async (value) => {
|
|
@@ -244,7 +291,9 @@ export function createRuntimeReceiptValuePersister(
|
|
|
244
291
|
...(input.store ? { store: input.store } : {}),
|
|
245
292
|
});
|
|
246
293
|
}
|
|
247
|
-
|
|
294
|
+
const outcome = await writes.submit({ key: 'r2', value });
|
|
295
|
+
if (!outcome.ok) throw outcome.error;
|
|
296
|
+
return outcome.value;
|
|
248
297
|
};
|
|
249
298
|
}
|
|
250
299
|
|