deepline 0.3.90 → 0.3.92
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 +101 -6
- package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
- package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +7 -12
- package/dist/bundling-sources/sdk/src/types.ts +6 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
- package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
- package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +48 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
- package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
- package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
- package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
- package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
- package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
- package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
- package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
- package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
- package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
- package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
- package/dist/cli/index.js +369 -162
- package/dist/cli/index.mjs +369 -162
- package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
- package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
- package/dist/index.d.mts +45 -8
- package/dist/index.d.ts +45 -8
- package/dist/index.js +61 -14
- package/dist/index.mjs +61 -14
- package/dist/install-integrity.json +54 -2
- package/dist/monitor-fleet-contract.d.mts +3 -10
- package/dist/monitor-fleet-contract.d.ts +3 -10
- package/dist/monitor-fleet-contract.js +2 -0
- package/dist/monitor-fleet-contract.mjs +1 -1
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +3 -3
- package/dist/release.d.ts +3 -3
- package/dist/release.js +7 -12
- package/dist/release.mjs +7 -12
- package/package.json +2 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import type { PlayRunnerEvent } from '../protocol';
|
|
2
|
+
import {
|
|
3
|
+
RUNTIME_RUNNER_TERMINAL_PROGRESS_MAX_EVENTS,
|
|
4
|
+
} from '../runtime-contract';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Push-execution live progress channel (runs INSIDE the Daytona sandbox).
|
|
8
|
+
*
|
|
9
|
+
* The babysitting model streamed progress by having the WORKER poll a progress
|
|
10
|
+
* JSONL sidecar file out of the sandbox every second. Push execution deletes
|
|
11
|
+
* that file poll: the runner instead PUSHES the same progress events to the
|
|
12
|
+
* receipt gateway's `runner_progress_events` action on a small batched cadence.
|
|
13
|
+
* The gateway folds them into the durable run-log/progress path (the same
|
|
14
|
+
* postgres-progress reporter the worker used), so live progress survives without
|
|
15
|
+
* per-second sandbox filesystem reads.
|
|
16
|
+
*
|
|
17
|
+
* Progress is best-effort observability: the authoritative terminal result still
|
|
18
|
+
* flows through the runner's stdout `result` event and the gateway's receipt
|
|
19
|
+
* terminal. A push failure must therefore never fail the play — it is logged and
|
|
20
|
+
* dropped.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const RUNNER_PROGRESS_PUSH_BATCH_MS = 1_000;
|
|
24
|
+
export const RUNNER_PROGRESS_PUSH_MAX_BATCH = 500;
|
|
25
|
+
/**
|
|
26
|
+
* Progress is best-effort. Never let a stalled observability upload consume the
|
|
27
|
+
* runner's terminal-settlement grace or delay the authoritative result record.
|
|
28
|
+
*/
|
|
29
|
+
export const RUNNER_PROGRESS_PUSH_STOP_TIMEOUT_MS = 5_000;
|
|
30
|
+
|
|
31
|
+
const APP_RUNTIME_COMPAT_PATH = '/api/v2/plays/runtime';
|
|
32
|
+
|
|
33
|
+
export interface RunnerProgressPusher {
|
|
34
|
+
/** Enqueue one progress event for the next batch flush. */
|
|
35
|
+
enqueue: (event: PlayRunnerEvent) => void;
|
|
36
|
+
/** Stop delivery and return all observations not known to be delivered. */
|
|
37
|
+
sealForTerminal: () => {
|
|
38
|
+
events: PlayRunnerEvent[];
|
|
39
|
+
liveLogOffset: number;
|
|
40
|
+
};
|
|
41
|
+
/** Flush any pending events and stop the batch timer. */
|
|
42
|
+
stop: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunnerProgressPushOptions {
|
|
46
|
+
gatewayBaseUrl: string;
|
|
47
|
+
executorToken: string;
|
|
48
|
+
runId: string;
|
|
49
|
+
playName: string;
|
|
50
|
+
/** Injectable for tests. Defaults to a bearer POST to the gateway. */
|
|
51
|
+
sendBatch?: (input: {
|
|
52
|
+
gatewayBaseUrl: string;
|
|
53
|
+
executorToken: string;
|
|
54
|
+
runId: string;
|
|
55
|
+
playName: string;
|
|
56
|
+
events: PlayRunnerEvent[];
|
|
57
|
+
liveLogOffset: number;
|
|
58
|
+
signal: AbortSignal;
|
|
59
|
+
}) => Promise<void>;
|
|
60
|
+
setIntervalImpl?: (fn: () => void, ms: number) => { unref?: () => void };
|
|
61
|
+
clearIntervalImpl?: (handle: unknown) => void;
|
|
62
|
+
warn?: (message: string, detail: Record<string, unknown>) => void;
|
|
63
|
+
batchMs?: number;
|
|
64
|
+
maxBatch?: number;
|
|
65
|
+
stopTimeoutMs?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function defaultSendBatch(input: {
|
|
69
|
+
gatewayBaseUrl: string;
|
|
70
|
+
executorToken: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
playName: string;
|
|
73
|
+
events: PlayRunnerEvent[];
|
|
74
|
+
liveLogOffset: number;
|
|
75
|
+
signal: AbortSignal;
|
|
76
|
+
}): Promise<void> {
|
|
77
|
+
const url = `${input.gatewayBaseUrl.replace(/\/$/, '')}${APP_RUNTIME_COMPAT_PATH}`;
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'content-type': 'application/json',
|
|
82
|
+
authorization: `Bearer ${input.executorToken}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
action: 'runner_progress_events',
|
|
86
|
+
runId: input.runId,
|
|
87
|
+
playName: input.playName,
|
|
88
|
+
events: input.events,
|
|
89
|
+
liveLogOffset: input.liveLogOffset,
|
|
90
|
+
}),
|
|
91
|
+
signal: input.signal,
|
|
92
|
+
redirect: 'manual',
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await response.text().catch(() => '');
|
|
96
|
+
throw new Error(
|
|
97
|
+
`runner_progress_events failed (${response.status}): ${body}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await response.text().catch(() => '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type ProgressBatch = {
|
|
104
|
+
events: PlayRunnerEvent[];
|
|
105
|
+
/** Number of durable play-authored log lines before this batch. */
|
|
106
|
+
liveLogOffset: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function durableLogCount(events: readonly PlayRunnerEvent[]): number {
|
|
110
|
+
return events.reduce(
|
|
111
|
+
(count, event) =>
|
|
112
|
+
event.type === 'log' && event.source !== 'worker' && event.line.length > 0
|
|
113
|
+
? count + 1
|
|
114
|
+
: count,
|
|
115
|
+
0,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createRunnerProgressPusher(
|
|
120
|
+
options: RunnerProgressPushOptions,
|
|
121
|
+
): RunnerProgressPusher {
|
|
122
|
+
const sendBatch = options.sendBatch ?? defaultSendBatch;
|
|
123
|
+
const setIntervalImpl =
|
|
124
|
+
options.setIntervalImpl ??
|
|
125
|
+
((fn, ms) => setInterval(fn, ms) as unknown as { unref?: () => void });
|
|
126
|
+
const clearIntervalImpl =
|
|
127
|
+
options.clearIntervalImpl ??
|
|
128
|
+
((handle) => clearInterval(handle as ReturnType<typeof setInterval>));
|
|
129
|
+
const warn =
|
|
130
|
+
options.warn ??
|
|
131
|
+
((message, detail) =>
|
|
132
|
+
console.warn(`[play-runner.progress-push] ${message}`, detail));
|
|
133
|
+
const batchMs = options.batchMs ?? RUNNER_PROGRESS_PUSH_BATCH_MS;
|
|
134
|
+
const maxBatch = options.maxBatch ?? RUNNER_PROGRESS_PUSH_MAX_BATCH;
|
|
135
|
+
const stopTimeoutMs =
|
|
136
|
+
options.stopTimeoutMs ?? RUNNER_PROGRESS_PUSH_STOP_TIMEOUT_MS;
|
|
137
|
+
|
|
138
|
+
const pending: PlayRunnerEvent[] = [];
|
|
139
|
+
const queuedBatches: ProgressBatch[] = [];
|
|
140
|
+
// A gateway acknowledgement is the durability boundary. Preserve any batch
|
|
141
|
+
// that failed before that boundary so the terminal request can
|
|
142
|
+
// carry it to whichever healthy replica Fly selects next.
|
|
143
|
+
const failedBatches: ProgressBatch[] = [];
|
|
144
|
+
const activeBatches = new Map<AbortController, ProgressBatch>();
|
|
145
|
+
let nextBatchLiveLogOffset = 0;
|
|
146
|
+
let deliveryBroken = false;
|
|
147
|
+
let stopped = false;
|
|
148
|
+
let abandoned = false;
|
|
149
|
+
let suppressWarnings = false;
|
|
150
|
+
let inFlight: Promise<void> = Promise.resolve();
|
|
151
|
+
let handle: { unref?: () => void } | null = null;
|
|
152
|
+
|
|
153
|
+
const flush = (): Promise<void> => {
|
|
154
|
+
if (abandoned) {
|
|
155
|
+
pending.length = 0;
|
|
156
|
+
return inFlight;
|
|
157
|
+
}
|
|
158
|
+
if (pending.length === 0 || deliveryBroken) return inFlight;
|
|
159
|
+
const events = pending.splice(0, Math.min(pending.length, maxBatch));
|
|
160
|
+
const batch: ProgressBatch = {
|
|
161
|
+
events,
|
|
162
|
+
liveLogOffset: nextBatchLiveLogOffset,
|
|
163
|
+
};
|
|
164
|
+
nextBatchLiveLogOffset += durableLogCount(events);
|
|
165
|
+
queuedBatches.push(batch);
|
|
166
|
+
inFlight = inFlight
|
|
167
|
+
.catch(() => {})
|
|
168
|
+
.then(async () => {
|
|
169
|
+
// stop() can time out while this batch waits behind an earlier upload.
|
|
170
|
+
// Do not start it after the authoritative terminal has been emitted.
|
|
171
|
+
if (abandoned) return;
|
|
172
|
+
if (deliveryBroken) {
|
|
173
|
+
const queuedIndex = queuedBatches.indexOf(batch);
|
|
174
|
+
if (queuedIndex >= 0) queuedBatches.splice(queuedIndex, 1);
|
|
175
|
+
failedBatches.push(batch);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const queuedIndex = queuedBatches.indexOf(batch);
|
|
180
|
+
if (queuedIndex >= 0) queuedBatches.splice(queuedIndex, 1);
|
|
181
|
+
activeBatches.set(controller, batch);
|
|
182
|
+
try {
|
|
183
|
+
await sendBatch({
|
|
184
|
+
gatewayBaseUrl: options.gatewayBaseUrl,
|
|
185
|
+
executorToken: options.executorToken,
|
|
186
|
+
runId: options.runId,
|
|
187
|
+
playName: options.playName,
|
|
188
|
+
events: batch.events,
|
|
189
|
+
liveLogOffset: batch.liveLogOffset,
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (!abandoned) {
|
|
194
|
+
failedBatches.push(batch);
|
|
195
|
+
// Offsets are positional. After one unknown delivery outcome, keep
|
|
196
|
+
// the remaining contiguous suffix for the terminal request rather
|
|
197
|
+
// than sending holes that another replica could misinterpret.
|
|
198
|
+
deliveryBroken = true;
|
|
199
|
+
}
|
|
200
|
+
if (!suppressWarnings) {
|
|
201
|
+
warn('batch_failed', {
|
|
202
|
+
runId: options.runId,
|
|
203
|
+
count: batch.events.length,
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
activeBatches.delete(controller);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return inFlight;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
handle = setIntervalImpl(() => {
|
|
215
|
+
void flush();
|
|
216
|
+
}, batchMs);
|
|
217
|
+
handle.unref?.();
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
enqueue(event) {
|
|
221
|
+
if (stopped) return;
|
|
222
|
+
pending.push(event);
|
|
223
|
+
if (pending.length >= maxBatch) {
|
|
224
|
+
void flush();
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
sealForTerminal() {
|
|
228
|
+
if (stopped) return { events: [], liveLogOffset: nextBatchLiveLogOffset };
|
|
229
|
+
stopped = true;
|
|
230
|
+
if (handle) {
|
|
231
|
+
clearIntervalImpl(handle);
|
|
232
|
+
handle = null;
|
|
233
|
+
}
|
|
234
|
+
// Preserve every batch removed from `pending`, including batches queued
|
|
235
|
+
// behind an earlier upload. The terminal request is the final owner of
|
|
236
|
+
// these observations; do not drop a burst merely because it has not yet
|
|
237
|
+
// acquired an AbortController.
|
|
238
|
+
const retainedBatches: ProgressBatch[] = [
|
|
239
|
+
...failedBatches,
|
|
240
|
+
...activeBatches.values(),
|
|
241
|
+
...queuedBatches,
|
|
242
|
+
...(pending.length
|
|
243
|
+
? [{ events: [...pending], liveLogOffset: nextBatchLiveLogOffset }]
|
|
244
|
+
: []),
|
|
245
|
+
];
|
|
246
|
+
const unboundedTail = retainedBatches.flatMap((batch) => batch.events);
|
|
247
|
+
const droppedEvents = Math.max(
|
|
248
|
+
0,
|
|
249
|
+
unboundedTail.length - RUNTIME_RUNNER_TERMINAL_PROGRESS_MAX_EVENTS,
|
|
250
|
+
);
|
|
251
|
+
const droppedPrefix = unboundedTail.slice(0, droppedEvents);
|
|
252
|
+
const tail = unboundedTail.slice(
|
|
253
|
+
-RUNTIME_RUNNER_TERMINAL_PROGRESS_MAX_EVENTS,
|
|
254
|
+
);
|
|
255
|
+
const liveLogOffset =
|
|
256
|
+
(retainedBatches[0]?.liveLogOffset ?? nextBatchLiveLogOffset) +
|
|
257
|
+
durableLogCount(droppedPrefix);
|
|
258
|
+
if (tail.length < unboundedTail.length) {
|
|
259
|
+
warn('terminal_tail_truncated', {
|
|
260
|
+
runId: options.runId,
|
|
261
|
+
retainedEvents: tail.length,
|
|
262
|
+
droppedEvents: unboundedTail.length - tail.length,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
failedBatches.length = 0;
|
|
266
|
+
queuedBatches.length = 0;
|
|
267
|
+
pending.length = 0;
|
|
268
|
+
abandoned = true;
|
|
269
|
+
suppressWarnings = true;
|
|
270
|
+
for (const controller of activeBatches.keys()) controller.abort();
|
|
271
|
+
return { events: tail, liveLogOffset };
|
|
272
|
+
},
|
|
273
|
+
async stop() {
|
|
274
|
+
if (stopped) return;
|
|
275
|
+
stopped = true;
|
|
276
|
+
if (handle) {
|
|
277
|
+
clearIntervalImpl(handle);
|
|
278
|
+
handle = null;
|
|
279
|
+
}
|
|
280
|
+
const drain = async () => {
|
|
281
|
+
// Drain everything still buffered.
|
|
282
|
+
while (pending.length > 0 && !deliveryBroken) {
|
|
283
|
+
await flush();
|
|
284
|
+
}
|
|
285
|
+
await inFlight.catch(() => {});
|
|
286
|
+
if (deliveryBroken) pending.length = 0;
|
|
287
|
+
};
|
|
288
|
+
let stopTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
289
|
+
const outcome = await Promise.race([
|
|
290
|
+
drain().then(() => 'drained' as const),
|
|
291
|
+
new Promise<'timed_out'>((resolve) => {
|
|
292
|
+
stopTimeoutHandle = setTimeout(
|
|
293
|
+
() => resolve('timed_out'),
|
|
294
|
+
stopTimeoutMs,
|
|
295
|
+
);
|
|
296
|
+
}),
|
|
297
|
+
]);
|
|
298
|
+
if (stopTimeoutHandle) clearTimeout(stopTimeoutHandle);
|
|
299
|
+
if (outcome === 'drained') return;
|
|
300
|
+
|
|
301
|
+
// The authoritative terminal follows this method. Abort ordinary fetches
|
|
302
|
+
// and drop non-cooperative test/transport promises without letting their
|
|
303
|
+
// later rejection write a post-terminal diagnostic.
|
|
304
|
+
abandoned = true;
|
|
305
|
+
suppressWarnings = true;
|
|
306
|
+
const pendingEvents = pending.length;
|
|
307
|
+
pending.length = 0;
|
|
308
|
+
const inFlightEvents = [...activeBatches.values()].reduce(
|
|
309
|
+
(sum, batch) => sum + batch.events.length,
|
|
310
|
+
0,
|
|
311
|
+
);
|
|
312
|
+
for (const controller of activeBatches.keys()) controller.abort();
|
|
313
|
+
warn('drain_timed_out', {
|
|
314
|
+
runId: options.runId,
|
|
315
|
+
timeoutMs: stopTimeoutMs,
|
|
316
|
+
pendingEvents,
|
|
317
|
+
inFlightEvents,
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isAppRuntimeApiCapacityError } from '../app-runtime-api';
|
|
2
|
+
import { isRuntimeApiCapacityError } from '../runtime-api';
|
|
3
|
+
|
|
4
|
+
const RUNTIME_SHEET_CONTENTION_MAX_ATTEMPTS = 4;
|
|
5
|
+
|
|
6
|
+
function persistenceCapacityRetryAfterMs(error: unknown): number | null {
|
|
7
|
+
if (isAppRuntimeApiCapacityError(error) || isRuntimeApiCapacityError(error)) {
|
|
8
|
+
return Math.max(0, Math.ceil(error.retryAfterMs));
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function capacityRetryDelayMs(
|
|
14
|
+
retryAfterMs: number,
|
|
15
|
+
random: () => number,
|
|
16
|
+
): number {
|
|
17
|
+
// retryAfterMs is the server's lower bound. Add one-sided jitter so a
|
|
18
|
+
// synchronized cohort spreads out without retrying before capacity is
|
|
19
|
+
// expected to be available.
|
|
20
|
+
const jitter = Math.max(0, Math.min(1, random())) * 0.2 + 1;
|
|
21
|
+
return Math.ceil(retryAfterMs * jitter);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRetryableRuntimePersistenceContention(error: unknown): boolean {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
return /deadlock detected|Runtime Postgres connection timed out|could not serialize access|tuple concurrently updated/i.test(
|
|
27
|
+
message,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
32
|
+
if (!signal?.aborted) return;
|
|
33
|
+
if (signal.reason !== undefined) throw signal.reason;
|
|
34
|
+
const error = new Error('Runtime Sheet persistence retry was aborted.');
|
|
35
|
+
error.name = 'AbortError';
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function abortableSleep(
|
|
40
|
+
ms: number,
|
|
41
|
+
signal: AbortSignal | undefined,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
throwIfAborted(signal);
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
signal?.removeEventListener('abort', onAbort);
|
|
47
|
+
resolve();
|
|
48
|
+
}, ms);
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
signal?.removeEventListener('abort', onAbort);
|
|
52
|
+
try {
|
|
53
|
+
throwIfAborted(signal);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function retryRuntimeSheetPersistence<T>(input: {
|
|
63
|
+
description: string;
|
|
64
|
+
log: (line: string) => void;
|
|
65
|
+
run: () => Promise<T>;
|
|
66
|
+
signal: AbortSignal;
|
|
67
|
+
random?: () => number;
|
|
68
|
+
sleep?: (ms: number, signal: AbortSignal | undefined) => Promise<void>;
|
|
69
|
+
}): Promise<T> {
|
|
70
|
+
const random = input.random ?? Math.random;
|
|
71
|
+
const sleep = input.sleep ?? abortableSleep;
|
|
72
|
+
let contentionAttempt = 0;
|
|
73
|
+
let capacityAttempt = 0;
|
|
74
|
+
|
|
75
|
+
for (;;) {
|
|
76
|
+
throwIfAborted(input.signal);
|
|
77
|
+
try {
|
|
78
|
+
return await input.run();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throwIfAborted(input.signal);
|
|
81
|
+
const retryAfterMs = persistenceCapacityRetryAfterMs(error);
|
|
82
|
+
if (retryAfterMs !== null) {
|
|
83
|
+
capacityAttempt += 1;
|
|
84
|
+
const delayMs = capacityRetryDelayMs(retryAfterMs, random);
|
|
85
|
+
input.log(
|
|
86
|
+
`[warn] ${input.description} waiting for persistence capacity attempt=${capacityAttempt} delay_ms=${delayMs}: ${
|
|
87
|
+
error instanceof Error ? error.message : String(error)
|
|
88
|
+
}`,
|
|
89
|
+
);
|
|
90
|
+
await sleep(delayMs, input.signal);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
contentionAttempt += 1;
|
|
95
|
+
if (
|
|
96
|
+
contentionAttempt >= RUNTIME_SHEET_CONTENTION_MAX_ATTEMPTS ||
|
|
97
|
+
!isRetryableRuntimePersistenceContention(error)
|
|
98
|
+
) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
const delayMs = 150 * contentionAttempt;
|
|
102
|
+
input.log(
|
|
103
|
+
`[warn] ${input.description} retryable persistence contention attempt=${contentionAttempt}/${RUNTIME_SHEET_CONTENTION_MAX_ATTEMPTS} delay_ms=${delayMs}: ${
|
|
104
|
+
error instanceof Error ? error.message : String(error)
|
|
105
|
+
}`,
|
|
106
|
+
);
|
|
107
|
+
await sleep(delayMs, input.signal);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|