deepline 0.3.258 → 0.3.259
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/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +21 -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 +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -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 +12 -1
|
@@ -100,6 +100,7 @@ import {
|
|
|
100
100
|
runtimeWorkCountersFromResources,
|
|
101
101
|
runtimeWorkCountersFromStats,
|
|
102
102
|
} from '../runtime-work-counters';
|
|
103
|
+
import { readPlayLaunchSurface } from '../launch-source-policy';
|
|
103
104
|
import {
|
|
104
105
|
isCtxFetchHttpFailureDiagnostic,
|
|
105
106
|
parseCtxFetchHttpFailureDiagnostic,
|
|
@@ -311,7 +312,9 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
|
|
|
311
312
|
// v50 adds a partial run-scoped outbox index for projection lease verification.
|
|
312
313
|
// v51 also creates that index in isolated (transactional) bootstraps and makes
|
|
313
314
|
// deploy readiness require it; v50 only built it on the concurrent path.
|
|
314
|
-
|
|
315
|
+
// v52 indexes only delivered outbox rows that still contain raw log bodies.
|
|
316
|
+
// The concurrent build lets retention skip already-scrubbed history.
|
|
317
|
+
export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 52;
|
|
315
318
|
const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
|
|
316
319
|
'scheduler_schema_migrations';
|
|
317
320
|
export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
|
|
@@ -446,6 +449,15 @@ type PostgresSchedulerClaimExecutionOptions = Pick<
|
|
|
446
449
|
/** Internal provider-read coordinates which must survive terminal-worker
|
|
447
450
|
* loss. They are persisted only on the matching durable cleanup obligation. */
|
|
448
451
|
sandboxCrashDiagnostic?: PostgresSchedulerSandboxCrashDiagnosticRequest;
|
|
452
|
+
/** Settled progress facts for a failed runner result, folded before the
|
|
453
|
+
* terminal write. They travel in the `run.failed` outbox row ahead of the
|
|
454
|
+
* terminal fact, the same carrier a direct completion uses, so the ledger
|
|
455
|
+
* settles them before the run's terminal makes its steps immutable. */
|
|
456
|
+
failedTerminalProgressEvents?: readonly unknown[];
|
|
457
|
+
/** Forwarded to `failPostgresSchedulerAttempt`: a failure terminal written
|
|
458
|
+
* for a detached-runner attempt must not overwrite an authoritative pushed
|
|
459
|
+
* runner terminal that won after the attempt was claimed. */
|
|
460
|
+
preserveRunnerTerminal?: boolean;
|
|
449
461
|
};
|
|
450
462
|
|
|
451
463
|
export type PostgresSchedulerWorkerOnceResult =
|
|
@@ -900,6 +912,20 @@ export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_MS =
|
|
|
900
912
|
export const POSTGRES_SCHEDULER_OUTBOX_LOG_BODY_RETENTION_MS =
|
|
901
913
|
7 * 24 * 60 * 60 * 1_000;
|
|
902
914
|
export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_BATCH_LIMIT = 1_000;
|
|
915
|
+
// Keep this expression identical in the partial index and retention query so
|
|
916
|
+
// PostgreSQL can prove that the ordered candidate scan is index-qualified.
|
|
917
|
+
// Strict JSONPath plus the array guards matches the old jsonb_array_elements
|
|
918
|
+
// eligibility test, including present-but-null log fields.
|
|
919
|
+
const DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE = `
|
|
920
|
+
(
|
|
921
|
+
(event_type = 'run.events'
|
|
922
|
+
AND jsonb_typeof(payload_json->'events') = 'array'
|
|
923
|
+
AND payload_json @? 'strict $.events[*] ? (@.type == "log.appended" && (exists(@.lines) || exists(@.lineContexts)))'::jsonpath)
|
|
924
|
+
OR (event_type IN ('run.completed', 'run.failed')
|
|
925
|
+
AND (payload_json ? 'terminalLogLines'
|
|
926
|
+
OR (jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
|
|
927
|
+
AND payload_json @? 'strict $.terminalProgressEvents[*] ? (@.type == "log.appended" && (exists(@.lines) || exists(@.lineContexts)))'::jsonpath)))
|
|
928
|
+
)`;
|
|
903
929
|
const RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
|
|
904
930
|
const COMPUTE_BILLING_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
|
|
905
931
|
|
|
@@ -1933,6 +1959,15 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1933
1959
|
AND index_row.indisready AND index_row.indisvalid
|
|
1934
1960
|
)
|
|
1935
1961
|
)
|
|
1962
|
+
AND EXISTS (
|
|
1963
|
+
SELECT 1 FROM pg_index AS index_row
|
|
1964
|
+
JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
|
|
1965
|
+
JOIN pg_namespace AS index_namespace ON index_namespace.oid = index_class.relnamespace
|
|
1966
|
+
WHERE index_namespace.nspname = $1
|
|
1967
|
+
AND index_class.relname = 'runtime_scheduler_outbox_delivered_log_body_idx'
|
|
1968
|
+
AND index_row.indrelid = to_regclass($3)
|
|
1969
|
+
AND index_row.indisready AND index_row.indisvalid
|
|
1970
|
+
)
|
|
1936
1971
|
AND (
|
|
1937
1972
|
$6::boolean = false
|
|
1938
1973
|
OR EXISTS (
|
|
@@ -3852,6 +3887,12 @@ export async function ensurePostgresSchedulerSchema(
|
|
|
3852
3887
|
ON ${tableName('outbox', options)} (delivered_at, event_id)
|
|
3853
3888
|
WHERE delivered_at IS NOT NULL AND failed_at IS NULL
|
|
3854
3889
|
`);
|
|
3890
|
+
await client.query(`
|
|
3891
|
+
CREATE INDEX IF NOT EXISTS runtime_scheduler_outbox_delivered_log_body_idx
|
|
3892
|
+
ON ${tableName('outbox', options)} (delivered_at, event_id)
|
|
3893
|
+
WHERE delivered_at IS NOT NULL AND failed_at IS NULL
|
|
3894
|
+
AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}
|
|
3895
|
+
`);
|
|
3855
3896
|
await client.query(`
|
|
3856
3897
|
CREATE INDEX IF NOT EXISTS runtime_scheduler_outbox_blocked_oldest_idx
|
|
3857
3898
|
ON ${tableName('outbox', options)} (created_at, event_id)
|
|
@@ -4004,6 +4045,13 @@ const POSTGRES_SCHEDULER_RUNTIME_ADMIN_INDEXES = [
|
|
|
4004
4045
|
definition: `(delivered_at, event_id)
|
|
4005
4046
|
WHERE delivered_at IS NOT NULL AND failed_at IS NULL`,
|
|
4006
4047
|
},
|
|
4048
|
+
{
|
|
4049
|
+
name: 'runtime_scheduler_outbox_delivered_log_body_idx',
|
|
4050
|
+
table: 'outbox',
|
|
4051
|
+
definition: `(delivered_at, event_id)
|
|
4052
|
+
WHERE delivered_at IS NOT NULL AND failed_at IS NULL
|
|
4053
|
+
AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}`,
|
|
4054
|
+
},
|
|
4007
4055
|
{
|
|
4008
4056
|
name: 'runtime_scheduler_outbox_blocked_oldest_idx',
|
|
4009
4057
|
table: 'outbox',
|
|
@@ -5053,6 +5101,7 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
5053
5101
|
'staticPipeline', launch_payload.launch_json -> 'staticPipeline',
|
|
5054
5102
|
'source', launch_payload.launch_json ->> 'source',
|
|
5055
5103
|
'triggerSource', launch_payload.launch_json ->> 'triggerSource',
|
|
5104
|
+
'launchSurface', launch_payload.launch_json ->> 'launchSurface',
|
|
5056
5105
|
'inputFileId', launch_payload.launch_json ->> 'inputFileId',
|
|
5057
5106
|
'inputBytes', launch_payload.launch_json -> 'inputBytes',
|
|
5058
5107
|
'inputSha256', launch_payload.launch_json ->> 'inputSha256',
|
|
@@ -7184,6 +7233,10 @@ export async function markPostgresSchedulerAttemptWaiting(
|
|
|
7184
7233
|
const eventKey = suspensionEventKey(input.suspension);
|
|
7185
7234
|
const detachedRunnerCeilingDeadlineMs =
|
|
7186
7235
|
detachedRunnerCeilingDeadlineMsFromSuspension(input.suspension);
|
|
7236
|
+
const sleepWakeAtMs = sleepWakeAtMsFromSuspension(
|
|
7237
|
+
input.suspension,
|
|
7238
|
+
Date.now(),
|
|
7239
|
+
);
|
|
7187
7240
|
const waiting = await client.query<{ run_id: string; event_id: string }>(
|
|
7188
7241
|
`
|
|
7189
7242
|
WITH waiting_attempt AS (
|
|
@@ -7252,7 +7305,8 @@ export async function markPostgresSchedulerAttemptWaiting(
|
|
|
7252
7305
|
'run.waiting',
|
|
7253
7306
|
jsonb_strip_nulls(jsonb_build_object(
|
|
7254
7307
|
'kind', NULLIF(btrim($4::jsonb ->> 'kind'), ''),
|
|
7255
|
-
'eventKey', NULLIF($7::text, '')
|
|
7308
|
+
'eventKey', NULLIF($7::text, ''),
|
|
7309
|
+
'waitUntil', $10::double precision
|
|
7256
7310
|
)),
|
|
7257
7311
|
${causalOutboxCreatedAt('waiting_run.run_id', options)}
|
|
7258
7312
|
FROM waiting_run
|
|
@@ -7270,6 +7324,7 @@ export async function markPostgresSchedulerAttemptWaiting(
|
|
|
7270
7324
|
eventKey,
|
|
7271
7325
|
input.preserveHeartbeatAt === true,
|
|
7272
7326
|
detachedRunnerCeilingDeadlineMs,
|
|
7327
|
+
sleepWakeAtMs,
|
|
7273
7328
|
],
|
|
7274
7329
|
);
|
|
7275
7330
|
const row = waiting.rows[0];
|
|
@@ -7283,7 +7338,29 @@ export async function markPostgresSchedulerAttemptWaiting(
|
|
|
7283
7338
|
* lane. Other wait kinds intentionally return null: event/HITL waits are not
|
|
7284
7339
|
* execution timeouts.
|
|
7285
7340
|
*/
|
|
7286
|
-
|
|
7341
|
+
/**
|
|
7342
|
+
* When a `ctx.sleep` park will wake, measured from the moment the scheduler
|
|
7343
|
+
* records the wait. Observers render it ("sleeping until 14:05:09") so a sleep
|
|
7344
|
+
* is never described as an approval or event wait.
|
|
7345
|
+
*/
|
|
7346
|
+
export function sleepWakeAtMsFromSuspension(
|
|
7347
|
+
suspension: unknown,
|
|
7348
|
+
nowMs: number,
|
|
7349
|
+
): number | null {
|
|
7350
|
+
if (
|
|
7351
|
+
!suspension ||
|
|
7352
|
+
typeof suspension !== 'object' ||
|
|
7353
|
+
Array.isArray(suspension)
|
|
7354
|
+
) {
|
|
7355
|
+
return null;
|
|
7356
|
+
}
|
|
7357
|
+
const candidate = suspension as { kind?: unknown; delayMs?: unknown };
|
|
7358
|
+
if (candidate.kind !== 'sleep') return null;
|
|
7359
|
+
const delayMs = Number(candidate.delayMs);
|
|
7360
|
+
return Number.isFinite(delayMs) && delayMs >= 0 ? nowMs + delayMs : null;
|
|
7361
|
+
}
|
|
7362
|
+
|
|
7363
|
+
export function detachedRunnerCeilingDeadlineMsFromSuspension(
|
|
7287
7364
|
suspension: unknown,
|
|
7288
7365
|
): number | null {
|
|
7289
7366
|
if (
|
|
@@ -7964,6 +8041,204 @@ export async function deferPostgresSchedulerAttemptForCapacity(
|
|
|
7964
8041
|
return deferred.rows.length > 0;
|
|
7965
8042
|
}
|
|
7966
8043
|
|
|
8044
|
+
/** `error_json.kind` stamped on an attempt retired by an executor-token
|
|
8045
|
+
* outage. Consecutive attempts carrying it form one bounded outage. */
|
|
8046
|
+
export const EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND =
|
|
8047
|
+
'executor_token_unavailable';
|
|
8048
|
+
|
|
8049
|
+
/**
|
|
8050
|
+
* Return a fresh, pre-code attempt to durable queue admission after the
|
|
8051
|
+
* executor-token route was transiently unreachable. Like a capacity deferral,
|
|
8052
|
+
* no Play code ran and no engine attempt is spent. Unlike capacity, an
|
|
8053
|
+
* unreachable app may never recover, so the deferral is bounded by wall-clock:
|
|
8054
|
+
* the outage starts at the oldest attempt in the unbroken tail of attempts
|
|
8055
|
+
* retired by this same outage, and once it is older than `maxOutageMs` the
|
|
8056
|
+
* attempt is left untouched (`deferred: false`) for the caller to terminalize.
|
|
8057
|
+
*/
|
|
8058
|
+
export async function deferPostgresSchedulerAttemptForExecutorTokenOutage(
|
|
8059
|
+
client: PostgresSchedulerQueryClient,
|
|
8060
|
+
input: {
|
|
8061
|
+
runId: string;
|
|
8062
|
+
attempt: number;
|
|
8063
|
+
leaseToken: string;
|
|
8064
|
+
reason: string;
|
|
8065
|
+
maxOutageMs: number;
|
|
8066
|
+
},
|
|
8067
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
8068
|
+
): Promise<{ deferred: boolean; outageStartedAtMs: number | null }> {
|
|
8069
|
+
const result = await client.query<{
|
|
8070
|
+
outage_started_at_ms: number | string | null;
|
|
8071
|
+
deferred: boolean;
|
|
8072
|
+
}>(
|
|
8073
|
+
`
|
|
8074
|
+
WITH current_attempt AS (
|
|
8075
|
+
SELECT created_at
|
|
8076
|
+
FROM ${tableName('attempts', options)}
|
|
8077
|
+
WHERE run_id = $1
|
|
8078
|
+
AND attempt = $2
|
|
8079
|
+
AND lease_token = $3
|
|
8080
|
+
AND status IN ('claimed', 'running')
|
|
8081
|
+
),
|
|
8082
|
+
outage AS (
|
|
8083
|
+
SELECT LEAST(
|
|
8084
|
+
(SELECT created_at FROM current_attempt),
|
|
8085
|
+
(
|
|
8086
|
+
SELECT min(prior.created_at)
|
|
8087
|
+
FROM ${tableName('attempts', options)} AS prior
|
|
8088
|
+
WHERE prior.run_id = $1
|
|
8089
|
+
AND prior.attempt < $2
|
|
8090
|
+
AND prior.error_json ->> 'kind' = $6
|
|
8091
|
+
AND NOT EXISTS (
|
|
8092
|
+
SELECT 1
|
|
8093
|
+
FROM ${tableName('attempts', options)} AS gap
|
|
8094
|
+
WHERE gap.run_id = $1
|
|
8095
|
+
AND gap.attempt > prior.attempt
|
|
8096
|
+
AND gap.attempt < $2
|
|
8097
|
+
AND gap.error_json ->> 'kind' IS DISTINCT FROM $6
|
|
8098
|
+
)
|
|
8099
|
+
)
|
|
8100
|
+
) AS started_at
|
|
8101
|
+
),
|
|
8102
|
+
deferred_attempt AS (
|
|
8103
|
+
UPDATE ${tableName('attempts', options)}
|
|
8104
|
+
SET status = 'stale',
|
|
8105
|
+
lease_expires_at = NULL,
|
|
8106
|
+
heartbeat_at = now(),
|
|
8107
|
+
error_json = $4::jsonb,
|
|
8108
|
+
updated_at = now()
|
|
8109
|
+
WHERE run_id = $1
|
|
8110
|
+
AND attempt = $2
|
|
8111
|
+
AND status IN ('claimed', 'running')
|
|
8112
|
+
AND lease_token = $3
|
|
8113
|
+
AND (SELECT started_at FROM outage) >
|
|
8114
|
+
clock_timestamp() - ($5::double precision * interval '1 millisecond')
|
|
8115
|
+
RETURNING run_id
|
|
8116
|
+
),
|
|
8117
|
+
requeued_run AS (
|
|
8118
|
+
UPDATE ${tableName('runs', options)}
|
|
8119
|
+
SET status = 'queued',
|
|
8120
|
+
suspension_json = NULL,
|
|
8121
|
+
wait_deadline = NULL,
|
|
8122
|
+
updated_at = now()
|
|
8123
|
+
WHERE run_id IN (SELECT run_id FROM deferred_attempt)
|
|
8124
|
+
AND status IN ('queued', 'running', 'waiting')
|
|
8125
|
+
RETURNING run_id
|
|
8126
|
+
)
|
|
8127
|
+
SELECT
|
|
8128
|
+
(extract(epoch FROM (SELECT started_at FROM outage)) * 1000)::float8
|
|
8129
|
+
AS outage_started_at_ms,
|
|
8130
|
+
EXISTS (SELECT 1 FROM requeued_run) AS deferred
|
|
8131
|
+
`,
|
|
8132
|
+
[
|
|
8133
|
+
input.runId,
|
|
8134
|
+
input.attempt,
|
|
8135
|
+
input.leaseToken,
|
|
8136
|
+
stringifyPostgresJson({
|
|
8137
|
+
kind: EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND,
|
|
8138
|
+
message: input.reason,
|
|
8139
|
+
}),
|
|
8140
|
+
input.maxOutageMs,
|
|
8141
|
+
EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND,
|
|
8142
|
+
],
|
|
8143
|
+
);
|
|
8144
|
+
const row = result.rows[0];
|
|
8145
|
+
const startedAtMs =
|
|
8146
|
+
row?.outage_started_at_ms == null ? NaN : Number(row.outage_started_at_ms);
|
|
8147
|
+
return {
|
|
8148
|
+
deferred: row?.deferred === true,
|
|
8149
|
+
outageStartedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : null,
|
|
8150
|
+
};
|
|
8151
|
+
}
|
|
8152
|
+
|
|
8153
|
+
/**
|
|
8154
|
+
* Take the fence of one parked detached-runner attempt so its owner can write
|
|
8155
|
+
* a failure terminal. This is the single-run form of the overdue-ceiling claim
|
|
8156
|
+
* (same guards, same `waiting -> running` transition and `run.resumed` fact):
|
|
8157
|
+
* it requires the exact attempt and lease token, refuses when an authoritative
|
|
8158
|
+
* runner terminal was already pushed, and refuses when a newer attempt exists.
|
|
8159
|
+
* Returns false (no write) when any guard fails.
|
|
8160
|
+
*/
|
|
8161
|
+
export async function claimPostgresSchedulerWaitingDetachedAttemptForTerminal(
|
|
8162
|
+
client: PostgresSchedulerQueryClient,
|
|
8163
|
+
input: { runId: string; attempt: number; leaseToken: string },
|
|
8164
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
8165
|
+
): Promise<boolean> {
|
|
8166
|
+
const claimed = await client.query<{ run_id: string }>(
|
|
8167
|
+
`
|
|
8168
|
+
WITH candidate AS MATERIALIZED (
|
|
8169
|
+
SELECT runs.run_id, attempts.attempt
|
|
8170
|
+
FROM ${tableName('runs', options)} AS runs
|
|
8171
|
+
JOIN ${tableName('attempts', options)} AS attempts
|
|
8172
|
+
ON attempts.run_id = runs.run_id
|
|
8173
|
+
AND attempts.attempt = (runs.suspension_json ->> 'runnerAttempt')::integer
|
|
8174
|
+
WHERE runs.run_id = $1
|
|
8175
|
+
AND runs.status = 'waiting'
|
|
8176
|
+
AND runs.suspension_json ->> 'kind' = 'detached_runner'
|
|
8177
|
+
AND attempts.attempt = $2
|
|
8178
|
+
AND attempts.status = 'waiting'
|
|
8179
|
+
AND attempts.lease_token = $3
|
|
8180
|
+
AND (
|
|
8181
|
+
attempts.runner_result_json IS NULL
|
|
8182
|
+
OR attempts.runner_result_json ->> 'status' = 'suspended'
|
|
8183
|
+
)
|
|
8184
|
+
AND NOT EXISTS (
|
|
8185
|
+
SELECT 1
|
|
8186
|
+
FROM ${tableName('attempts', options)} AS newer
|
|
8187
|
+
WHERE newer.run_id = attempts.run_id
|
|
8188
|
+
AND newer.attempt > attempts.attempt
|
|
8189
|
+
AND newer.status IN ('claimed', 'running', 'waiting', 'completed', 'failed')
|
|
8190
|
+
)
|
|
8191
|
+
FOR UPDATE OF runs, attempts
|
|
8192
|
+
),
|
|
8193
|
+
running_attempt AS (
|
|
8194
|
+
UPDATE ${tableName('attempts', options)} AS target
|
|
8195
|
+
SET status = 'running',
|
|
8196
|
+
heartbeat_at = now(),
|
|
8197
|
+
updated_at = now()
|
|
8198
|
+
FROM candidate
|
|
8199
|
+
WHERE target.run_id = candidate.run_id
|
|
8200
|
+
AND target.attempt = candidate.attempt
|
|
8201
|
+
AND target.status = 'waiting'
|
|
8202
|
+
AND target.lease_token = $3
|
|
8203
|
+
RETURNING target.run_id
|
|
8204
|
+
),
|
|
8205
|
+
running_run AS (
|
|
8206
|
+
UPDATE ${tableName('runs', options)} AS target
|
|
8207
|
+
SET status = 'running',
|
|
8208
|
+
suspension_json = NULL,
|
|
8209
|
+
wait_deadline = NULL,
|
|
8210
|
+
updated_at = now()
|
|
8211
|
+
WHERE target.run_id IN (SELECT run_id FROM running_attempt)
|
|
8212
|
+
AND target.status = 'waiting'
|
|
8213
|
+
RETURNING target.run_id
|
|
8214
|
+
),
|
|
8215
|
+
resumed_outbox AS (
|
|
8216
|
+
INSERT INTO ${tableName('outbox', options)} (
|
|
8217
|
+
event_id, run_id, event_type, payload_json
|
|
8218
|
+
)
|
|
8219
|
+
SELECT
|
|
8220
|
+
$4,
|
|
8221
|
+
running_run.run_id,
|
|
8222
|
+
'run.resumed',
|
|
8223
|
+
jsonb_build_object(
|
|
8224
|
+
'kind', 'detached_runner',
|
|
8225
|
+
'eventKey', 'detached_runner_terminal_salvage'
|
|
8226
|
+
)
|
|
8227
|
+
FROM running_run
|
|
8228
|
+
ON CONFLICT (event_id) DO NOTHING
|
|
8229
|
+
)
|
|
8230
|
+
SELECT run_id FROM running_run
|
|
8231
|
+
`,
|
|
8232
|
+
[
|
|
8233
|
+
input.runId,
|
|
8234
|
+
input.attempt,
|
|
8235
|
+
input.leaseToken,
|
|
8236
|
+
`run-resumed:detached-runner-salvage:${input.runId}:${input.attempt}`,
|
|
8237
|
+
],
|
|
8238
|
+
);
|
|
8239
|
+
return claimed.rows.length > 0;
|
|
8240
|
+
}
|
|
8241
|
+
|
|
7967
8242
|
export type PostgresSchedulerRecoveryCircuit = {
|
|
7968
8243
|
dependency: string;
|
|
7969
8244
|
state: 'closed' | 'open' | 'half_open';
|
|
@@ -12733,6 +13008,9 @@ export async function failPostgresSchedulerAttempt(
|
|
|
12733
13008
|
* hard ceiling in the same fenced statement. */
|
|
12734
13009
|
preserveRunnerTerminal?: boolean;
|
|
12735
13010
|
sandboxCrashDiagnostic?: PostgresSchedulerSandboxCrashDiagnosticRequest;
|
|
13011
|
+
/** Progress facts the ledger must apply before `run.failed` (see
|
|
13012
|
+
* `failedTerminalProgressEvents`). Omitted from the payload when empty. */
|
|
13013
|
+
terminalProgressEvents?: readonly unknown[];
|
|
12736
13014
|
},
|
|
12737
13015
|
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
12738
13016
|
): Promise<boolean> {
|
|
@@ -13021,7 +13299,11 @@ export async function failPostgresSchedulerAttempt(
|
|
|
13021
13299
|
'terminalLogContexts', $14::jsonb,
|
|
13022
13300
|
'resultSummary', $13::jsonb,
|
|
13023
13301
|
'terminalWorkCounterSegment', $15::jsonb
|
|
13024
|
-
)
|
|
13302
|
+
) || CASE
|
|
13303
|
+
WHEN jsonb_array_length($16::jsonb) > 0
|
|
13304
|
+
THEN jsonb_build_object('terminalProgressEvents', $16::jsonb)
|
|
13305
|
+
ELSE '{}'::jsonb
|
|
13306
|
+
END,
|
|
13025
13307
|
${causalOutboxCreatedAt('run_terminal.run_id', options)}
|
|
13026
13308
|
FROM run_terminal
|
|
13027
13309
|
ON CONFLICT (event_id) DO NOTHING
|
|
@@ -13047,6 +13329,7 @@ export async function failPostgresSchedulerAttempt(
|
|
|
13047
13329
|
stringifyPostgresJson(resultSummary),
|
|
13048
13330
|
JSON.stringify(terminalLogContexts),
|
|
13049
13331
|
stringifyPostgresJson(terminalWorkCounterSegment ?? null),
|
|
13332
|
+
stringifyPostgresJson(input.terminalProgressEvents ?? []),
|
|
13050
13333
|
],
|
|
13051
13334
|
);
|
|
13052
13335
|
return failed.rows.length > 0;
|
|
@@ -14130,6 +14413,12 @@ export async function executePostgresSchedulerClaim(
|
|
|
14130
14413
|
terminalLogLines: terminalLogProjection.lines,
|
|
14131
14414
|
terminalLogContexts: terminalLogProjection.lineContexts,
|
|
14132
14415
|
sandboxCrashDiagnostic: options.sandboxCrashDiagnostic,
|
|
14416
|
+
...(options.preserveRunnerTerminal
|
|
14417
|
+
? { preserveRunnerTerminal: true }
|
|
14418
|
+
: {}),
|
|
14419
|
+
...(runnerFailureResult && options.failedTerminalProgressEvents?.length
|
|
14420
|
+
? { terminalProgressEvents: options.failedTerminalProgressEvents }
|
|
14421
|
+
: {}),
|
|
14133
14422
|
},
|
|
14134
14423
|
options,
|
|
14135
14424
|
);
|
|
@@ -17021,8 +17310,18 @@ export async function readPendingPostgresSchedulerOutboxEvents(
|
|
|
17021
17310
|
-- terminal facts. Project it first so a lifecycle backlog cannot make
|
|
17022
17311
|
-- an accepted run invisible; per-run predecessor checks above retain
|
|
17023
17312
|
-- strict ordering within each run.
|
|
17313
|
+
-- A terminal head comes next: it is a run's last fact (one per run, so
|
|
17314
|
+
-- it cannot starve anything), its predecessors are already delivered
|
|
17315
|
+
-- (checked above), and it is what a caller is waiting on. Under a
|
|
17316
|
+
-- lifecycle backlog, FIFO left a committed terminal queued behind other
|
|
17317
|
+
-- runs' progress for seconds (live-run-observation-e2e: 4.9 s from
|
|
17318
|
+
-- commit to first claim while the projector drained continuously).
|
|
17024
17319
|
ORDER BY
|
|
17025
|
-
CASE
|
|
17320
|
+
CASE
|
|
17321
|
+
WHEN candidate.event_type = 'run.created' THEN 0
|
|
17322
|
+
WHEN candidate.event_id LIKE 'run-terminal:%' THEN 1
|
|
17323
|
+
ELSE 2
|
|
17324
|
+
END,
|
|
17026
17325
|
coalesce(candidate.next_attempt_at, candidate.created_at),
|
|
17027
17326
|
candidate.created_at,
|
|
17028
17327
|
candidate.event_id
|
|
@@ -17416,37 +17715,11 @@ export async function purgePostgresSchedulerDeliveredOutboxRetentionBatch(
|
|
|
17416
17715
|
await client.query(
|
|
17417
17716
|
`WITH candidates AS (
|
|
17418
17717
|
SELECT event_id
|
|
17419
|
-
FROM ${tableName('outbox', options)}
|
|
17718
|
+
FROM ${tableName('outbox', options)}
|
|
17420
17719
|
WHERE delivered_at IS NOT NULL
|
|
17421
17720
|
AND failed_at IS NULL
|
|
17422
17721
|
AND delivered_at < $1::timestamptz
|
|
17423
|
-
AND
|
|
17424
|
-
(event_type = 'run.events'
|
|
17425
|
-
AND jsonb_typeof(payload_json->'events') = 'array'
|
|
17426
|
-
AND EXISTS (
|
|
17427
|
-
SELECT 1
|
|
17428
|
-
FROM jsonb_array_elements(
|
|
17429
|
-
CASE WHEN jsonb_typeof(payload_json->'events') = 'array'
|
|
17430
|
-
THEN payload_json->'events' ELSE '[]'::jsonb END
|
|
17431
|
-
) AS item
|
|
17432
|
-
WHERE item->>'type' = 'log.appended'
|
|
17433
|
-
AND (item ? 'lines' OR item ? 'lineContexts')
|
|
17434
|
-
))
|
|
17435
|
-
OR (event_type IN ('run.completed', 'run.failed')
|
|
17436
|
-
AND (
|
|
17437
|
-
payload_json ? 'terminalLogLines'
|
|
17438
|
-
OR (jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
|
|
17439
|
-
AND EXISTS (
|
|
17440
|
-
SELECT 1
|
|
17441
|
-
FROM jsonb_array_elements(
|
|
17442
|
-
CASE WHEN jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
|
|
17443
|
-
THEN payload_json->'terminalProgressEvents' ELSE '[]'::jsonb END
|
|
17444
|
-
) AS item
|
|
17445
|
-
WHERE item->>'type' = 'log.appended'
|
|
17446
|
-
AND (item ? 'lines' OR item ? 'lineContexts')
|
|
17447
|
-
))
|
|
17448
|
-
))
|
|
17449
|
-
)
|
|
17722
|
+
AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}
|
|
17450
17723
|
ORDER BY delivered_at ASC, event_id ASC
|
|
17451
17724
|
LIMIT $2::integer
|
|
17452
17725
|
FOR UPDATE SKIP LOCKED
|
|
@@ -17720,8 +17993,29 @@ export async function markPostgresSchedulerOutboxDelivered(
|
|
|
17720
17993
|
leaseOwner?: string | null;
|
|
17721
17994
|
},
|
|
17722
17995
|
): Promise<number> {
|
|
17723
|
-
|
|
17724
|
-
|
|
17996
|
+
return (
|
|
17997
|
+
await markPostgresSchedulerOutboxDeliveredRows(client, eventIds, options)
|
|
17998
|
+
).length;
|
|
17999
|
+
}
|
|
18000
|
+
|
|
18001
|
+
/**
|
|
18002
|
+
* Lease-fenced acknowledgement that also reports each row's event type and
|
|
18003
|
+
* commit-to-acknowledgement lag on the database clock, so lag metrics never
|
|
18004
|
+
* depend on skew between the projector host and Postgres.
|
|
18005
|
+
*/
|
|
18006
|
+
export async function markPostgresSchedulerOutboxDeliveredRows(
|
|
18007
|
+
client: PostgresSchedulerQueryClient,
|
|
18008
|
+
eventIds: string[],
|
|
18009
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'> & {
|
|
18010
|
+
leaseOwner?: string | null;
|
|
18011
|
+
},
|
|
18012
|
+
): Promise<RunLedgerOutboxDeliveredRow[]> {
|
|
18013
|
+
if (eventIds.length === 0) return [];
|
|
18014
|
+
const result = await client.query<{
|
|
18015
|
+
event_id: string;
|
|
18016
|
+
event_type: string;
|
|
18017
|
+
lag_ms: number | string | null;
|
|
18018
|
+
}>(
|
|
17725
18019
|
`
|
|
17726
18020
|
WITH delivered AS (
|
|
17727
18021
|
UPDATE ${tableName('outbox', options)}
|
|
@@ -17733,9 +18027,14 @@ export async function markPostgresSchedulerOutboxDelivered(
|
|
|
17733
18027
|
WHERE event_id = ANY($1::text[])
|
|
17734
18028
|
AND delivered_at IS NULL
|
|
17735
18029
|
AND ($2::text IS NULL OR lease_owner = $2)
|
|
17736
|
-
RETURNING event_id
|
|
18030
|
+
RETURNING event_id,
|
|
18031
|
+
event_type,
|
|
18032
|
+
(extract(epoch FROM (delivered_at - created_at)) * 1000)::float8
|
|
18033
|
+
AS lag_ms
|
|
17737
18034
|
)
|
|
17738
18035
|
SELECT event_id,
|
|
18036
|
+
event_type,
|
|
18037
|
+
lag_ms,
|
|
17739
18038
|
pg_notify(
|
|
17740
18039
|
$3,
|
|
17741
18040
|
json_build_object('eventId', event_id, 'kind', 'outbox_delivered')::text
|
|
@@ -17748,7 +18047,51 @@ export async function markPostgresSchedulerOutboxDelivered(
|
|
|
17748
18047
|
postgresSchedulerNotifyChannel('terminal', options),
|
|
17749
18048
|
],
|
|
17750
18049
|
);
|
|
17751
|
-
return result.rows.
|
|
18050
|
+
return result.rows.map((row) => {
|
|
18051
|
+
const lagMs = Number(row.lag_ms);
|
|
18052
|
+
return {
|
|
18053
|
+
eventType: row.event_type,
|
|
18054
|
+
lagMs: Number.isFinite(lagMs) ? Math.max(0, lagMs) : 0,
|
|
18055
|
+
};
|
|
18056
|
+
});
|
|
18057
|
+
}
|
|
18058
|
+
|
|
18059
|
+
/**
|
|
18060
|
+
* Give an unfinished delivery lease back without settling a failure. Used
|
|
18061
|
+
* only when the caller abandoned its own latency budget (the inline admission
|
|
18062
|
+
* attempt): the row keeps its attempt count and due time, and the outbox
|
|
18063
|
+
* channel wakes the durable projector to take it immediately.
|
|
18064
|
+
*/
|
|
18065
|
+
export async function releasePostgresSchedulerOutboxLease(
|
|
18066
|
+
client: PostgresSchedulerQueryClient,
|
|
18067
|
+
eventIds: string[],
|
|
18068
|
+
options: Pick<PostgresSchedulerOptions, 'schema'> & {
|
|
18069
|
+
leaseOwner: string;
|
|
18070
|
+
},
|
|
18071
|
+
): Promise<string[]> {
|
|
18072
|
+
const leaseOwner = options.leaseOwner.trim();
|
|
18073
|
+
if (eventIds.length === 0 || !leaseOwner) return [];
|
|
18074
|
+
const result = await client.query<{ event_id: string }>(
|
|
18075
|
+
`
|
|
18076
|
+
WITH released AS (
|
|
18077
|
+
UPDATE ${tableName('outbox', options)}
|
|
18078
|
+
SET lease_owner = NULL,
|
|
18079
|
+
lease_expires_at = NULL
|
|
18080
|
+
WHERE event_id = ANY($1::text[])
|
|
18081
|
+
AND delivered_at IS NULL
|
|
18082
|
+
AND lease_owner = $2
|
|
18083
|
+
RETURNING event_id
|
|
18084
|
+
)
|
|
18085
|
+
SELECT event_id,
|
|
18086
|
+
pg_notify(
|
|
18087
|
+
$3,
|
|
18088
|
+
json_build_object('eventId', event_id, 'kind', 'outbox_released')::text
|
|
18089
|
+
)
|
|
18090
|
+
FROM released
|
|
18091
|
+
`,
|
|
18092
|
+
[eventIds, leaseOwner, postgresSchedulerNotifyChannel('outbox', options)],
|
|
18093
|
+
);
|
|
18094
|
+
return result.rows.map((row) => row.event_id);
|
|
17752
18095
|
}
|
|
17753
18096
|
|
|
17754
18097
|
function normalizeOutboxIngressPreview(
|
|
@@ -17915,6 +18258,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
|
|
|
17915
18258
|
// fabricates an integration-event log from its missing event key.
|
|
17916
18259
|
if (waitKind === 'detached_runner') return [];
|
|
17917
18260
|
const eventKey = stringPayloadField(event.payload, 'eventKey');
|
|
18261
|
+
const waitUntil = numberPayloadField(event.payload, 'waitUntil');
|
|
17918
18262
|
const lifecycle = {
|
|
17919
18263
|
type: event.eventType,
|
|
17920
18264
|
runId: event.runId,
|
|
@@ -17923,6 +18267,9 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
|
|
|
17923
18267
|
...(event.eventType === 'run.waiting' && waitKind !== null
|
|
17924
18268
|
? { waitKind }
|
|
17925
18269
|
: {}),
|
|
18270
|
+
...(event.eventType === 'run.waiting' && waitUntil !== null
|
|
18271
|
+
? { waitUntil }
|
|
18272
|
+
: {}),
|
|
17926
18273
|
} as PlayRunLedgerEvent;
|
|
17927
18274
|
if (
|
|
17928
18275
|
waitKind !== null &&
|
|
@@ -18181,6 +18528,39 @@ export type RunLedgerOutboxDispatchInput = {
|
|
|
18181
18528
|
* app-side authoritative projector. The Fly worker MUST pass `gateway`.
|
|
18182
18529
|
*/
|
|
18183
18530
|
tokenAuthority?: RunLedgerOutboxTokenAuthority;
|
|
18531
|
+
/**
|
|
18532
|
+
* Run-scoped claims only: claim nothing unless this exact event is the
|
|
18533
|
+
* run's pending causal head. The inline admission attempt uses it so it can
|
|
18534
|
+
* only ever deliver the one `run.created` row it just committed.
|
|
18535
|
+
*/
|
|
18536
|
+
headEventId?: string | null;
|
|
18537
|
+
/**
|
|
18538
|
+
* Caller-owned deadline for the app-runtime append. When it fires, the
|
|
18539
|
+
* attempt is abandoned rather than failed: its lease is released without a
|
|
18540
|
+
* projection-attempt increment or retry backoff, so the durable projector
|
|
18541
|
+
* can take the same row immediately under the same idempotency key.
|
|
18542
|
+
*/
|
|
18543
|
+
signal?: AbortSignal;
|
|
18544
|
+
/**
|
|
18545
|
+
* Observation hook for acknowledged rows (event type and commit-to-ack lag
|
|
18546
|
+
* measured on the database clock). Never affects delivery.
|
|
18547
|
+
*/
|
|
18548
|
+
onDelivered?: (deliveries: readonly RunLedgerOutboxDeliveredRow[]) => void;
|
|
18549
|
+
/**
|
|
18550
|
+
* Background lease holders (the outbox projector and the worker's
|
|
18551
|
+
* untargeted fast path) set this: after acknowledging a batch, also carry
|
|
18552
|
+
* the run's terminal fact through when it is now pending (see
|
|
18553
|
+
* `continueRunLedgerOutboxThroughTerminal`). Callers that drive their own
|
|
18554
|
+
* bounded batches over a pre-leased prefix (cancellation, inline
|
|
18555
|
+
* `run.created`, the targeted terminal drain) leave it unset.
|
|
18556
|
+
*/
|
|
18557
|
+
continueThroughTerminal?: boolean;
|
|
18558
|
+
};
|
|
18559
|
+
|
|
18560
|
+
/** One acknowledged outbox row, for bounded-cardinality lag metrics. */
|
|
18561
|
+
export type RunLedgerOutboxDeliveredRow = {
|
|
18562
|
+
eventType: string;
|
|
18563
|
+
lagMs: number;
|
|
18184
18564
|
};
|
|
18185
18565
|
|
|
18186
18566
|
export async function dispatchRunLedgerOutboxToLedgerOnce(
|
|
@@ -18267,6 +18647,7 @@ export async function dispatchRunLedgerOutboxToLedgerThroughEventWithClientScope
|
|
|
18267
18647
|
{
|
|
18268
18648
|
...input,
|
|
18269
18649
|
waitForRunLock: true,
|
|
18650
|
+
continueThroughTerminal: false,
|
|
18270
18651
|
limit:
|
|
18271
18652
|
remaining === null ? batchSize : Math.min(batchSize, remaining),
|
|
18272
18653
|
},
|
|
@@ -18400,6 +18781,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18400
18781
|
JOIN ${tableName('runs', options)} AS runs ON runs.run_id = head.run_id
|
|
18401
18782
|
WHERE head.failed_at IS NULL
|
|
18402
18783
|
AND coalesce(head.next_attempt_at, head.created_at) <= now()
|
|
18784
|
+
AND ($3::text IS NULL OR head.event_id = $3::text)
|
|
18403
18785
|
AND NOT EXISTS (
|
|
18404
18786
|
SELECT 1 FROM ${tableName('outbox', options)} AS active
|
|
18405
18787
|
WHERE active.run_id = runs.run_id
|
|
@@ -18410,7 +18792,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18410
18792
|
)
|
|
18411
18793
|
FOR UPDATE OF runs SKIP LOCKED
|
|
18412
18794
|
`,
|
|
18413
|
-
[targetRunId, leaseOwner],
|
|
18795
|
+
[targetRunId, leaseOwner, input.headEventId?.trim() || null],
|
|
18414
18796
|
)
|
|
18415
18797
|
: await client.query<{ run_id: string }>(`
|
|
18416
18798
|
WITH heads AS MATERIALIZED (
|
|
@@ -18437,10 +18819,18 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18437
18819
|
AND active.lease_owner IS NOT NULL
|
|
18438
18820
|
AND active.lease_expires_at > now()
|
|
18439
18821
|
)
|
|
18822
|
+
-- admission_first: admission, then a run whose head is its
|
|
18823
|
+
-- terminal (its last fact; see
|
|
18824
|
+
-- readPendingPostgresSchedulerOutboxEvents), then the FIFO
|
|
18825
|
+
-- backlog. oldest_ready: pure age order, the anti-starvation turn.
|
|
18440
18826
|
ORDER BY ${
|
|
18441
18827
|
input.headPriority === 'oldest_ready'
|
|
18442
18828
|
? ''
|
|
18443
|
-
:
|
|
18829
|
+
: `CASE
|
|
18830
|
+
WHEN head.event_type = 'run.created' THEN 0
|
|
18831
|
+
WHEN head.event_id LIKE 'run-terminal:%' THEN 1
|
|
18832
|
+
ELSE 2
|
|
18833
|
+
END,`
|
|
18444
18834
|
}
|
|
18445
18835
|
head.created_at, runs.run_id
|
|
18446
18836
|
FOR UPDATE OF runs SKIP LOCKED LIMIT 1
|
|
@@ -18531,6 +18921,8 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18531
18921
|
}
|
|
18532
18922
|
: {}),
|
|
18533
18923
|
retryPolicy: 'none' as const,
|
|
18924
|
+
// The caller-owned admission deadline (see `signal` above) wins.
|
|
18925
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
18534
18926
|
};
|
|
18535
18927
|
const renewDelivery = () =>
|
|
18536
18928
|
withClient((client) =>
|
|
@@ -18598,14 +18990,28 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18598
18990
|
});
|
|
18599
18991
|
}
|
|
18600
18992
|
}
|
|
18601
|
-
const
|
|
18602
|
-
|
|
18993
|
+
const deliveredRows = await withClient((client) =>
|
|
18994
|
+
markPostgresSchedulerOutboxDeliveredRows(
|
|
18603
18995
|
client,
|
|
18604
18996
|
events.map((event) => event.eventId),
|
|
18605
18997
|
{ ...options, leaseOwner },
|
|
18606
18998
|
),
|
|
18607
18999
|
);
|
|
18608
|
-
|
|
19000
|
+
const markedDelivered = deliveredRows.length;
|
|
19001
|
+
if (input.onDelivered && deliveredRows.length > 0) {
|
|
19002
|
+
try {
|
|
19003
|
+
input.onDelivered(deliveredRows);
|
|
19004
|
+
} catch (observerError) {
|
|
19005
|
+
// Metrics are observation only; they never change a settled delivery.
|
|
19006
|
+
console.warn('[run-ledger-outbox.delivery_observer_failed]', {
|
|
19007
|
+
error:
|
|
19008
|
+
observerError instanceof Error
|
|
19009
|
+
? observerError.message
|
|
19010
|
+
: String(observerError),
|
|
19011
|
+
});
|
|
19012
|
+
}
|
|
19013
|
+
}
|
|
19014
|
+
const delivered: PostgresSchedulerOutboxProjectionResult = {
|
|
18609
19015
|
attempted: events.length,
|
|
18610
19016
|
projected: markedDelivered,
|
|
18611
19017
|
eventIds:
|
|
@@ -18617,7 +19023,42 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18617
19023
|
retrying: 0,
|
|
18618
19024
|
retryingEventIds: [],
|
|
18619
19025
|
};
|
|
19026
|
+
if (
|
|
19027
|
+
input.continueThroughTerminal !== true ||
|
|
19028
|
+
input.headEventId ||
|
|
19029
|
+
markedDelivered !== events.length
|
|
19030
|
+
) {
|
|
19031
|
+
return delivered;
|
|
19032
|
+
}
|
|
19033
|
+
return await continueRunLedgerOutboxThroughTerminal(
|
|
19034
|
+
withClient,
|
|
19035
|
+
{ input, leaseOwner, runId: first.runId, delivered, events },
|
|
19036
|
+
options,
|
|
19037
|
+
);
|
|
18620
19038
|
} catch (error) {
|
|
19039
|
+
if (input.signal?.aborted) {
|
|
19040
|
+
// The caller's latency budget expired; this is not a delivery failure.
|
|
19041
|
+
// Release the lease (fenced to this owner) without charging a
|
|
19042
|
+
// projection attempt or scheduling backoff, and wake the durable
|
|
19043
|
+
// projector. If the append already reached the ledger, the projector's
|
|
19044
|
+
// replay carries the same idempotency key and is a no-op there.
|
|
19045
|
+
const released = await withClient((client) =>
|
|
19046
|
+
releasePostgresSchedulerOutboxLease(
|
|
19047
|
+
client,
|
|
19048
|
+
events.map((event) => event.eventId),
|
|
19049
|
+
{ ...options, leaseOwner },
|
|
19050
|
+
),
|
|
19051
|
+
);
|
|
19052
|
+
return {
|
|
19053
|
+
attempted: events.length,
|
|
19054
|
+
projected: 0,
|
|
19055
|
+
eventIds: [],
|
|
19056
|
+
failed: 0,
|
|
19057
|
+
failedEventIds: [],
|
|
19058
|
+
retrying: released.length,
|
|
19059
|
+
retryingEventIds: released,
|
|
19060
|
+
};
|
|
19061
|
+
}
|
|
18621
19062
|
const settlement = await withClient(async (client) => {
|
|
18622
19063
|
const blocked: string[] = [];
|
|
18623
19064
|
const retrying: string[] = [];
|
|
@@ -18650,6 +19091,68 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
|
18650
19091
|
}
|
|
18651
19092
|
}
|
|
18652
19093
|
|
|
19094
|
+
/**
|
|
19095
|
+
* Whoever holds a run's outbox lease delivers its terminal fact.
|
|
19096
|
+
*
|
|
19097
|
+
* A worker commits a run's terminal and immediately asks to deliver it
|
|
19098
|
+
* (`run-terminal:<runId>`). When another dispatcher (normally the projector)
|
|
19099
|
+
* is mid-append on an earlier event of the same run, that run-scoped claim
|
|
19100
|
+
* finds the run leased and correctly claims nothing, and the terminal then
|
|
19101
|
+
* waited for the projector's next wave: seconds of a finished run still
|
|
19102
|
+
* reading "running" (live-run-observation-e2e, 4.2 s). So the lease holder,
|
|
19103
|
+
* having acknowledged its batch, carries the run's terminal fact through in
|
|
19104
|
+
* the same turn when it is now pending. The terminal is the run's last fact,
|
|
19105
|
+
* so this is one finite causal drain (the same leased, idempotent
|
|
19106
|
+
* `dispatchRunLedgerOutboxToLedgerThroughEvent` the worker uses), not a loop;
|
|
19107
|
+
* a run without a pending terminal costs one indexed read.
|
|
19108
|
+
*/
|
|
19109
|
+
async function continueRunLedgerOutboxThroughTerminal(
|
|
19110
|
+
withClient: <T>(
|
|
19111
|
+
operation: (client: PostgresSchedulerQueryClient) => Promise<T>,
|
|
19112
|
+
) => Promise<T>,
|
|
19113
|
+
input: {
|
|
19114
|
+
input: RunLedgerOutboxDispatchInput & { waitForRunLock?: boolean };
|
|
19115
|
+
leaseOwner: string;
|
|
19116
|
+
runId: string;
|
|
19117
|
+
delivered: PostgresSchedulerOutboxProjectionResult;
|
|
19118
|
+
events: readonly PostgresSchedulerOutboxEvent[];
|
|
19119
|
+
},
|
|
19120
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
19121
|
+
): Promise<PostgresSchedulerOutboxProjectionResult> {
|
|
19122
|
+
const terminalEventId = `run-terminal:${input.runId}`;
|
|
19123
|
+
if (input.events.some((event) => event.eventId === terminalEventId)) {
|
|
19124
|
+
return input.delivered;
|
|
19125
|
+
}
|
|
19126
|
+
const terminal = await withClient((client) =>
|
|
19127
|
+
readPostgresSchedulerOutboxDeliveryStatus(
|
|
19128
|
+
client,
|
|
19129
|
+
{ runId: input.runId, eventId: terminalEventId },
|
|
19130
|
+
{ ...options, includeBlocked: false },
|
|
19131
|
+
),
|
|
19132
|
+
);
|
|
19133
|
+
if (!terminal.exists || terminal.delivered) return input.delivered;
|
|
19134
|
+
const drained =
|
|
19135
|
+
await dispatchRunLedgerOutboxToLedgerThroughEventWithClientScope(
|
|
19136
|
+
withClient,
|
|
19137
|
+
{
|
|
19138
|
+
...input.input,
|
|
19139
|
+
runId: input.runId,
|
|
19140
|
+
leaseOwner: input.leaseOwner,
|
|
19141
|
+
targetEventId: terminalEventId,
|
|
19142
|
+
},
|
|
19143
|
+
options,
|
|
19144
|
+
);
|
|
19145
|
+
return {
|
|
19146
|
+
attempted: input.delivered.attempted + drained.attempted,
|
|
19147
|
+
projected: input.delivered.projected + drained.projected,
|
|
19148
|
+
eventIds: [...input.delivered.eventIds, ...drained.eventIds],
|
|
19149
|
+
failed: drained.failed,
|
|
19150
|
+
failedEventIds: drained.failedEventIds,
|
|
19151
|
+
retrying: drained.retrying,
|
|
19152
|
+
retryingEventIds: drained.retryingEventIds,
|
|
19153
|
+
};
|
|
19154
|
+
}
|
|
19155
|
+
|
|
18653
19156
|
async function projectPostgresSchedulerCreatedEvent(
|
|
18654
19157
|
runtimeApi: WorkerRuntimeApiContext,
|
|
18655
19158
|
event: PostgresSchedulerOutboxEvent,
|
|
@@ -18660,6 +19163,7 @@ async function projectPostgresSchedulerCreatedEvent(
|
|
|
18660
19163
|
const raw = (key: string) => payloadField(event.payload, key);
|
|
18661
19164
|
const definitionScope = value('definitionScope');
|
|
18662
19165
|
const triggerSource = value('triggerSource');
|
|
19166
|
+
const launchSurface = readPlayLaunchSurface(value('launchSurface'));
|
|
18663
19167
|
const secretRefs = raw('secretRefs');
|
|
18664
19168
|
const inputSummary = raw('inputSummary');
|
|
18665
19169
|
const inputBytes = number('inputBytes');
|
|
@@ -18678,6 +19182,9 @@ async function projectPostgresSchedulerCreatedEvent(
|
|
|
18678
19182
|
workflowId: event.runId,
|
|
18679
19183
|
runId: event.runId,
|
|
18680
19184
|
workflowFamilyKey: value('workflowFamilyKey') ?? event.runId,
|
|
19185
|
+
// Written in the admission transaction, so this is when the run was
|
|
19186
|
+
// accepted, not when the projection happened to deliver it.
|
|
19187
|
+
acceptedAt: event.createdAt.getTime(),
|
|
18681
19188
|
...(value('parentRunId') ? { parentRunId: value('parentRunId') } : {}),
|
|
18682
19189
|
...(value('rootRunId') ? { rootRunId: value('rootRunId') } : {}),
|
|
18683
19190
|
...(value('asyncLaunchKey')
|
|
@@ -18729,6 +19236,7 @@ async function projectPostgresSchedulerCreatedEvent(
|
|
|
18729
19236
|
triggerSource === 'sql_listener'
|
|
18730
19237
|
? { triggerSource }
|
|
18731
19238
|
: {}),
|
|
19239
|
+
...(launchSurface ? { launchSurface } : {}),
|
|
18732
19240
|
...(inputFileId ? { inputFileId } : {}),
|
|
18733
19241
|
...(inputBytes !== null ? { inputBytes } : {}),
|
|
18734
19242
|
...(inputSha256 ? { inputSha256 } : {}),
|