deepline 0.3.128 → 0.3.129
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 +34 -9
- package/dist/bundling-sources/sdk/src/http.ts +22 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
- package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
- package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
- package/dist/cli/index.js +250 -38
- package/dist/cli/index.mjs +250 -38
- package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
- package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +231 -35
- package/dist/index.mjs +231 -35
- package/dist/install-integrity.json +31 -2
- 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 -1
- 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
|
@@ -20,6 +20,7 @@ import { PROD_DEFAULT_RUNTIME_SCHEDULER_SCHEMA } from '../runtime-scheduler-topo
|
|
|
20
20
|
import type { PlayRunnerRuntimeResource } from '../runner-backends/types';
|
|
21
21
|
import {
|
|
22
22
|
RuntimeSandboxOperationState,
|
|
23
|
+
type RuntimeSandboxProvider,
|
|
23
24
|
runtimeSandboxProviderForResourceKind,
|
|
24
25
|
} from './runtime-sandbox-operation';
|
|
25
26
|
import {
|
|
@@ -53,6 +54,10 @@ import type {
|
|
|
53
54
|
RuntimeStepReceipt,
|
|
54
55
|
SkipRuntimeStepReceiptInput,
|
|
55
56
|
} from '../ctx-types';
|
|
57
|
+
import {
|
|
58
|
+
isRuntimeReceiptValueReference,
|
|
59
|
+
type RuntimeReceiptValueReference,
|
|
60
|
+
} from '../runtime-receipt-value';
|
|
56
61
|
import { PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS } from '../lease-policy';
|
|
57
62
|
import { RUNTIME_RELIABILITY_POLICY } from '../runtime-reliability-policy';
|
|
58
63
|
import { COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID } from '../durable-receipt-execution';
|
|
@@ -217,12 +222,46 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
|
|
|
217
222
|
* interrupted deployment cannot claim a newer version without its required
|
|
218
223
|
* objects.
|
|
219
224
|
*/
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
225
|
+
// v34 adds the delivered-outbox retention index. It is additive: old outbox
|
|
226
|
+
// rows and their durable delivery identities remain readable during the
|
|
227
|
+
// rolling window.
|
|
228
|
+
// v35 adds the admission-priority outbox index and wake trigger. They remain
|
|
229
|
+
// additive: the projector still leases every event from durable storage.
|
|
230
|
+
// v36 adds durable rate-permit reservations, so provider cancellation can
|
|
231
|
+
// distinguish an unused permit from one that may have reached the provider.
|
|
232
|
+
// Additive: a v34 reader ignores the table.
|
|
233
|
+
// v37 reserves immutable Integration Operation bodies before object upload.
|
|
234
|
+
// An old gateway ignores the nullable column; a current writer refuses to
|
|
235
|
+
// publish a reference without its fenced pending owner.
|
|
236
|
+
// v38 adds the durable Gradient2 provider-concurrency checkpoint. Older
|
|
237
|
+
// runners keep their declared cap; current runners only require the additive
|
|
238
|
+
// fields after the deploy-owned schema marker has advanced.
|
|
239
|
+
// v39 adds durable deduplication for buffered observations and durable
|
|
240
|
+
// server-authored 429 attribution. A lost acquire response cannot replay
|
|
241
|
+
// learned capacity, and a runner cannot forge occupancy/RTT feedback.
|
|
242
|
+
// v40 combines Foundation's additive objects with main's blocked-outbox
|
|
243
|
+
// oldest-first health index. Deployment must apply both branches' objects.
|
|
244
|
+
// v41 adds the runtime-owned cron schedule cursor and its due-time index from
|
|
245
|
+
// main's durable trigger admission (#5779). That branch numbered the same
|
|
246
|
+
// objects v37, which this lineage had already spent on immutable Integration
|
|
247
|
+
// Operation bodies, so the merged schema carries them here instead. The table
|
|
248
|
+
// is additive on both sides: v40 admission and outbox projection stay valid
|
|
249
|
+
// while a deploy expands the shared schema, and the marker then records the
|
|
250
|
+
// completed runtime-cron capability for deployment tooling.
|
|
251
|
+
// v42 absorbs main's bounded health indexes for the runtime-owned outbox,
|
|
252
|
+
// sandbox cleanup, and compute-billing queues (#6086 lineage). That branch
|
|
253
|
+
// numbered them v38, which this lineage had already spent, so the objects are
|
|
254
|
+
// carried here instead; all nine are `CREATE INDEX IF NOT EXISTS` and additive,
|
|
255
|
+
// so v41 admission and outbox projection stay valid while a deploy expands the
|
|
256
|
+
// shared schema, and the marker then records the completed health capability.
|
|
257
|
+
// v43 adds per-run fair-share tracking for provider rate buckets: the
|
|
258
|
+
// `rate_requesters` table and a nullable `requester_id` on concurrency leases.
|
|
259
|
+
// Both are additive; only an acquire that carries a run identity reads them, so
|
|
260
|
+
// v42 runtime paths stay valid while a deploy expands the shared schema.
|
|
261
|
+
// v44 moves the v39-v43 shared-schema expansion onto the online, statement-
|
|
262
|
+
// scoped migration path. The reader contract is unchanged; the marker ensures
|
|
263
|
+
// deployment retries do not re-enter the old cross-table DDL transaction.
|
|
264
|
+
export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 44;
|
|
226
265
|
const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
|
|
227
266
|
'scheduler_schema_migrations';
|
|
228
267
|
export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
|
|
@@ -514,6 +553,8 @@ type ReceiptRow = {
|
|
|
514
553
|
k: string;
|
|
515
554
|
status: string;
|
|
516
555
|
output_json: unknown | null;
|
|
556
|
+
/** Nullable during the inline/reference reader expansion. */
|
|
557
|
+
output_ref?: unknown | null;
|
|
517
558
|
error: string | null;
|
|
518
559
|
error_payload: unknown | null;
|
|
519
560
|
failure_kind: number | string | null;
|
|
@@ -1004,8 +1045,18 @@ export type PostgresSchedulerSchemaShapeRequirements = Readonly<{
|
|
|
1004
1045
|
blockedOutboxIndex?: boolean;
|
|
1005
1046
|
admissionPriorityIndex?: boolean;
|
|
1006
1047
|
terminalComputeBillingOutboxTrigger?: boolean;
|
|
1048
|
+
/** v35 added the table; a v34 reader does not require it. */
|
|
1049
|
+
rateReservations?: boolean;
|
|
1050
|
+
/** v36 added the pending immutable-value owner; a v35 reader ignores it. */
|
|
1051
|
+
integrationOperationValueReservation?: boolean;
|
|
1052
|
+
/** v37 added the durable Gradient2 checkpoint; a v36 reader ignores it. */
|
|
1053
|
+
gradient2ConcurrencyCheckpoint?: boolean;
|
|
1054
|
+
/** v38 makes buffered Gradient2 observations idempotent; v37 ignores it. */
|
|
1055
|
+
gradient2ObservationDeduplication?: boolean;
|
|
1007
1056
|
outboxWakeTrigger?: boolean;
|
|
1008
1057
|
cronScheduleSchema?: boolean;
|
|
1058
|
+
/** v43 added per-run fair-share tracking for rate buckets; v42 ignores it. */
|
|
1059
|
+
rateRequesterFairShare?: boolean;
|
|
1009
1060
|
}>;
|
|
1010
1061
|
|
|
1011
1062
|
export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
@@ -1077,7 +1128,51 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1077
1128
|
AND column_default IS NOT NULL
|
|
1078
1129
|
)
|
|
1079
1130
|
AND (
|
|
1080
|
-
$
|
|
1131
|
+
$27::boolean = false
|
|
1132
|
+
OR (
|
|
1133
|
+
SELECT count(DISTINCT column_name)
|
|
1134
|
+
FROM information_schema.columns
|
|
1135
|
+
WHERE table_schema = $1
|
|
1136
|
+
AND table_name = 'rate_bucket'
|
|
1137
|
+
AND column_name IN (
|
|
1138
|
+
'learned_concurrency_limit',
|
|
1139
|
+
'concurrency_short_rtt_ms',
|
|
1140
|
+
'concurrency_long_rtt_ms',
|
|
1141
|
+
'concurrency_minimum_rtt_ms',
|
|
1142
|
+
'concurrency_successful_samples',
|
|
1143
|
+
'concurrency_window_limit',
|
|
1144
|
+
'concurrency_has_observed_throttle'
|
|
1145
|
+
)
|
|
1146
|
+
) = 7
|
|
1147
|
+
)
|
|
1148
|
+
AND (
|
|
1149
|
+
$28::boolean = false
|
|
1150
|
+
OR (
|
|
1151
|
+
to_regclass($29) IS NOT NULL
|
|
1152
|
+
AND EXISTS (
|
|
1153
|
+
SELECT 1
|
|
1154
|
+
FROM information_schema.columns
|
|
1155
|
+
WHERE table_schema = $1
|
|
1156
|
+
AND table_name = 'rate_concurrency_observers'
|
|
1157
|
+
AND column_name = 'expires_at_ms'
|
|
1158
|
+
AND data_type = 'bigint'
|
|
1159
|
+
AND is_nullable = 'NO'
|
|
1160
|
+
)
|
|
1161
|
+
AND (
|
|
1162
|
+
SELECT count(DISTINCT column_name)
|
|
1163
|
+
FROM information_schema.columns
|
|
1164
|
+
WHERE table_schema = $1
|
|
1165
|
+
AND table_name = 'rate_concurrency_leases'
|
|
1166
|
+
AND column_name IN (
|
|
1167
|
+
'in_flight_at_start',
|
|
1168
|
+
'target_requests_per_second',
|
|
1169
|
+
'minimum_rtt_ms'
|
|
1170
|
+
)
|
|
1171
|
+
) = 3
|
|
1172
|
+
)
|
|
1173
|
+
)
|
|
1174
|
+
AND (
|
|
1175
|
+
$31::boolean = false
|
|
1081
1176
|
OR EXISTS (
|
|
1082
1177
|
SELECT 1
|
|
1083
1178
|
FROM pg_index AS index_row
|
|
@@ -1088,8 +1183,8 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1088
1183
|
WHERE index_namespace.nspname = $1
|
|
1089
1184
|
AND index_class.relname = 'runtime_scheduler_outbox_admission_pending_idx'
|
|
1090
1185
|
AND index_row.indrelid = to_regclass($8)
|
|
1091
|
-
|
|
1092
|
-
|
|
1186
|
+
AND index_row.indisready
|
|
1187
|
+
AND index_row.indisvalid
|
|
1093
1188
|
)
|
|
1094
1189
|
)
|
|
1095
1190
|
AND (
|
|
@@ -1103,7 +1198,7 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1103
1198
|
)
|
|
1104
1199
|
)
|
|
1105
1200
|
AND (
|
|
1106
|
-
$
|
|
1201
|
+
$30::boolean = false
|
|
1107
1202
|
OR EXISTS (
|
|
1108
1203
|
SELECT 1
|
|
1109
1204
|
FROM pg_trigger
|
|
@@ -1254,10 +1349,24 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1254
1349
|
AND to_regclass($20) IS NOT NULL
|
|
1255
1350
|
AND to_regclass($21) IS NOT NULL
|
|
1256
1351
|
AND to_regclass($22) IS NOT NULL
|
|
1352
|
+
AND ($24::boolean = false OR to_regclass($23) IS NOT NULL)
|
|
1257
1353
|
AND (
|
|
1258
|
-
$
|
|
1354
|
+
$34::boolean = false
|
|
1259
1355
|
OR (
|
|
1260
|
-
to_regclass($
|
|
1356
|
+
to_regclass($35) IS NOT NULL
|
|
1357
|
+
AND EXISTS (
|
|
1358
|
+
SELECT 1
|
|
1359
|
+
FROM information_schema.columns
|
|
1360
|
+
WHERE table_schema = $1
|
|
1361
|
+
AND table_name = 'rate_concurrency_leases'
|
|
1362
|
+
AND column_name = 'requester_id'
|
|
1363
|
+
)
|
|
1364
|
+
)
|
|
1365
|
+
)
|
|
1366
|
+
AND (
|
|
1367
|
+
$32::boolean = false
|
|
1368
|
+
OR (
|
|
1369
|
+
to_regclass($33) IS NOT NULL
|
|
1261
1370
|
AND (
|
|
1262
1371
|
SELECT count(DISTINCT column_name)
|
|
1263
1372
|
FROM information_schema.columns
|
|
@@ -1291,7 +1400,7 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1291
1400
|
ON index_namespace.oid = index_class.relnamespace
|
|
1292
1401
|
WHERE index_namespace.nspname = $1
|
|
1293
1402
|
AND index_class.relname = 'runtime_scheduler_cron_schedules_due_idx'
|
|
1294
|
-
AND index_row.indrelid = to_regclass($
|
|
1403
|
+
AND index_row.indrelid = to_regclass($33)
|
|
1295
1404
|
AND index_row.indisready
|
|
1296
1405
|
AND index_row.indisvalid
|
|
1297
1406
|
)
|
|
@@ -1315,6 +1424,7 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1315
1424
|
AND constraint_row.contype = 'p'
|
|
1316
1425
|
) = 'operation_key'
|
|
1317
1426
|
AND (
|
|
1427
|
+
$26::boolean = false OR (
|
|
1318
1428
|
SELECT count(DISTINCT column_name)
|
|
1319
1429
|
FROM information_schema.columns
|
|
1320
1430
|
WHERE table_schema = $1
|
|
@@ -1323,9 +1433,35 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1323
1433
|
'operation_request_digest',
|
|
1324
1434
|
'legacy_request_fingerprint',
|
|
1325
1435
|
'canonical_outcome_version',
|
|
1326
|
-
'canonical_outcome_json'
|
|
1436
|
+
'canonical_outcome_json',
|
|
1437
|
+
'canonical_outcome_ref',
|
|
1438
|
+
'canonical_outcome_pending_ref'
|
|
1327
1439
|
)
|
|
1328
|
-
|
|
1440
|
+
) = 6
|
|
1441
|
+
)
|
|
1442
|
+
AND (
|
|
1443
|
+
$26::boolean = false OR (
|
|
1444
|
+
to_regclass($25) IS NOT NULL
|
|
1445
|
+
AND (
|
|
1446
|
+
SELECT count(DISTINCT column_name)
|
|
1447
|
+
FROM information_schema.columns
|
|
1448
|
+
WHERE table_schema = $1
|
|
1449
|
+
AND table_name = 'runtime_integration_operation_value_retirements'
|
|
1450
|
+
AND column_name IN (
|
|
1451
|
+
'org_id',
|
|
1452
|
+
'object_key',
|
|
1453
|
+
'object_digest',
|
|
1454
|
+
'operation_key',
|
|
1455
|
+
'reference_json',
|
|
1456
|
+
'state',
|
|
1457
|
+
'claim_token',
|
|
1458
|
+
'created_at',
|
|
1459
|
+
'updated_at',
|
|
1460
|
+
'deletion_confirmed_at'
|
|
1461
|
+
)
|
|
1462
|
+
) = 10
|
|
1463
|
+
)
|
|
1464
|
+
)
|
|
1329
1465
|
AND (
|
|
1330
1466
|
SELECT count(DISTINCT column_name)
|
|
1331
1467
|
FROM information_schema.columns
|
|
@@ -1538,10 +1674,19 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1538
1674
|
`${quotedSchema}.${quoteIdent('runtime_gateway_reader_fences')}`,
|
|
1539
1675
|
`${quotedSchema}.${quoteIdent('runtime_sandbox_operations')}`,
|
|
1540
1676
|
`${quotedSchema}.${quoteIdent('outbox_repair_operations')}`,
|
|
1677
|
+
`${quotedSchema}.${quoteIdent('rate_reservations')}`,
|
|
1678
|
+
requirements.rateReservations !== false,
|
|
1679
|
+
`${quotedSchema}.${quoteIdent('runtime_integration_operation_value_retirements')}`,
|
|
1680
|
+
requirements.integrationOperationValueReservation !== false,
|
|
1681
|
+
requirements.gradient2ConcurrencyCheckpoint !== false,
|
|
1682
|
+
requirements.gradient2ObservationDeduplication !== false,
|
|
1683
|
+
`${quotedSchema}.${quoteIdent('rate_concurrency_observers')}`,
|
|
1541
1684
|
requirements.outboxWakeTrigger !== false,
|
|
1542
1685
|
requirements.admissionPriorityIndex !== false,
|
|
1543
1686
|
requirements.cronScheduleSchema !== false,
|
|
1544
1687
|
`${quotedSchema}.${quoteIdent(POSTGRES_SCHEDULER_CRON_SCHEDULES_TABLE)}`,
|
|
1688
|
+
requirements.rateRequesterFairShare !== false,
|
|
1689
|
+
`${quotedSchema}.${quoteIdent('rate_requesters')}`,
|
|
1545
1690
|
],
|
|
1546
1691
|
);
|
|
1547
1692
|
if (currentShape.rows[0]?.ready !== true) return false;
|
|
@@ -1697,6 +1842,89 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1697
1842
|
return runtimeAdminIndexes.rows[0]?.ready === true;
|
|
1698
1843
|
}
|
|
1699
1844
|
|
|
1845
|
+
/**
|
|
1846
|
+
* Expand the shared scheduler schema without retaining locks across tables.
|
|
1847
|
+
*
|
|
1848
|
+
* The deployment caller gives every statement its own short transaction with
|
|
1849
|
+
* transaction-local lock and statement timeouts. Never wrap the whole helper
|
|
1850
|
+
* in one transaction: old workers acquire the same live-table locks in a
|
|
1851
|
+
* different order and can deadlock the release. The helper does not write the
|
|
1852
|
+
* schema marker; callers record it only after the complete expansion and all
|
|
1853
|
+
* concurrent indexes are verified.
|
|
1854
|
+
*/
|
|
1855
|
+
export async function ensurePostgresSchedulerOnlineAdditiveExpansion(
|
|
1856
|
+
client: PostgresSchedulerQueryClient,
|
|
1857
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
1858
|
+
): Promise<void> {
|
|
1859
|
+
const schema = schemaName(options);
|
|
1860
|
+
await client.query(`
|
|
1861
|
+
ALTER TABLE ${tableName('receipts', options)}
|
|
1862
|
+
ADD COLUMN IF NOT EXISTS output_ref jsonb
|
|
1863
|
+
`);
|
|
1864
|
+
await client.query(`
|
|
1865
|
+
ALTER TABLE ${tableName('integration_invocation_receipts', options)}
|
|
1866
|
+
ADD COLUMN IF NOT EXISTS canonical_outcome_ref jsonb,
|
|
1867
|
+
ADD COLUMN IF NOT EXISTS canonical_outcome_pending_ref jsonb
|
|
1868
|
+
`);
|
|
1869
|
+
await client.query(`
|
|
1870
|
+
CREATE TABLE IF NOT EXISTS ${tableName(
|
|
1871
|
+
'runtime_integration_operation_value_retirements',
|
|
1872
|
+
options,
|
|
1873
|
+
)} (
|
|
1874
|
+
org_id text NOT NULL,
|
|
1875
|
+
object_key text NOT NULL,
|
|
1876
|
+
object_digest text NOT NULL,
|
|
1877
|
+
operation_key text NOT NULL,
|
|
1878
|
+
reference_json jsonb NOT NULL,
|
|
1879
|
+
state text NOT NULL,
|
|
1880
|
+
claim_token text,
|
|
1881
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1882
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
1883
|
+
deletion_confirmed_at timestamptz,
|
|
1884
|
+
PRIMARY KEY (org_id, object_key, object_digest),
|
|
1885
|
+
CHECK (state IN ('pending', 'deleting', 'deleted', 'cancelled')),
|
|
1886
|
+
CHECK ((state = 'deleting') = (claim_token IS NOT NULL))
|
|
1887
|
+
)
|
|
1888
|
+
`);
|
|
1889
|
+
await client.query(`
|
|
1890
|
+
CREATE INDEX IF NOT EXISTS ${quoteIdent(
|
|
1891
|
+
`${schema}_integration_operation_value_retirements_pending_idx`,
|
|
1892
|
+
)}
|
|
1893
|
+
ON ${tableName(
|
|
1894
|
+
'runtime_integration_operation_value_retirements',
|
|
1895
|
+
options,
|
|
1896
|
+
)} (state, updated_at)
|
|
1897
|
+
`);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/** Read the highest deployment-owned scheduler schema marker. */
|
|
1901
|
+
export async function readPostgresSchedulerSchemaVersion(
|
|
1902
|
+
client: PostgresSchedulerQueryClient,
|
|
1903
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
1904
|
+
): Promise<number> {
|
|
1905
|
+
const result = await client.query<{ version: number | null }>(
|
|
1906
|
+
`SELECT max(version) AS version FROM ${tableName(
|
|
1907
|
+
POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE,
|
|
1908
|
+
options,
|
|
1909
|
+
)}`,
|
|
1910
|
+
);
|
|
1911
|
+
return Number(result.rows[0]?.version ?? 0);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/** Record the deployment-owned marker after an online expansion is complete. */
|
|
1915
|
+
export async function recordPostgresSchedulerSchemaVersion(
|
|
1916
|
+
client: PostgresSchedulerQueryClient,
|
|
1917
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
1918
|
+
): Promise<void> {
|
|
1919
|
+
await client.query(
|
|
1920
|
+
`INSERT INTO ${tableName(
|
|
1921
|
+
POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE,
|
|
1922
|
+
options,
|
|
1923
|
+
)} (version) VALUES ($1) ON CONFLICT (version) DO NOTHING`,
|
|
1924
|
+
[POSTGRES_SCHEDULER_SCHEMA_VERSION],
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1700
1928
|
/**
|
|
1701
1929
|
* Deployment completion check. Migration bookkeeping and the full target
|
|
1702
1930
|
* shape belong to deployment tooling, never request or worker admission.
|
|
@@ -2622,6 +2850,13 @@ export async function ensurePostgresSchedulerSchema(
|
|
|
2622
2850
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2623
2851
|
PRIMARY KEY (org_id, k)
|
|
2624
2852
|
)
|
|
2853
|
+
`);
|
|
2854
|
+
// Readers first: older rows keep `output_json`; a referenced body is
|
|
2855
|
+
// published only after it was acknowledged by object storage. The nullable
|
|
2856
|
+
// expansion keeps mixed scheduler-schema lanes readable during rollout.
|
|
2857
|
+
await client.query(`
|
|
2858
|
+
ALTER TABLE ${tableName('receipts', options)}
|
|
2859
|
+
ADD COLUMN IF NOT EXISTS output_ref jsonb
|
|
2625
2860
|
`);
|
|
2626
2861
|
await client.query(`
|
|
2627
2862
|
CREATE TABLE IF NOT EXISTS ${tableName(
|
|
@@ -2653,11 +2888,45 @@ export async function ensurePostgresSchedulerSchema(
|
|
|
2653
2888
|
ADD COLUMN IF NOT EXISTS operation_request_digest text,
|
|
2654
2889
|
ADD COLUMN IF NOT EXISTS legacy_request_fingerprint text,
|
|
2655
2890
|
ADD COLUMN IF NOT EXISTS canonical_outcome_version smallint,
|
|
2656
|
-
ADD COLUMN IF NOT EXISTS canonical_outcome_json jsonb
|
|
2891
|
+
ADD COLUMN IF NOT EXISTS canonical_outcome_json jsonb,
|
|
2892
|
+
ADD COLUMN IF NOT EXISTS canonical_outcome_ref jsonb,
|
|
2893
|
+
ADD COLUMN IF NOT EXISTS canonical_outcome_pending_ref jsonb
|
|
2657
2894
|
`);
|
|
2658
2895
|
await client.query(`
|
|
2659
2896
|
ALTER TABLE ${tableName('integration_invocation_receipts', options)}
|
|
2660
2897
|
ADD COLUMN IF NOT EXISTS parent_run_id text NOT NULL DEFAULT ''
|
|
2898
|
+
`);
|
|
2899
|
+
// A reference stays live while it is attached to an operation row. A
|
|
2900
|
+
// later recovery detaches failed or incomplete output into this small,
|
|
2901
|
+
// fenced journal; the gateway can then delete the immutable object and
|
|
2902
|
+
// confirm absence before the journal says it is gone. This is deliberately
|
|
2903
|
+
// a journal rather than an object TTL: completed operation outcomes remain
|
|
2904
|
+
// the replay authority until the operation itself is explicitly replaced.
|
|
2905
|
+
await client.query(`
|
|
2906
|
+
CREATE TABLE IF NOT EXISTS ${tableName(
|
|
2907
|
+
'runtime_integration_operation_value_retirements',
|
|
2908
|
+
options,
|
|
2909
|
+
)} (
|
|
2910
|
+
org_id text NOT NULL,
|
|
2911
|
+
object_key text NOT NULL,
|
|
2912
|
+
object_digest text NOT NULL,
|
|
2913
|
+
operation_key text NOT NULL,
|
|
2914
|
+
reference_json jsonb NOT NULL,
|
|
2915
|
+
state text NOT NULL CHECK (state IN ('pending', 'deleting', 'deleted', 'cancelled')),
|
|
2916
|
+
claim_token text,
|
|
2917
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2918
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
2919
|
+
deletion_confirmed_at timestamptz,
|
|
2920
|
+
PRIMARY KEY (org_id, object_key, object_digest),
|
|
2921
|
+
CHECK ((state = 'deleting') = (claim_token IS NOT NULL))
|
|
2922
|
+
)
|
|
2923
|
+
`);
|
|
2924
|
+
await client.query(`
|
|
2925
|
+
CREATE INDEX IF NOT EXISTS ${quoteIdent(
|
|
2926
|
+
`${schema}_integration_operation_value_retirements_pending_idx`,
|
|
2927
|
+
)}
|
|
2928
|
+
ON ${tableName('runtime_integration_operation_value_retirements', options)}
|
|
2929
|
+
(state, updated_at)
|
|
2661
2930
|
`);
|
|
2662
2931
|
// Gateway reader capability is a shared-storage compatibility fact. It is
|
|
2663
2932
|
// intentionally separate from runtime release admission: worker lanes do
|
|
@@ -3961,6 +4230,72 @@ export async function ensureAbsurdSchema(
|
|
|
3961
4230
|
}
|
|
3962
4231
|
}
|
|
3963
4232
|
|
|
4233
|
+
/**
|
|
4234
|
+
* The runtime is allowed to use the shared Absurd schema only after the
|
|
4235
|
+
* deployment migration has installed its exact engine and Deepline
|
|
4236
|
+
* compatibility contract. This is deliberately read-only: workers and
|
|
4237
|
+
* serverless submitters must never take the global Absurd bootstrap lock or
|
|
4238
|
+
* run DDL while serving a customer request.
|
|
4239
|
+
*/
|
|
4240
|
+
export class AbsurdSchemaNotReadyError extends Error {
|
|
4241
|
+
constructor(message: string) {
|
|
4242
|
+
super(message);
|
|
4243
|
+
this.name = 'AbsurdSchemaNotReadyError';
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
export async function assertAbsurdSchemaReady(
|
|
4248
|
+
client: PostgresSchedulerQueryClient,
|
|
4249
|
+
): Promise<void> {
|
|
4250
|
+
let result: {
|
|
4251
|
+
rows: Array<{
|
|
4252
|
+
version: string | null;
|
|
4253
|
+
compatibilityVersion: string | null;
|
|
4254
|
+
}>;
|
|
4255
|
+
};
|
|
4256
|
+
try {
|
|
4257
|
+
result = await client.query<{
|
|
4258
|
+
version: string | null;
|
|
4259
|
+
compatibilityVersion: string | null;
|
|
4260
|
+
}>(`
|
|
4261
|
+
SELECT
|
|
4262
|
+
absurd.get_schema_version() AS "version",
|
|
4263
|
+
absurd.deepline_no_scheduler_org_cap_compatibility_version()
|
|
4264
|
+
AS "compatibilityVersion"
|
|
4265
|
+
`);
|
|
4266
|
+
} catch (error) {
|
|
4267
|
+
const code =
|
|
4268
|
+
typeof error === 'object' && error !== null && 'code' in error
|
|
4269
|
+
? (error as { code?: unknown }).code
|
|
4270
|
+
: undefined;
|
|
4271
|
+
if (
|
|
4272
|
+
code === '42883' ||
|
|
4273
|
+
(error instanceof Error &&
|
|
4274
|
+
/does not exist|undefined function/i.test(error.message))
|
|
4275
|
+
) {
|
|
4276
|
+
throw new AbsurdSchemaNotReadyError(
|
|
4277
|
+
'Absurd scheduler schema is absent or missing the Deepline compatibility marker; run the runtime scheduler migration before activating a lane.',
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
throw error;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
const state = result.rows[0];
|
|
4284
|
+
if (
|
|
4285
|
+
state?.version !== ABSURD_SCHEMA_VERSION ||
|
|
4286
|
+
state.compatibilityVersion !==
|
|
4287
|
+
ABSURD_NO_SCHEDULER_ORG_CAP_COMPATIBILITY_VERSION
|
|
4288
|
+
) {
|
|
4289
|
+
throw new AbsurdSchemaNotReadyError(
|
|
4290
|
+
'Absurd scheduler schema is incompatible: ' +
|
|
4291
|
+
`required_version=${ABSURD_SCHEMA_VERSION} observed_version=${state?.version ?? 'missing'} ` +
|
|
4292
|
+
`required_compatibility=${ABSURD_NO_SCHEDULER_ORG_CAP_COMPATIBILITY_VERSION} ` +
|
|
4293
|
+
`observed_compatibility=${state?.compatibilityVersion ?? 'missing'}. ` +
|
|
4294
|
+
'Run the runtime scheduler migration before activating a lane.',
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
|
|
3964
4299
|
export async function submitPostgresSchedulerRun(
|
|
3965
4300
|
client: PostgresSchedulerQueryClient,
|
|
3966
4301
|
input: PlaySchedulerSubmitInput,
|
|
@@ -7834,6 +8169,65 @@ export async function recordPostgresSchedulerManagedSandboxOperationLive(
|
|
|
7834
8169
|
return updated.rows.length === 1;
|
|
7835
8170
|
}
|
|
7836
8171
|
|
|
8172
|
+
/**
|
|
8173
|
+
* Answers one question a rejected promotion cannot answer on its own: did this
|
|
8174
|
+
* attempt lose its fence, or did it merely re-drive a promotion it already
|
|
8175
|
+
* completed?
|
|
8176
|
+
*
|
|
8177
|
+
* `recordPostgresSchedulerManagedSandboxOperationLive` is a one-shot
|
|
8178
|
+
* `reserved -> live` compare-and-set, so it returns false both when the fence
|
|
8179
|
+
* is gone and when this same fenced attempt already promoted this same sandbox.
|
|
8180
|
+
* A provider retry inside one attempt legitimately re-reports an earlier
|
|
8181
|
+
* sandbox (for example to stamp its billing end when it is retired), and
|
|
8182
|
+
* reading that rejection as a lost fence terminates a run whose attempt is
|
|
8183
|
+
* still fully owned.
|
|
8184
|
+
*
|
|
8185
|
+
* This proves the benign case from durable state rather than assuming it: it
|
|
8186
|
+
* returns true only when the attempt row still carries this lease token in a
|
|
8187
|
+
* live status AND the operation is already live for this exact sandbox. Any
|
|
8188
|
+
* other shape leaves the caller's fence loss loud and unchanged.
|
|
8189
|
+
*/
|
|
8190
|
+
export async function readPostgresSchedulerManagedSandboxOperationAlreadyLive(
|
|
8191
|
+
client: PostgresSchedulerQueryClient,
|
|
8192
|
+
input: {
|
|
8193
|
+
runId: string;
|
|
8194
|
+
attempt: number;
|
|
8195
|
+
leaseToken: string;
|
|
8196
|
+
operationKey: string;
|
|
8197
|
+
provider: RuntimeSandboxProvider;
|
|
8198
|
+
sandboxId: string;
|
|
8199
|
+
},
|
|
8200
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
8201
|
+
): Promise<boolean> {
|
|
8202
|
+
const found = await client.query<{ operation_key: string }>(
|
|
8203
|
+
`
|
|
8204
|
+
SELECT operation.operation_key
|
|
8205
|
+
FROM ${tableName('runtime_sandbox_operations', options)} AS operation
|
|
8206
|
+
JOIN ${tableName('attempts', options)} AS attempt
|
|
8207
|
+
ON attempt.run_id = operation.run_id
|
|
8208
|
+
AND attempt.attempt = operation.attempt
|
|
8209
|
+
WHERE operation.operation_key = $4
|
|
8210
|
+
AND operation.run_id = $1
|
|
8211
|
+
AND operation.attempt = $2
|
|
8212
|
+
AND operation.provider = $5
|
|
8213
|
+
AND operation.state = $6
|
|
8214
|
+
AND operation.sandbox_id = $7
|
|
8215
|
+
AND attempt.lease_token = $3
|
|
8216
|
+
AND attempt.status IN ('claimed', 'running', 'waiting')
|
|
8217
|
+
`,
|
|
8218
|
+
[
|
|
8219
|
+
input.runId,
|
|
8220
|
+
input.attempt,
|
|
8221
|
+
input.leaseToken,
|
|
8222
|
+
input.operationKey,
|
|
8223
|
+
input.provider,
|
|
8224
|
+
RuntimeSandboxOperationState.Live,
|
|
8225
|
+
input.sandboxId,
|
|
8226
|
+
],
|
|
8227
|
+
);
|
|
8228
|
+
return found.rows.length === 1;
|
|
8229
|
+
}
|
|
8230
|
+
|
|
7837
8231
|
/**
|
|
7838
8232
|
* A create call that timed out may settle only after its caller has marked the
|
|
7839
8233
|
* attempt terminal. Preserve the provider fact under that terminal attempt,
|
|
@@ -8661,6 +9055,10 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
|
|
|
8661
9055
|
attempt: number;
|
|
8662
9056
|
leaseToken: string;
|
|
8663
9057
|
result: unknown;
|
|
9058
|
+
/** Progress facts folded before direct completion. They travel with the
|
|
9059
|
+
* terminal outbox so the ledger sees progress before terminal state in one
|
|
9060
|
+
* ordered projection. */
|
|
9061
|
+
terminalProgressEvents?: readonly unknown[];
|
|
8664
9062
|
engineWake: {
|
|
8665
9063
|
queueName: string;
|
|
8666
9064
|
eventName: string;
|
|
@@ -8715,7 +9113,7 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
|
|
|
8715
9113
|
OR run.status = 'running'
|
|
8716
9114
|
)
|
|
8717
9115
|
AND (
|
|
8718
|
-
$
|
|
9116
|
+
$15::boolean
|
|
8719
9117
|
OR run.launch_json #>> '{contractSnapshot,billingLimit,maxCreditsPerRun}' IS NULL
|
|
8720
9118
|
)
|
|
8721
9119
|
FOR UPDATE
|
|
@@ -8779,7 +9177,7 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
|
|
|
8779
9177
|
WHERE target.run_id = $1
|
|
8780
9178
|
AND target.attempt = $2
|
|
8781
9179
|
AND target.status IN ('running', 'waiting')
|
|
8782
|
-
AND target.lease_token = $
|
|
9180
|
+
AND target.lease_token = $11
|
|
8783
9181
|
AND (target.runner_result_json IS NULL OR target.runner_result_json = $3::jsonb)
|
|
8784
9182
|
AND run.run_id = target.run_id
|
|
8785
9183
|
AND NOT EXISTS (
|
|
@@ -8939,21 +9337,22 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
|
|
|
8939
9337
|
'run.completed',
|
|
8940
9338
|
jsonb_build_object(
|
|
8941
9339
|
'output', $6::jsonb,
|
|
8942
|
-
'terminalLogLines', $7::jsonb
|
|
9340
|
+
'terminalLogLines', $7::jsonb,
|
|
9341
|
+
'terminalProgressEvents', $8::jsonb
|
|
8943
9342
|
),
|
|
8944
9343
|
${causalOutboxCreatedAt('run_terminal.run_id', options)}
|
|
8945
9344
|
FROM run_terminal
|
|
8946
9345
|
ON CONFLICT (event_id) DO NOTHING
|
|
8947
9346
|
),
|
|
8948
9347
|
notify_terminal AS (
|
|
8949
|
-
SELECT run_id, pg_notify($
|
|
9348
|
+
SELECT run_id, pg_notify($9, $10) FROM run_terminal
|
|
8950
9349
|
),
|
|
8951
9350
|
engine_wake AS (
|
|
8952
|
-
SELECT run_id, absurd.emit_event($
|
|
9351
|
+
SELECT run_id, absurd.emit_event($12, $13, 'null'::jsonb)
|
|
8953
9352
|
FROM notify_terminal
|
|
8954
9353
|
),
|
|
8955
9354
|
notify_engine AS (
|
|
8956
|
-
SELECT run_id, pg_notify($
|
|
9355
|
+
SELECT run_id, pg_notify($14, $12) FROM engine_wake
|
|
8957
9356
|
)
|
|
8958
9357
|
SELECT run_id FROM notify_engine
|
|
8959
9358
|
`,
|
|
@@ -8965,6 +9364,7 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
|
|
|
8965
9364
|
stringifyPostgresJson(runtimeTiming ?? null),
|
|
8966
9365
|
stringifyPostgresJson(ledgerTerminalResult ?? null),
|
|
8967
9366
|
JSON.stringify(terminalLogLines),
|
|
9367
|
+
JSON.stringify(input.terminalProgressEvents ?? []),
|
|
8968
9368
|
postgresSchedulerNotifyChannel('terminal', options),
|
|
8969
9369
|
JSON.stringify({ runId: input.runId, status: 'completed' }),
|
|
8970
9370
|
input.leaseToken,
|
|
@@ -11081,6 +11481,7 @@ export async function failPostgresSchedulerAttempt(
|
|
|
11081
11481
|
leaseToken: string;
|
|
11082
11482
|
error: string;
|
|
11083
11483
|
result?: unknown;
|
|
11484
|
+
terminalLogLines?: readonly string[] | null;
|
|
11084
11485
|
/** Refuse to overwrite a detached runner terminal that won after claim. */
|
|
11085
11486
|
preserveRunnerTerminal?: boolean;
|
|
11086
11487
|
sandboxCrashDiagnostic?: PostgresSchedulerSandboxCrashDiagnosticRequest;
|
|
@@ -11105,6 +11506,12 @@ export async function failPostgresSchedulerAttempt(
|
|
|
11105
11506
|
terminalResult === undefined ? (input.result ?? null) : terminalResult,
|
|
11106
11507
|
terminalResult === undefined ? {} : { content: 'full' },
|
|
11107
11508
|
);
|
|
11509
|
+
// Keep the recovery tail separate from result: large failures are represented
|
|
11510
|
+
// by a result reference, but their customer guidance must still be logged.
|
|
11511
|
+
const terminalLogLines = (input.terminalLogLines ?? [])
|
|
11512
|
+
.map((line) => line.trim())
|
|
11513
|
+
.filter(Boolean)
|
|
11514
|
+
.slice(-LOG_TAIL_LIMIT);
|
|
11108
11515
|
const failed = await client.query<{ run_id: string }>(
|
|
11109
11516
|
`
|
|
11110
11517
|
WITH failed AS (
|
|
@@ -11337,7 +11744,8 @@ export async function failPostgresSchedulerAttempt(
|
|
|
11337
11744
|
'run.failed',
|
|
11338
11745
|
jsonb_build_object(
|
|
11339
11746
|
'error', $4::jsonb ->> 'message',
|
|
11340
|
-
'result', $8::jsonb
|
|
11747
|
+
'result', $8::jsonb,
|
|
11748
|
+
'terminalLogLines', $12::jsonb
|
|
11341
11749
|
),
|
|
11342
11750
|
${causalOutboxCreatedAt('run_terminal.run_id', options)}
|
|
11343
11751
|
FROM run_terminal
|
|
@@ -11360,6 +11768,7 @@ export async function failPostgresSchedulerAttempt(
|
|
|
11360
11768
|
runtimeTerminalReasonFromError(input.error),
|
|
11361
11769
|
stringifyPostgresJson(input.sandboxCrashDiagnostic ?? null),
|
|
11362
11770
|
input.preserveRunnerTerminal === true,
|
|
11771
|
+
JSON.stringify(terminalLogLines),
|
|
11363
11772
|
],
|
|
11364
11773
|
);
|
|
11365
11774
|
return failed.rows.length > 0;
|
|
@@ -11424,7 +11833,21 @@ export async function cancelPostgresSchedulerRun(
|
|
|
11424
11833
|
WHERE target.run_id = $1
|
|
11425
11834
|
AND target.status IN ('queued', 'running', 'waiting')
|
|
11426
11835
|
${strandedEligibility}
|
|
11427
|
-
RETURNING target.run_id, target.launch_json
|
|
11836
|
+
RETURNING target.run_id, target.org_id, target.launch_json
|
|
11837
|
+
),
|
|
11838
|
+
released_receipts AS (
|
|
11839
|
+
-- Cancellation is a terminal, and a terminal releases everything the
|
|
11840
|
+
-- run claimed. A receipt left 'running' behind a dead lease matches no
|
|
11841
|
+
-- non-forced claim status set, so a successor would park on it for the
|
|
11842
|
+
-- whole receipt wait budget instead of executing the call itself.
|
|
11843
|
+
UPDATE ${tableName('receipts', options)} AS receipt
|
|
11844
|
+
SET ${RELEASE_RECEIPT_TO_PENDING_SET_CLAUSE}
|
|
11845
|
+
FROM cancelled_run
|
|
11846
|
+
WHERE receipt.org_id = cancelled_run.org_id
|
|
11847
|
+
AND receipt.status IN ('queued', 'running')
|
|
11848
|
+
AND COALESCE(receipt.lease_owner_run_id, receipt.run_id)
|
|
11849
|
+
= cancelled_run.run_id
|
|
11850
|
+
RETURNING receipt.k
|
|
11428
11851
|
),
|
|
11429
11852
|
cancelled_attempts AS (
|
|
11430
11853
|
UPDATE ${tableName('attempts', options)}
|
|
@@ -11878,6 +12301,33 @@ function extractRunnerRuntimeTiming(
|
|
|
11878
12301
|
|
|
11879
12302
|
const CTX_LOG_TIMESTAMP_PREFIX_RE = /^\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\]\s+/;
|
|
11880
12303
|
const RUN_LOG_TAIL_METADATA_MAX_BYTES = 128 * 1024;
|
|
12304
|
+
// The runner's terminal resource snapshot is a small aggregate (phase timings,
|
|
12305
|
+
// process CPU/RSS, per-map admission state). It rides in result metadata so
|
|
12306
|
+
// terminal diagnostics never depend on a bounded log tail.
|
|
12307
|
+
const RUNTIME_RESOURCES_METADATA_MAX_BYTES = 32 * 1024;
|
|
12308
|
+
|
|
12309
|
+
function boundedRuntimeResourcesMetadata(
|
|
12310
|
+
runnerResult: unknown,
|
|
12311
|
+
): Record<string, unknown> | null {
|
|
12312
|
+
const stats = (runnerResult as { stats?: unknown }).stats;
|
|
12313
|
+
if (!stats || typeof stats !== 'object' || Array.isArray(stats)) return null;
|
|
12314
|
+
const runtimeResources = (stats as { runtimeResources?: unknown })
|
|
12315
|
+
.runtimeResources;
|
|
12316
|
+
if (
|
|
12317
|
+
!runtimeResources ||
|
|
12318
|
+
typeof runtimeResources !== 'object' ||
|
|
12319
|
+
Array.isArray(runtimeResources)
|
|
12320
|
+
) {
|
|
12321
|
+
return null;
|
|
12322
|
+
}
|
|
12323
|
+
if (
|
|
12324
|
+
jsonByteLengthUpTo(runtimeResources, RUNTIME_RESOURCES_METADATA_MAX_BYTES)
|
|
12325
|
+
.exceeded
|
|
12326
|
+
) {
|
|
12327
|
+
return null;
|
|
12328
|
+
}
|
|
12329
|
+
return runtimeResources as Record<string, unknown>;
|
|
12330
|
+
}
|
|
11881
12331
|
|
|
11882
12332
|
function boundedRunLogTailMetadata(logs: string[]): {
|
|
11883
12333
|
tail: string[];
|
|
@@ -11915,11 +12365,18 @@ function terminalLogLinesForOutbox(
|
|
|
11915
12365
|
return [];
|
|
11916
12366
|
}
|
|
11917
12367
|
return logs
|
|
11918
|
-
.filter(
|
|
11919
|
-
(
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
12368
|
+
.filter((line): line is string => {
|
|
12369
|
+
if (typeof line !== 'string') return false;
|
|
12370
|
+
const normalized = readProvenanceTag(line).line.trim();
|
|
12371
|
+
// Retain bounded terminal measurement summaries alongside timestamped
|
|
12372
|
+
// customer/recovery lines after a detached runner has exited.
|
|
12373
|
+
return (
|
|
12374
|
+
CTX_LOG_TIMESTAMP_PREFIX_RE.test(normalized) ||
|
|
12375
|
+
normalized.startsWith('[runtime.phase_summary] ') ||
|
|
12376
|
+
normalized.startsWith('[runtime.logs] ') ||
|
|
12377
|
+
normalized.startsWith('[runtime.log_transport] ')
|
|
12378
|
+
);
|
|
12379
|
+
})
|
|
11923
12380
|
.map((line) => line.trim())
|
|
11924
12381
|
.slice(-LOG_TAIL_LIMIT);
|
|
11925
12382
|
}
|
|
@@ -11961,6 +12418,9 @@ function attachRunnerWorkProgressMetadata(
|
|
|
11961
12418
|
options.logMetadata === 'omit' || metadata?.runLogTail || logs.length === 0
|
|
11962
12419
|
? null
|
|
11963
12420
|
: boundedRunLogTailMetadata(logs);
|
|
12421
|
+
const runtimeResources = metadata?.runtimeResources
|
|
12422
|
+
? null
|
|
12423
|
+
: boundedRuntimeResourcesMetadata(runnerResult);
|
|
11964
12424
|
const outputWarnings = Array.isArray(
|
|
11965
12425
|
(runnerResult as { outputWarnings?: unknown }).outputWarnings,
|
|
11966
12426
|
)
|
|
@@ -12002,6 +12462,7 @@ function attachRunnerWorkProgressMetadata(
|
|
|
12002
12462
|
if (
|
|
12003
12463
|
(total === null || skipped === null || metadata?.workProgress) &&
|
|
12004
12464
|
!runLogTail &&
|
|
12465
|
+
!runtimeResources &&
|
|
12005
12466
|
mergedOutputWarnings.length === 0
|
|
12006
12467
|
) {
|
|
12007
12468
|
return output;
|
|
@@ -12034,6 +12495,7 @@ function attachRunnerWorkProgressMetadata(
|
|
|
12034
12495
|
...(metadata ?? {}),
|
|
12035
12496
|
...(workProgress ? { workProgress } : {}),
|
|
12036
12497
|
...(runLogTail ? { runLogTail } : {}),
|
|
12498
|
+
...(runtimeResources ? { runtimeResources } : {}),
|
|
12037
12499
|
...(mergedOutputWarnings.length > 0
|
|
12038
12500
|
? { outputWarnings: mergedOutputWarnings }
|
|
12039
12501
|
: {}),
|
|
@@ -12185,6 +12647,10 @@ export async function executePostgresSchedulerClaim(
|
|
|
12185
12647
|
leaseToken: claim.leaseToken,
|
|
12186
12648
|
error: message,
|
|
12187
12649
|
result: terminalFailureResult,
|
|
12650
|
+
terminalLogLines: terminalLogLinesForOutbox(
|
|
12651
|
+
terminalFailureResult,
|
|
12652
|
+
options.terminalLogOutbox,
|
|
12653
|
+
),
|
|
12188
12654
|
sandboxCrashDiagnostic: options.sandboxCrashDiagnostic,
|
|
12189
12655
|
},
|
|
12190
12656
|
options,
|
|
@@ -12495,10 +12961,21 @@ function postgresSchedulerReceiptLeaseId(input: {
|
|
|
12495
12961
|
|
|
12496
12962
|
function mapReceiptRow(row: ReceiptRow): RuntimeStepReceipt {
|
|
12497
12963
|
const leaseExpiresAt = row.lease_expires_at;
|
|
12964
|
+
const outputRef = isRuntimeReceiptValueReference(row.output_ref)
|
|
12965
|
+
? row.output_ref
|
|
12966
|
+
: null;
|
|
12498
12967
|
return {
|
|
12499
12968
|
key: row.k,
|
|
12500
12969
|
status: row.status as RuntimeStepReceipt['status'],
|
|
12501
12970
|
...(row.output_json === null ? {} : { output: row.output_json }),
|
|
12971
|
+
...(outputRef
|
|
12972
|
+
? {
|
|
12973
|
+
outputRef,
|
|
12974
|
+
...(outputRef.inlineFields
|
|
12975
|
+
? { inlineFields: outputRef.inlineFields }
|
|
12976
|
+
: {}),
|
|
12977
|
+
}
|
|
12978
|
+
: {}),
|
|
12502
12979
|
...(row.error ? { error: row.error } : {}),
|
|
12503
12980
|
...(row.error_payload &&
|
|
12504
12981
|
typeof row.error_payload === 'object' &&
|
|
@@ -12529,7 +13006,7 @@ export async function getPostgresSchedulerReceipt(
|
|
|
12529
13006
|
): Promise<RuntimeStepReceipt | null> {
|
|
12530
13007
|
const result = await client.query<ReceiptRow>(
|
|
12531
13008
|
`
|
|
12532
|
-
SELECT k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13009
|
+
SELECT k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
12533
13010
|
FROM ${tableName('receipts', options)}
|
|
12534
13011
|
WHERE org_id = $1
|
|
12535
13012
|
AND k = $2
|
|
@@ -12555,6 +13032,7 @@ export async function getPostgresSchedulerReceipts(
|
|
|
12555
13032
|
SELECT receipt.k,
|
|
12556
13033
|
receipt.status,
|
|
12557
13034
|
receipt.output_json,
|
|
13035
|
+
receipt.output_ref,
|
|
12558
13036
|
receipt.error,
|
|
12559
13037
|
receipt.error_payload,
|
|
12560
13038
|
receipt.failure_kind,
|
|
@@ -12778,7 +13256,7 @@ export async function claimPostgresSchedulerReceipt(
|
|
|
12778
13256
|
-- Completed/skipped receipts are immutable cache hits unless a caller
|
|
12779
13257
|
-- explicitly forces refresh. Read them before the UPSERT so popular
|
|
12780
13258
|
-- prebuilt keys do not take a conflicting-row lock or create WAL.
|
|
12781
|
-
SELECT k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13259
|
+
SELECT k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
12782
13260
|
FROM ${tableName('receipts', options)}
|
|
12783
13261
|
WHERE org_id = $9
|
|
12784
13262
|
AND k = $1
|
|
@@ -12822,6 +13300,7 @@ export async function claimPostgresSchedulerReceipt(
|
|
|
12822
13300
|
ELSE now() + ($6::double precision * interval '1 millisecond')
|
|
12823
13301
|
END,
|
|
12824
13302
|
output_json = NULL,
|
|
13303
|
+
output_ref = NULL,
|
|
12825
13304
|
error = NULL,
|
|
12826
13305
|
error_payload = NULL,
|
|
12827
13306
|
failure_kind = 0,
|
|
@@ -12855,13 +13334,13 @@ export async function claimPostgresSchedulerReceipt(
|
|
|
12855
13334
|
AND COALESCE(${tableName('receipts', options)}.lease_owner_attempt, 0) < $5::integer
|
|
12856
13335
|
)
|
|
12857
13336
|
)
|
|
12858
|
-
RETURNING k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13337
|
+
RETURNING k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
12859
13338
|
)
|
|
12860
|
-
SELECT k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at,
|
|
13339
|
+
SELECT k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at,
|
|
12861
13340
|
'existing'::text AS claim_state
|
|
12862
13341
|
FROM terminal
|
|
12863
13342
|
UNION ALL
|
|
12864
|
-
SELECT k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at,
|
|
13343
|
+
SELECT k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at,
|
|
12865
13344
|
'claimed'::text AS claim_state
|
|
12866
13345
|
FROM claimed
|
|
12867
13346
|
`,
|
|
@@ -12939,6 +13418,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
12939
13418
|
SELECT receipt.k,
|
|
12940
13419
|
receipt.status,
|
|
12941
13420
|
receipt.output_json,
|
|
13421
|
+
receipt.output_ref,
|
|
12942
13422
|
receipt.error,
|
|
12943
13423
|
receipt.error_payload,
|
|
12944
13424
|
receipt.failure_kind,
|
|
@@ -13004,6 +13484,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13004
13484
|
ELSE now() + ($6::double precision * interval '1 millisecond')
|
|
13005
13485
|
END,
|
|
13006
13486
|
output_json = NULL,
|
|
13487
|
+
output_ref = NULL,
|
|
13007
13488
|
error = NULL,
|
|
13008
13489
|
error_payload = NULL,
|
|
13009
13490
|
failure_kind = 0,
|
|
@@ -13022,12 +13503,13 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13022
13503
|
AND ${tableName('receipts', options)}.lease_expires_at IS NULL
|
|
13023
13504
|
OR ${tableName('receipts', options)}.lease_expires_at <= now()
|
|
13024
13505
|
)
|
|
13025
|
-
RETURNING k, status, output_json, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13506
|
+
RETURNING k, status, output_json, output_ref, error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13026
13507
|
),
|
|
13027
13508
|
latest AS (
|
|
13028
13509
|
SELECT receipt.k,
|
|
13029
13510
|
receipt.status,
|
|
13030
13511
|
receipt.output_json,
|
|
13512
|
+
receipt.output_ref,
|
|
13031
13513
|
receipt.error,
|
|
13032
13514
|
receipt.error_payload,
|
|
13033
13515
|
receipt.failure_kind,
|
|
@@ -13064,6 +13546,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13064
13546
|
SELECT terminal.k,
|
|
13065
13547
|
terminal.status,
|
|
13066
13548
|
terminal.output_json,
|
|
13549
|
+
terminal.output_ref,
|
|
13067
13550
|
terminal.error,
|
|
13068
13551
|
terminal.error_payload,
|
|
13069
13552
|
terminal.failure_kind,
|
|
@@ -13080,6 +13563,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13080
13563
|
SELECT claimed.k,
|
|
13081
13564
|
claimed.status,
|
|
13082
13565
|
claimed.output_json,
|
|
13566
|
+
claimed.output_ref,
|
|
13083
13567
|
claimed.error,
|
|
13084
13568
|
claimed.error_payload,
|
|
13085
13569
|
claimed.failure_kind,
|
|
@@ -13097,6 +13581,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13097
13581
|
SELECT latest.k,
|
|
13098
13582
|
latest.status,
|
|
13099
13583
|
latest.output_json,
|
|
13584
|
+
latest.output_ref,
|
|
13100
13585
|
latest.error,
|
|
13101
13586
|
latest.error_payload,
|
|
13102
13587
|
latest.failure_kind,
|
|
@@ -13116,6 +13601,7 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13116
13601
|
SELECT unioned.k,
|
|
13117
13602
|
unioned.status,
|
|
13118
13603
|
unioned.output_json,
|
|
13604
|
+
unioned.output_ref,
|
|
13119
13605
|
unioned.error,
|
|
13120
13606
|
unioned.error_payload,
|
|
13121
13607
|
unioned.failure_kind,
|
|
@@ -13156,22 +13642,88 @@ export async function claimPostgresSchedulerReceipts(
|
|
|
13156
13642
|
);
|
|
13157
13643
|
}
|
|
13158
13644
|
|
|
13645
|
+
/**
|
|
13646
|
+
* A storage completion whose value is raw or already canonicalized by the
|
|
13647
|
+
* gateway. Transport callers retain the public `output` requirement; the
|
|
13648
|
+
* gateway intentionally supplies only `outputJson` or `outputRef` after it has
|
|
13649
|
+
* released that duplicate structured graph.
|
|
13650
|
+
*/
|
|
13651
|
+
type RuntimeStepReceiptCompletionValue = {
|
|
13652
|
+
output?: unknown;
|
|
13653
|
+
outputJson?: string | null;
|
|
13654
|
+
outputRef?: RuntimeReceiptValueReference | null;
|
|
13655
|
+
};
|
|
13656
|
+
|
|
13657
|
+
type RuntimeStepReceiptCompletionInput = Omit<
|
|
13658
|
+
CompleteRuntimeStepReceiptInput,
|
|
13659
|
+
'output'
|
|
13660
|
+
> &
|
|
13661
|
+
RuntimeStepReceiptCompletionValue;
|
|
13662
|
+
|
|
13663
|
+
type RuntimeStepReceiptCompletionBatchInput = Omit<
|
|
13664
|
+
CompleteRuntimeStepReceiptsInput['receipts'][number],
|
|
13665
|
+
'output'
|
|
13666
|
+
> &
|
|
13667
|
+
RuntimeStepReceiptCompletionValue;
|
|
13668
|
+
|
|
13669
|
+
/**
|
|
13670
|
+
* Canonical `jsonb` text for one receipt completion.
|
|
13671
|
+
*
|
|
13672
|
+
* The receipt gateway already redacted and serialized the output once, and
|
|
13673
|
+
* hands that exact text through as `outputJson`, so the same value is not
|
|
13674
|
+
* walked a second time here. Callers that pass only a value get the identical
|
|
13675
|
+
* bytes from `stringifyPostgresJson`.
|
|
13676
|
+
*/
|
|
13677
|
+
function receiptOutputJson(
|
|
13678
|
+
input: RuntimeStepReceiptCompletionValue,
|
|
13679
|
+
): string | null {
|
|
13680
|
+
if (input.outputRef) {
|
|
13681
|
+
if (input.outputJson !== null && input.outputJson !== undefined) {
|
|
13682
|
+
throw new Error(
|
|
13683
|
+
'Receipt completion cannot contain inline output and outputRef.',
|
|
13684
|
+
);
|
|
13685
|
+
}
|
|
13686
|
+
return null;
|
|
13687
|
+
}
|
|
13688
|
+
return (
|
|
13689
|
+
input.outputJson ??
|
|
13690
|
+
stringifyRequiredReceiptJson(input.output ?? null, 'Receipt output')
|
|
13691
|
+
);
|
|
13692
|
+
}
|
|
13693
|
+
|
|
13694
|
+
function receiptOutputRefJson(
|
|
13695
|
+
input: RuntimeStepReceiptCompletionValue,
|
|
13696
|
+
): string | null {
|
|
13697
|
+
return input.outputRef
|
|
13698
|
+
? stringifyRequiredReceiptJson(input.outputRef, 'Receipt output reference')
|
|
13699
|
+
: null;
|
|
13700
|
+
}
|
|
13701
|
+
|
|
13702
|
+
function stringifyRequiredReceiptJson(value: unknown, field: string): string {
|
|
13703
|
+
const serialized = stringifyPostgresJson(value);
|
|
13704
|
+
if (serialized === undefined) {
|
|
13705
|
+
throw new TypeError(`${field} must be JSON-serializable.`);
|
|
13706
|
+
}
|
|
13707
|
+
return serialized;
|
|
13708
|
+
}
|
|
13709
|
+
|
|
13159
13710
|
export async function completePostgresSchedulerReceipt(
|
|
13160
13711
|
client: PostgresSchedulerQueryClient,
|
|
13161
|
-
input:
|
|
13712
|
+
input: RuntimeStepReceiptCompletionInput & { returnOutput?: boolean },
|
|
13162
13713
|
options: PostgresSchedulerReceiptOptions,
|
|
13163
13714
|
): Promise<RuntimeStepReceipt | null> {
|
|
13164
13715
|
if (input.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID) {
|
|
13165
13716
|
const inserted = await client.query<ReceiptRow>(
|
|
13166
13717
|
`
|
|
13167
13718
|
INSERT INTO ${tableName('receipts', options)} AS receipt (
|
|
13168
|
-
org_id, k, status, output_json, error, error_payload, failure_kind, run_id,
|
|
13719
|
+
org_id, k, status, output_json, output_ref, error, error_payload, failure_kind, run_id,
|
|
13169
13720
|
lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13170
|
-
) VALUES ($1, $2, 'completed', $3::jsonb, NULL, NULL, 0, $
|
|
13721
|
+
) VALUES ($1, $2, 'completed', $3::jsonb, $4::jsonb, NULL, NULL, 0, $5,
|
|
13171
13722
|
NULL, NULL, NULL, NULL)
|
|
13172
13723
|
ON CONFLICT (org_id, k) DO UPDATE
|
|
13173
13724
|
SET status = EXCLUDED.status,
|
|
13174
13725
|
output_json = EXCLUDED.output_json,
|
|
13726
|
+
output_ref = EXCLUDED.output_ref,
|
|
13175
13727
|
error = NULL,
|
|
13176
13728
|
error_payload = NULL,
|
|
13177
13729
|
failure_kind = 0,
|
|
@@ -13183,25 +13735,33 @@ export async function completePostgresSchedulerReceipt(
|
|
|
13183
13735
|
updated_at = now()
|
|
13184
13736
|
WHERE receipt.status NOT IN ('completed', 'skipped')
|
|
13185
13737
|
RETURNING k, status,
|
|
13186
|
-
CASE WHEN $
|
|
13738
|
+
CASE WHEN $6::boolean THEN output_json ELSE NULL::jsonb END AS output_json,
|
|
13739
|
+
output_ref,
|
|
13187
13740
|
error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
13188
13741
|
lease_owner_attempt, lease_expires_at
|
|
13189
13742
|
`,
|
|
13190
13743
|
[
|
|
13191
13744
|
receiptOrgId(options),
|
|
13192
13745
|
input.key,
|
|
13193
|
-
|
|
13746
|
+
receiptOutputJson(input),
|
|
13747
|
+
receiptOutputRefJson(input),
|
|
13194
13748
|
input.runId,
|
|
13195
13749
|
input.returnOutput !== false,
|
|
13196
13750
|
],
|
|
13197
13751
|
);
|
|
13198
|
-
if (inserted.rows[0])
|
|
13752
|
+
if (inserted.rows[0]) {
|
|
13753
|
+
return {
|
|
13754
|
+
...mapReceiptRow(inserted.rows[0]),
|
|
13755
|
+
completionDisposition: 'persisted',
|
|
13756
|
+
};
|
|
13757
|
+
}
|
|
13199
13758
|
// A separate statement gets a fresh READ COMMITTED snapshot after a
|
|
13200
13759
|
// concurrent insert wins and commits.
|
|
13201
13760
|
const winner = await client.query<ReceiptRow>(
|
|
13202
13761
|
`
|
|
13203
13762
|
SELECT k, status,
|
|
13204
13763
|
CASE WHEN $3::boolean THEN output_json ELSE NULL::jsonb END AS output_json,
|
|
13764
|
+
output_ref,
|
|
13205
13765
|
error, failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
13206
13766
|
lease_owner_attempt, lease_expires_at
|
|
13207
13767
|
FROM ${tableName('receipts', options)}
|
|
@@ -13212,7 +13772,12 @@ export async function completePostgresSchedulerReceipt(
|
|
|
13212
13772
|
`,
|
|
13213
13773
|
[receiptOrgId(options), input.key, input.returnOutput !== false],
|
|
13214
13774
|
);
|
|
13215
|
-
return winner.rows[0]
|
|
13775
|
+
return winner.rows[0]
|
|
13776
|
+
? {
|
|
13777
|
+
...mapReceiptRow(winner.rows[0]),
|
|
13778
|
+
completionDisposition: 'existing',
|
|
13779
|
+
}
|
|
13780
|
+
: null;
|
|
13216
13781
|
}
|
|
13217
13782
|
const runAttempt = normalizePostgresSchedulerReceiptRunAttempt(
|
|
13218
13783
|
input.runAttempt,
|
|
@@ -13223,6 +13788,7 @@ export async function completePostgresSchedulerReceipt(
|
|
|
13223
13788
|
UPDATE ${tableName('receipts', options)}
|
|
13224
13789
|
SET status = 'completed',
|
|
13225
13790
|
output_json = $3::jsonb,
|
|
13791
|
+
output_ref = $7::jsonb,
|
|
13226
13792
|
error = NULL,
|
|
13227
13793
|
error_payload = NULL,
|
|
13228
13794
|
failure_kind = 0,
|
|
@@ -13243,51 +13809,226 @@ export async function completePostgresSchedulerReceipt(
|
|
|
13243
13809
|
)
|
|
13244
13810
|
)
|
|
13245
13811
|
RETURNING k, status,
|
|
13246
|
-
CASE WHEN $
|
|
13812
|
+
CASE WHEN $8::boolean THEN output_json ELSE NULL::jsonb END AS output_json,
|
|
13813
|
+
output_ref,
|
|
13247
13814
|
error, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13248
13815
|
`,
|
|
13249
13816
|
[
|
|
13250
13817
|
input.key,
|
|
13251
13818
|
input.runId,
|
|
13252
|
-
|
|
13819
|
+
receiptOutputJson(input),
|
|
13253
13820
|
runAttempt,
|
|
13254
13821
|
leaseId,
|
|
13255
13822
|
receiptOrgId(options),
|
|
13823
|
+
receiptOutputRefJson(input),
|
|
13256
13824
|
input.returnOutput !== false,
|
|
13257
13825
|
],
|
|
13258
13826
|
);
|
|
13259
|
-
return result.rows[0]
|
|
13827
|
+
return result.rows[0]
|
|
13828
|
+
? {
|
|
13829
|
+
...mapReceiptRow(result.rows[0]),
|
|
13830
|
+
completionDisposition: 'persisted',
|
|
13831
|
+
}
|
|
13832
|
+
: null;
|
|
13833
|
+
}
|
|
13834
|
+
|
|
13835
|
+
/**
|
|
13836
|
+
* Publish a batch of immutable completed receipts (receipt-cache inserts) in
|
|
13837
|
+
* one INSERT ... ON CONFLICT statement, with a second statement only for the
|
|
13838
|
+
* keys a concurrent publisher already won. Semantics match the scalar
|
|
13839
|
+
* `completePostgresSchedulerReceipt` cache-insert path receipt by receipt: a
|
|
13840
|
+
* key this batch persists reports `persisted`; a key already completed or
|
|
13841
|
+
* skipped by someone else (including an earlier duplicate of the same key in
|
|
13842
|
+
* this batch) reports the winner's row as `existing`; a key that is neither
|
|
13843
|
+
* reports null.
|
|
13844
|
+
*/
|
|
13845
|
+
async function completePostgresSchedulerCacheInsertReceipts(
|
|
13846
|
+
client: PostgresSchedulerQueryClient,
|
|
13847
|
+
input: {
|
|
13848
|
+
receipts: ReadonlyArray<RuntimeStepReceiptCompletionBatchInput>;
|
|
13849
|
+
returnOutput?: boolean;
|
|
13850
|
+
},
|
|
13851
|
+
options: PostgresSchedulerReceiptOptions,
|
|
13852
|
+
): Promise<Array<RuntimeStepReceipt | null>> {
|
|
13853
|
+
if (input.receipts.length === 0) return [];
|
|
13854
|
+
const returnOutput = input.returnOutput !== false;
|
|
13855
|
+
const orgId = receiptOrgId(options);
|
|
13856
|
+
// ON CONFLICT DO UPDATE rejects a statement that touches the same row
|
|
13857
|
+
// twice, so a key repeated inside one batch is inserted once; its later
|
|
13858
|
+
// duplicates resolve through the winner read exactly as the scalar path
|
|
13859
|
+
// resolved a second publisher.
|
|
13860
|
+
const firstOccurrence = new Map<string, number>();
|
|
13861
|
+
input.receipts.forEach((receipt, index) => {
|
|
13862
|
+
if (!firstOccurrence.has(receipt.key))
|
|
13863
|
+
firstOccurrence.set(receipt.key, index);
|
|
13864
|
+
});
|
|
13865
|
+
const insertIndexes = [...firstOccurrence.values()];
|
|
13866
|
+
const inserted = await client.query<ReceiptRow>(
|
|
13867
|
+
`
|
|
13868
|
+
INSERT INTO ${tableName('receipts', options)} AS receipt (
|
|
13869
|
+
org_id, k, status, output_json, output_ref, error, error_payload, failure_kind, run_id,
|
|
13870
|
+
lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at
|
|
13871
|
+
)
|
|
13872
|
+
SELECT $1, key_values.k, 'completed', output_values.output_json::jsonb,
|
|
13873
|
+
ref_values.output_ref::jsonb, NULL, NULL, 0,
|
|
13874
|
+
run_values.run_id, NULL, NULL, NULL, NULL
|
|
13875
|
+
FROM unnest($2::text[]) WITH ORDINALITY AS key_values(k, ord)
|
|
13876
|
+
JOIN unnest($3::text[]) WITH ORDINALITY AS output_values(output_json, ord)
|
|
13877
|
+
ON output_values.ord = key_values.ord
|
|
13878
|
+
JOIN unnest($4::text[]) WITH ORDINALITY AS ref_values(output_ref, ord)
|
|
13879
|
+
ON ref_values.ord = key_values.ord
|
|
13880
|
+
JOIN unnest($5::text[]) WITH ORDINALITY AS run_values(run_id, ord)
|
|
13881
|
+
ON run_values.ord = key_values.ord
|
|
13882
|
+
ON CONFLICT (org_id, k) DO UPDATE
|
|
13883
|
+
SET status = EXCLUDED.status,
|
|
13884
|
+
output_json = EXCLUDED.output_json,
|
|
13885
|
+
output_ref = EXCLUDED.output_ref,
|
|
13886
|
+
error = NULL,
|
|
13887
|
+
error_payload = NULL,
|
|
13888
|
+
failure_kind = 0,
|
|
13889
|
+
run_id = EXCLUDED.run_id,
|
|
13890
|
+
lease_id = NULL,
|
|
13891
|
+
lease_owner_run_id = NULL,
|
|
13892
|
+
lease_owner_attempt = NULL,
|
|
13893
|
+
lease_expires_at = NULL,
|
|
13894
|
+
updated_at = now()
|
|
13895
|
+
WHERE receipt.status NOT IN ('completed', 'skipped')
|
|
13896
|
+
RETURNING k, status,
|
|
13897
|
+
CASE WHEN $6::boolean THEN output_json ELSE NULL::jsonb END AS output_json,
|
|
13898
|
+
output_ref,
|
|
13899
|
+
error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
13900
|
+
lease_owner_attempt, lease_expires_at
|
|
13901
|
+
`,
|
|
13902
|
+
[
|
|
13903
|
+
orgId,
|
|
13904
|
+
insertIndexes.map((index) => input.receipts[index]!.key),
|
|
13905
|
+
insertIndexes.map((index) => receiptOutputJson(input.receipts[index]!)),
|
|
13906
|
+
insertIndexes.map((index) =>
|
|
13907
|
+
receiptOutputRefJson(input.receipts[index]!),
|
|
13908
|
+
),
|
|
13909
|
+
insertIndexes.map((index) => input.receipts[index]!.runId),
|
|
13910
|
+
returnOutput,
|
|
13911
|
+
],
|
|
13912
|
+
);
|
|
13913
|
+
const persistedByKey = new Map(inserted.rows.map((row) => [row.k, row]));
|
|
13914
|
+
const results: Array<RuntimeStepReceipt | null> = new Array(
|
|
13915
|
+
input.receipts.length,
|
|
13916
|
+
).fill(null);
|
|
13917
|
+
const unresolvedKeys = new Set<string>();
|
|
13918
|
+
input.receipts.forEach((receipt, index) => {
|
|
13919
|
+
const row = persistedByKey.get(receipt.key);
|
|
13920
|
+
if (row && firstOccurrence.get(receipt.key) === index) {
|
|
13921
|
+
results[index] = {
|
|
13922
|
+
...mapReceiptRow(row),
|
|
13923
|
+
completionDisposition: 'persisted',
|
|
13924
|
+
};
|
|
13925
|
+
} else {
|
|
13926
|
+
unresolvedKeys.add(receipt.key);
|
|
13927
|
+
}
|
|
13928
|
+
});
|
|
13929
|
+
if (unresolvedKeys.size === 0) return results;
|
|
13930
|
+
// A separate statement gets a fresh READ COMMITTED snapshot after a
|
|
13931
|
+
// concurrent publisher wins and commits.
|
|
13932
|
+
const winners = await client.query<ReceiptRow>(
|
|
13933
|
+
`
|
|
13934
|
+
SELECT k, status,
|
|
13935
|
+
CASE WHEN $3::boolean THEN output_json ELSE NULL::jsonb END AS output_json,
|
|
13936
|
+
output_ref,
|
|
13937
|
+
error, error_payload, failure_kind, run_id, lease_id, lease_owner_run_id,
|
|
13938
|
+
lease_owner_attempt, lease_expires_at
|
|
13939
|
+
FROM ${tableName('receipts', options)}
|
|
13940
|
+
WHERE org_id = $1
|
|
13941
|
+
AND k = ANY($2::text[])
|
|
13942
|
+
AND status IN ('completed', 'skipped')
|
|
13943
|
+
`,
|
|
13944
|
+
[orgId, [...unresolvedKeys], returnOutput],
|
|
13945
|
+
);
|
|
13946
|
+
const winnerByKey = new Map(winners.rows.map((row) => [row.k, row]));
|
|
13947
|
+
let alreadyWon = 0;
|
|
13948
|
+
let duplicatedInBatch = 0;
|
|
13949
|
+
let unresolved = 0;
|
|
13950
|
+
input.receipts.forEach((receipt, index) => {
|
|
13951
|
+
if (results[index]) return;
|
|
13952
|
+
const row = winnerByKey.get(receipt.key);
|
|
13953
|
+
results[index] = row
|
|
13954
|
+
? { ...mapReceiptRow(row), completionDisposition: 'existing' }
|
|
13955
|
+
: null;
|
|
13956
|
+
if (!row) unresolved += 1;
|
|
13957
|
+
else if (firstOccurrence.get(receipt.key) === index) alreadyWon += 1;
|
|
13958
|
+
else duplicatedInBatch += 1;
|
|
13959
|
+
});
|
|
13960
|
+
// A losing publisher is the intended path, but every one costs the client
|
|
13961
|
+
// a full-output read-back. Count them per batch so a read-back rate above
|
|
13962
|
+
// the duplicate-call rate is visible in the gateway log without tracing.
|
|
13963
|
+
console.info('[postgres-scheduler.receipt_publication_batch]', {
|
|
13964
|
+
receipts: input.receipts.length,
|
|
13965
|
+
persisted:
|
|
13966
|
+
input.receipts.length - alreadyWon - duplicatedInBatch - unresolved,
|
|
13967
|
+
alreadyWon,
|
|
13968
|
+
duplicatedInBatch,
|
|
13969
|
+
unresolved,
|
|
13970
|
+
});
|
|
13971
|
+
return results;
|
|
13260
13972
|
}
|
|
13261
13973
|
|
|
13262
13974
|
export async function completePostgresSchedulerReceipts(
|
|
13263
13975
|
client: PostgresSchedulerQueryClient,
|
|
13264
|
-
input:
|
|
13976
|
+
input: {
|
|
13977
|
+
receipts: ReadonlyArray<RuntimeStepReceiptCompletionBatchInput>;
|
|
13978
|
+
returnOutput?: boolean;
|
|
13979
|
+
},
|
|
13265
13980
|
options: PostgresSchedulerReceiptOptions,
|
|
13266
13981
|
): Promise<Array<RuntimeStepReceipt | null>> {
|
|
13267
13982
|
if (input.receipts.length === 0) return [];
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13983
|
+
const cacheInsertIndexes: number[] = [];
|
|
13984
|
+
const leasedIndexes: number[] = [];
|
|
13985
|
+
input.receipts.forEach((receipt, index) => {
|
|
13986
|
+
(receipt.leaseId === COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID
|
|
13987
|
+
? cacheInsertIndexes
|
|
13988
|
+
: leasedIndexes
|
|
13989
|
+
).push(index);
|
|
13990
|
+
});
|
|
13991
|
+
if (cacheInsertIndexes.length > 0) {
|
|
13273
13992
|
// Immutable cache publication is a different operation from leased Work
|
|
13274
|
-
// Receipt completion
|
|
13275
|
-
//
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13993
|
+
// Receipt completion, so a mixed batch runs as two set statements, never
|
|
13994
|
+
// as one receipt per query. A runner batch is whatever settled during one
|
|
13995
|
+
// gateway round trip, so under load it mixes both kinds every time; the
|
|
13996
|
+
// per-receipt fallback this replaces cost 229 serialized queries on one
|
|
13997
|
+
// connection for a single 1k-fixture completion request.
|
|
13998
|
+
const results: Array<RuntimeStepReceipt | null> = new Array(
|
|
13999
|
+
input.receipts.length,
|
|
14000
|
+
).fill(null);
|
|
14001
|
+
const published = await completePostgresSchedulerCacheInsertReceipts(
|
|
14002
|
+
client,
|
|
14003
|
+
{
|
|
14004
|
+
receipts: cacheInsertIndexes.map((index) => input.receipts[index]!),
|
|
14005
|
+
returnOutput: input.returnOutput,
|
|
14006
|
+
},
|
|
14007
|
+
options,
|
|
13284
14008
|
);
|
|
14009
|
+
cacheInsertIndexes.forEach((index, position) => {
|
|
14010
|
+
results[index] = published[position]!;
|
|
14011
|
+
});
|
|
14012
|
+
if (leasedIndexes.length > 0) {
|
|
14013
|
+
const completed = await completePostgresSchedulerReceipts(
|
|
14014
|
+
client,
|
|
14015
|
+
{
|
|
14016
|
+
receipts: leasedIndexes.map((index) => input.receipts[index]!),
|
|
14017
|
+
returnOutput: input.returnOutput,
|
|
14018
|
+
},
|
|
14019
|
+
options,
|
|
14020
|
+
);
|
|
14021
|
+
leasedIndexes.forEach((index, position) => {
|
|
14022
|
+
results[index] = completed[position]!;
|
|
14023
|
+
});
|
|
14024
|
+
}
|
|
14025
|
+
return results;
|
|
13285
14026
|
}
|
|
13286
14027
|
const normalized = input.receipts.map((receipt) => ({
|
|
13287
14028
|
...receipt,
|
|
13288
14029
|
runAttempt: normalizePostgresSchedulerReceiptRunAttempt(receipt.runAttempt),
|
|
13289
14030
|
leaseId: receipt.leaseId?.trim() || null,
|
|
13290
|
-
output:
|
|
14031
|
+
output: receiptOutputJson(receipt),
|
|
13291
14032
|
}));
|
|
13292
14033
|
const result = await client.query<ReceiptRow & { ord: number | string }>(
|
|
13293
14034
|
`
|
|
@@ -13297,6 +14038,7 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13297
14038
|
attempt_values.run_attempt,
|
|
13298
14039
|
lease_values.lease_id,
|
|
13299
14040
|
output_values.output_json,
|
|
14041
|
+
ref_values.output_ref,
|
|
13300
14042
|
key_values.ord
|
|
13301
14043
|
FROM unnest($1::text[]) WITH ORDINALITY AS key_values(k, ord)
|
|
13302
14044
|
JOIN unnest($2::text[]) WITH ORDINALITY AS run_values(run_id, ord)
|
|
@@ -13307,11 +14049,14 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13307
14049
|
ON lease_values.ord = key_values.ord
|
|
13308
14050
|
JOIN unnest($5::text[]) WITH ORDINALITY AS output_values(output_json, ord)
|
|
13309
14051
|
ON output_values.ord = key_values.ord
|
|
14052
|
+
JOIN unnest($6::text[]) WITH ORDINALITY AS ref_values(output_ref, ord)
|
|
14053
|
+
ON ref_values.ord = key_values.ord
|
|
13310
14054
|
),
|
|
13311
14055
|
completed AS (
|
|
13312
14056
|
UPDATE ${tableName('receipts', options)} AS target
|
|
13313
14057
|
SET status = 'completed',
|
|
13314
14058
|
output_json = inputs.output_json::jsonb,
|
|
14059
|
+
output_ref = inputs.output_ref::jsonb,
|
|
13315
14060
|
error = NULL,
|
|
13316
14061
|
error_payload = NULL,
|
|
13317
14062
|
failure_kind = 0,
|
|
@@ -13320,7 +14065,7 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13320
14065
|
updated_at = now()
|
|
13321
14066
|
FROM inputs
|
|
13322
14067
|
WHERE target.k = inputs.k
|
|
13323
|
-
AND target.org_id = $
|
|
14068
|
+
AND target.org_id = $7
|
|
13324
14069
|
AND target.run_id = inputs.run_id
|
|
13325
14070
|
AND target.status = 'running'
|
|
13326
14071
|
AND COALESCE(target.lease_owner_run_id, target.run_id) = inputs.run_id
|
|
@@ -13334,7 +14079,8 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13334
14079
|
)
|
|
13335
14080
|
RETURNING target.k,
|
|
13336
14081
|
target.status,
|
|
13337
|
-
CASE WHEN $
|
|
14082
|
+
CASE WHEN $8::boolean THEN target.output_json ELSE NULL::jsonb END AS output_json,
|
|
14083
|
+
target.output_ref,
|
|
13338
14084
|
target.error,
|
|
13339
14085
|
target.error_payload,
|
|
13340
14086
|
target.failure_kind,
|
|
@@ -13348,6 +14094,7 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13348
14094
|
SELECT completed.k,
|
|
13349
14095
|
completed.status,
|
|
13350
14096
|
completed.output_json,
|
|
14097
|
+
completed.output_ref,
|
|
13351
14098
|
completed.error,
|
|
13352
14099
|
completed.error_payload,
|
|
13353
14100
|
completed.failure_kind,
|
|
@@ -13367,11 +14114,19 @@ export async function completePostgresSchedulerReceipts(
|
|
|
13367
14114
|
normalized.map((receipt) => receipt.runAttempt),
|
|
13368
14115
|
normalized.map((receipt) => receipt.leaseId),
|
|
13369
14116
|
normalized.map((receipt) => receipt.output),
|
|
14117
|
+
normalized.map((receipt) => receiptOutputRefJson(receipt)),
|
|
13370
14118
|
receiptOrgId(options),
|
|
13371
14119
|
input.returnOutput !== false,
|
|
13372
14120
|
],
|
|
13373
14121
|
);
|
|
13374
|
-
return result.rows.map((row) =>
|
|
14122
|
+
return result.rows.map((row) =>
|
|
14123
|
+
row.k
|
|
14124
|
+
? {
|
|
14125
|
+
...mapReceiptRow(row),
|
|
14126
|
+
completionDisposition: 'persisted',
|
|
14127
|
+
}
|
|
14128
|
+
: null,
|
|
14129
|
+
);
|
|
13375
14130
|
}
|
|
13376
14131
|
|
|
13377
14132
|
export async function failPostgresSchedulerReceipt(
|
|
@@ -13388,6 +14143,7 @@ export async function failPostgresSchedulerReceipt(
|
|
|
13388
14143
|
UPDATE ${tableName('receipts', options)}
|
|
13389
14144
|
SET status = 'failed',
|
|
13390
14145
|
output_json = NULL,
|
|
14146
|
+
output_ref = NULL,
|
|
13391
14147
|
error = $3,
|
|
13392
14148
|
error_payload = $8::jsonb,
|
|
13393
14149
|
failure_kind = $6::smallint,
|
|
@@ -13467,6 +14223,7 @@ export async function failPostgresSchedulerReceipts(
|
|
|
13467
14223
|
UPDATE ${tableName('receipts', options)} AS target
|
|
13468
14224
|
SET status = 'failed',
|
|
13469
14225
|
output_json = NULL,
|
|
14226
|
+
output_ref = NULL,
|
|
13470
14227
|
error = inputs.error,
|
|
13471
14228
|
error_payload = inputs.error_payload,
|
|
13472
14229
|
failure_kind = inputs.failure_kind,
|
|
@@ -13546,6 +14303,7 @@ export async function releasePostgresSchedulerReceipt(
|
|
|
13546
14303
|
-- return to that claimable state for a later run to retry it.
|
|
13547
14304
|
SET status = 'pending',
|
|
13548
14305
|
output_json = NULL,
|
|
14306
|
+
output_ref = NULL,
|
|
13549
14307
|
error = NULL,
|
|
13550
14308
|
error_payload = NULL,
|
|
13551
14309
|
failure_kind = 0,
|
|
@@ -13572,6 +14330,22 @@ export async function releasePostgresSchedulerReceipt(
|
|
|
13572
14330
|
return result.rows[0] ? mapReceiptRow(result.rows[0]) : null;
|
|
13573
14331
|
}
|
|
13574
14332
|
|
|
14333
|
+
/**
|
|
14334
|
+
* The write that returns a claimed receipt to the free pool. Every release
|
|
14335
|
+
* path shares it so a successor's non-forced claim — whose status set is
|
|
14336
|
+
* `('pending','failed')` and whose lease guard requires no live lease — always
|
|
14337
|
+
* matches what a dead owner left behind.
|
|
14338
|
+
*/
|
|
14339
|
+
const RELEASE_RECEIPT_TO_PENDING_SET_CLAUSE = `status = 'pending',
|
|
14340
|
+
output_json = NULL,
|
|
14341
|
+
output_ref = NULL,
|
|
14342
|
+
error = NULL,
|
|
14343
|
+
error_payload = NULL,
|
|
14344
|
+
failure_kind = 0,
|
|
14345
|
+
lease_id = NULL,
|
|
14346
|
+
lease_expires_at = NULL,
|
|
14347
|
+
updated_at = now()`;
|
|
14348
|
+
|
|
13575
14349
|
export async function releasePostgresSchedulerReceiptsForAttempt(
|
|
13576
14350
|
client: PostgresSchedulerQueryClient,
|
|
13577
14351
|
input: { runId: string; runAttempt: number },
|
|
@@ -13583,14 +14357,7 @@ export async function releasePostgresSchedulerReceiptsForAttempt(
|
|
|
13583
14357
|
const released = await client.query<{ k: string }>(
|
|
13584
14358
|
`
|
|
13585
14359
|
UPDATE ${tableName('receipts', options)}
|
|
13586
|
-
SET
|
|
13587
|
-
output_json = NULL,
|
|
13588
|
-
error = NULL,
|
|
13589
|
-
error_payload = NULL,
|
|
13590
|
-
failure_kind = 0,
|
|
13591
|
-
lease_id = NULL,
|
|
13592
|
-
lease_expires_at = NULL,
|
|
13593
|
-
updated_at = now()
|
|
14360
|
+
SET ${RELEASE_RECEIPT_TO_PENDING_SET_CLAUSE}
|
|
13594
14361
|
WHERE org_id = $1
|
|
13595
14362
|
AND run_id = $2
|
|
13596
14363
|
AND status IN ('queued', 'running')
|
|
@@ -13843,6 +14610,7 @@ export async function skipPostgresSchedulerReceipt(
|
|
|
13843
14610
|
lease_owner_attempt = $4,
|
|
13844
14611
|
lease_expires_at = NULL,
|
|
13845
14612
|
output_json = $3::jsonb,
|
|
14613
|
+
output_ref = NULL,
|
|
13846
14614
|
error = NULL,
|
|
13847
14615
|
error_payload = NULL,
|
|
13848
14616
|
failure_kind = 0,
|
|
@@ -14945,8 +15713,14 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
|
|
|
14945
15713
|
];
|
|
14946
15714
|
}
|
|
14947
15715
|
if (event.eventType === 'run.completed') {
|
|
15716
|
+
const progressEvents = ledgerEventsPayloadField(
|
|
15717
|
+
event.payload,
|
|
15718
|
+
'terminalProgressEvents',
|
|
15719
|
+
event.runId,
|
|
15720
|
+
);
|
|
14948
15721
|
const lines = stringArrayPayloadField(event.payload, 'terminalLogLines');
|
|
14949
15722
|
return [
|
|
15723
|
+
...progressEvents,
|
|
14950
15724
|
...(lines.length === 0
|
|
14951
15725
|
? []
|
|
14952
15726
|
: [
|
|
@@ -14970,10 +15744,28 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
|
|
|
14970
15744
|
];
|
|
14971
15745
|
}
|
|
14972
15746
|
if (event.eventType === 'run.failed') {
|
|
15747
|
+
const progressEvents = ledgerEventsPayloadField(
|
|
15748
|
+
event.payload,
|
|
15749
|
+
'terminalProgressEvents',
|
|
15750
|
+
event.runId,
|
|
15751
|
+
);
|
|
15752
|
+
const lines = stringArrayPayloadField(event.payload, 'terminalLogLines');
|
|
14973
15753
|
const result = terminalRunResultForLedger(
|
|
14974
15754
|
payloadField(event.payload, 'result'),
|
|
14975
15755
|
);
|
|
14976
15756
|
return [
|
|
15757
|
+
...progressEvents,
|
|
15758
|
+
...(lines.length === 0
|
|
15759
|
+
? []
|
|
15760
|
+
: [
|
|
15761
|
+
{
|
|
15762
|
+
type: 'log.appended',
|
|
15763
|
+
runId: event.runId,
|
|
15764
|
+
occurredAt,
|
|
15765
|
+
source: 'worker',
|
|
15766
|
+
lines,
|
|
15767
|
+
} as PlayRunLedgerEvent,
|
|
15768
|
+
]),
|
|
14977
15769
|
{
|
|
14978
15770
|
type: 'run.failed',
|
|
14979
15771
|
runId: event.runId,
|
|
@@ -15930,6 +16722,26 @@ function stringArrayPayloadField(payload: unknown, key: string): string[] {
|
|
|
15930
16722
|
: [];
|
|
15931
16723
|
}
|
|
15932
16724
|
|
|
16725
|
+
function ledgerEventsPayloadField(
|
|
16726
|
+
payload: unknown,
|
|
16727
|
+
key: string,
|
|
16728
|
+
runId: string,
|
|
16729
|
+
): PlayRunLedgerIngressEvent[] {
|
|
16730
|
+
const value = payloadField(payload, key);
|
|
16731
|
+
if (!Array.isArray(value)) return [];
|
|
16732
|
+
return value.filter((candidate): candidate is PlayRunLedgerIngressEvent => {
|
|
16733
|
+
const record = recordPayload(candidate);
|
|
16734
|
+
return (
|
|
16735
|
+
record !== null &&
|
|
16736
|
+
record.runId === runId &&
|
|
16737
|
+
typeof record.occurredAt === 'number' &&
|
|
16738
|
+
Number.isFinite(record.occurredAt) &&
|
|
16739
|
+
typeof record.source === 'string' &&
|
|
16740
|
+
(typeof record.type === 'string' || typeof record.key === 'string')
|
|
16741
|
+
);
|
|
16742
|
+
});
|
|
16743
|
+
}
|
|
16744
|
+
|
|
15933
16745
|
function numberPayloadField(payload: unknown, key: string): number | null {
|
|
15934
16746
|
const value = payloadField(payload, key);
|
|
15935
16747
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|