deepline 0.3.17 → 0.3.18
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/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +32 -18
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +254 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +5 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +344 -26
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-reliability-policy.ts +391 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-traffic-policy.ts +125 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +36 -39
- package/dist/cli/index.js +252 -1
- package/dist/cli/index.mjs +252 -1
- package/dist/index.js +252 -1
- package/dist/index.mjs +252 -1
- package/dist/install-integrity.json +4 -0
- package/package.json +1 -1
|
@@ -24,11 +24,92 @@ import type { PlaySandboxRuntimeLimits } from '@shared_libs/play-runtime/sandbox
|
|
|
24
24
|
import { validateDaytonaExecutionContext } from './daytona-lifecycle';
|
|
25
25
|
import { stageRunnerPayload } from './daytona-payload-transport';
|
|
26
26
|
import { captureDetachedDaytonaRunnerReadinessBaseline } from './daytona-session-execution';
|
|
27
|
+
import {
|
|
28
|
+
isPlayRunRecoveryError,
|
|
29
|
+
recoveryForOpenModalCircuit,
|
|
30
|
+
recoveryForModalSandboxCreateFailure,
|
|
31
|
+
} from '@shared_libs/play-runtime/play-run-recovery-policy';
|
|
32
|
+
import { shouldInjectModalSandboxCreateResourceExhausted } from '@shared_libs/play-runtime/test-runtime-seams';
|
|
33
|
+
import { RUNTIME_RELIABILITY_POLICY } from '@shared_libs/play-runtime/runtime-reliability-policy';
|
|
27
34
|
|
|
28
|
-
const MODAL_RUNNER_READY_TIMEOUT_MS =
|
|
35
|
+
const MODAL_RUNNER_READY_TIMEOUT_MS =
|
|
36
|
+
RUNTIME_RELIABILITY_POLICY.sandbox.runnerReadyTimeoutMs;
|
|
37
|
+
const MODAL_SANDBOX_CREATE_TIMEOUT_MS =
|
|
38
|
+
RUNTIME_RELIABILITY_POLICY.sandbox.modalSandboxCreateTimeoutMs;
|
|
29
39
|
const MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const;
|
|
40
|
+
const MODAL_LATE_CREATE_PERSIST_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const;
|
|
30
41
|
type ModalSandbox = Awaited<ReturnType<ModalClient['sandboxes']['create']>>;
|
|
31
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A create RPC that outlives its client deadline is fundamentally ambiguous:
|
|
45
|
+
* Modal may still create the sandbox after the caller stops waiting. It is
|
|
46
|
+
* therefore neither a safe fresh retry nor a capacity rejection. The caller
|
|
47
|
+
* must terminalize/reconcile it and retain any local capacity reservation
|
|
48
|
+
* until a later provider-confirmed cleanup can release it.
|
|
49
|
+
*/
|
|
50
|
+
export class ModalSandboxCreateOutcomeUnknownError extends Error {
|
|
51
|
+
constructor(readonly timeoutMs: number) {
|
|
52
|
+
super(
|
|
53
|
+
`RUNTIME_MODAL_SANDBOX_CREATE_OUTCOME_UNKNOWN: Modal SandboxCreate did not settle within ${timeoutMs}ms. ` +
|
|
54
|
+
'No fresh sandbox retry is safe because the provider may still create the requested sandbox.',
|
|
55
|
+
);
|
|
56
|
+
this.name = 'ModalSandboxCreateOutcomeUnknownError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function awaitModalSandboxCreate<T>(input: {
|
|
61
|
+
create: Promise<T>;
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
onLateSuccess?: (value: T) => Promise<void> | void;
|
|
64
|
+
onLateFailure?: (error: unknown) => Promise<void> | void;
|
|
65
|
+
}): Promise<T> {
|
|
66
|
+
const timeoutMs = input.timeoutMs ?? MODAL_SANDBOX_CREATE_TIMEOUT_MS;
|
|
67
|
+
let settled = false;
|
|
68
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
69
|
+
return new Promise<T>((resolve, reject) => {
|
|
70
|
+
timer = setTimeout(() => {
|
|
71
|
+
settled = true;
|
|
72
|
+
reject(new ModalSandboxCreateOutcomeUnknownError(timeoutMs));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
void input.create.then(
|
|
75
|
+
(value) => {
|
|
76
|
+
if (!settled) {
|
|
77
|
+
settled = true;
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
resolve(value);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void Promise.resolve(input.onLateSuccess?.(value)).catch((error) => {
|
|
83
|
+
console.error('[play-runner.modal] late_create_cleanup_failed', {
|
|
84
|
+
error: error instanceof Error ? error.message : String(error),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
(error) => {
|
|
89
|
+
if (!settled) {
|
|
90
|
+
settled = true;
|
|
91
|
+
if (timer) clearTimeout(timer);
|
|
92
|
+
reject(error);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
void Promise.resolve(input.onLateFailure?.(error)).catch(
|
|
96
|
+
(callbackError) => {
|
|
97
|
+
console.error(
|
|
98
|
+
'[play-runner.modal] late_create_failure_cleanup_failed',
|
|
99
|
+
{
|
|
100
|
+
error:
|
|
101
|
+
callbackError instanceof Error
|
|
102
|
+
? callbackError.message
|
|
103
|
+
: String(callbackError),
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
32
113
|
export function modalSandboxLifetimeMs(
|
|
33
114
|
limits: PlaySandboxRuntimeLimits,
|
|
34
115
|
): number {
|
|
@@ -137,6 +218,37 @@ async function releaseModalSandboxCapacityAfterConfirmedTermination(input: {
|
|
|
137
218
|
throw lastError;
|
|
138
219
|
}
|
|
139
220
|
|
|
221
|
+
/**
|
|
222
|
+
* A create that settles after its caller timed out is already an external
|
|
223
|
+
* fact. Persist it before attempting any further provider operation: the
|
|
224
|
+
* durable cleanup controller then owns deletion, capacity release, and the
|
|
225
|
+
* late billing adjustment. A short retry absorbs a transient scheduler-store
|
|
226
|
+
* outage without turning this into a fresh Play attempt.
|
|
227
|
+
*/
|
|
228
|
+
async function persistLateModalSandboxResource(input: {
|
|
229
|
+
resource: Parameters<
|
|
230
|
+
NonNullable<PlayRunnerCallbacks['onLateRuntimeResourceAcquired']>
|
|
231
|
+
>[0];
|
|
232
|
+
record: NonNullable<PlayRunnerCallbacks['onLateRuntimeResourceAcquired']>;
|
|
233
|
+
}): Promise<void> {
|
|
234
|
+
let lastError: unknown;
|
|
235
|
+
for (const delayMs of MODAL_LATE_CREATE_PERSIST_RETRY_DELAYS_MS) {
|
|
236
|
+
if (delayMs > 0) {
|
|
237
|
+
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await input.record(input.resource);
|
|
241
|
+
return;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
lastError = error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new AggregateError(
|
|
247
|
+
[lastError],
|
|
248
|
+
'RUNTIME_MODAL_LATE_CREATE_PERSIST_FAILED: Modal created a sandbox after the caller deadline, but its durable cleanup obligation could not be recorded.',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
140
252
|
export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
141
253
|
async execute(config, callbacks) {
|
|
142
254
|
let runtimeResourceRegistrationError: unknown;
|
|
@@ -160,6 +272,11 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
160
272
|
const startedAt = Date.now();
|
|
161
273
|
const runtimeTiming: PlayRunnerRuntimeTiming = { backend: 'modal' };
|
|
162
274
|
emitModalStage(config.context, 'create:start');
|
|
275
|
+
const providerCircuitClaim =
|
|
276
|
+
await callbacks?.shouldAttemptSandboxProvider?.('modal');
|
|
277
|
+
if (providerCircuitClaim && !providerCircuitClaim.allowed) {
|
|
278
|
+
throw recoveryForOpenModalCircuit();
|
|
279
|
+
}
|
|
163
280
|
const modalConfig = await loadModalRequiredConfig({
|
|
164
281
|
runtimeSchedulerSchema: config.context.runtimeSchedulerSchema,
|
|
165
282
|
limits: config.context.sandboxRuntimeLimits,
|
|
@@ -170,33 +287,157 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
170
287
|
const image = modalConfig.client.images.fromRegistry(modalConfig.image);
|
|
171
288
|
const capacityReservation =
|
|
172
289
|
await callbacks?.reserveSandboxCapacity?.('modal');
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
290
|
+
const createTags = {
|
|
291
|
+
source: 'deepline-play-runner',
|
|
292
|
+
orgId: config.context.orgId ?? 'unknown',
|
|
293
|
+
workflowId: config.context.workflowId ?? 'unknown',
|
|
294
|
+
runId: config.context.runId ?? 'unknown',
|
|
295
|
+
attempt: String(config.context.runAttempt ?? 0),
|
|
296
|
+
};
|
|
297
|
+
let sandbox: ModalSandbox;
|
|
298
|
+
try {
|
|
299
|
+
// This is deliberately adjacent to the real provider boundary. The
|
|
300
|
+
// reservation proves the scheduler observes the same no-resource
|
|
301
|
+
// cleanup path as a genuine Modal rejection; a later persisted
|
|
302
|
+
// attempt calls the real Modal SDK normally.
|
|
303
|
+
if (
|
|
304
|
+
shouldInjectModalSandboxCreateResourceExhausted({
|
|
305
|
+
runtimeTestFaultHeader: config.context.runtimeTestFaultHeader,
|
|
306
|
+
runAttempt: config.context.runAttempt,
|
|
307
|
+
})
|
|
308
|
+
) {
|
|
309
|
+
throw Object.assign(
|
|
310
|
+
new Error(
|
|
311
|
+
'SandboxCreate RESOURCE_EXHAUSTED: injected preview runtime test fault',
|
|
312
|
+
),
|
|
313
|
+
{ code: 8, details: 'RESOURCE_EXHAUSTED' },
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const modalCreateRequestedAt = Date.now();
|
|
317
|
+
// A client timeout does not cancel Modal's create RPC. Persist the
|
|
318
|
+
// provider-scoped lookup key before beginning it, so a worker loss or
|
|
319
|
+
// failed late callback leaves an actionable reconciliation obligation
|
|
320
|
+
// rather than an invisible sandbox/capacity leak.
|
|
321
|
+
if (!callbacks?.onAmbiguousModalCreateIntent) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
'RUNTIME_MODAL_CREATE_RECONCILIATION_UNAVAILABLE: Modal create requires a durable ambiguity recorder.',
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
await callbacks.onAmbiguousModalCreateIntent({
|
|
327
|
+
modalAppId: app.appId,
|
|
328
|
+
runtimeEnvironment:
|
|
329
|
+
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
330
|
+
? 'preview'
|
|
331
|
+
: 'production',
|
|
332
|
+
tags: createTags,
|
|
333
|
+
...(capacityReservation
|
|
334
|
+
? { sandboxCapacityLeaseId: capacityReservation.leaseId }
|
|
190
335
|
: {}),
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
336
|
+
});
|
|
337
|
+
sandbox = await awaitModalSandboxCreate({
|
|
338
|
+
create: modalConfig.client.sandboxes.create(app, image, {
|
|
339
|
+
cpu: MODAL_SANDBOX_CPU_CORES,
|
|
340
|
+
cpuLimit: MODAL_SANDBOX_CPU_CORES,
|
|
341
|
+
memoryMiB: MODAL_SANDBOX_MEMORY_MIB,
|
|
342
|
+
memoryLimitMiB: MODAL_SANDBOX_MEMORY_MIB,
|
|
343
|
+
timeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
|
|
344
|
+
idleTimeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
|
|
345
|
+
workdir: modalConfig.workdir,
|
|
346
|
+
tags: createTags,
|
|
347
|
+
...(modalConfig.outboundCidrAllowlist
|
|
348
|
+
? { outboundCidrAllowlist: modalConfig.outboundCidrAllowlist }
|
|
349
|
+
: {}),
|
|
350
|
+
}),
|
|
351
|
+
onLateSuccess: async (lateSandbox) => {
|
|
352
|
+
// Do not directly delete a late sandbox. First store it as a
|
|
353
|
+
// durable cleanup obligation, then let the normal cleanup worker
|
|
354
|
+
// perform provider-confirmed deletion, capacity release, and
|
|
355
|
+
// billing settlement. That remains recoverable if this process
|
|
356
|
+
// exits between any of those operations.
|
|
357
|
+
const record = callbacks?.onLateRuntimeResourceAcquired;
|
|
358
|
+
if (!record) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
'RUNTIME_MODAL_LATE_CREATE_PERSIST_UNAVAILABLE: Modal created a sandbox after the caller deadline, but no durable resource recorder is configured.',
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
await persistLateModalSandboxResource({
|
|
364
|
+
record,
|
|
365
|
+
resource: {
|
|
366
|
+
kind: 'modal_sandbox',
|
|
367
|
+
sandboxId: lateSandbox.sandboxId,
|
|
368
|
+
runtimeEnvironment:
|
|
369
|
+
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
370
|
+
? 'preview'
|
|
371
|
+
: 'production',
|
|
372
|
+
modalAppId: app.appId,
|
|
373
|
+
billingStartedAt: modalCreateRequestedAt,
|
|
374
|
+
lateAcquired: true,
|
|
375
|
+
maxBillingDurationSeconds:
|
|
376
|
+
modalConfig.limits.timeoutSeconds +
|
|
377
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
378
|
+
cpu: MODAL_SANDBOX_CPU_CORES,
|
|
379
|
+
memoryGiB: MODAL_SANDBOX_MEMORY_MIB / 1024,
|
|
380
|
+
diskGiB: 0,
|
|
381
|
+
...(capacityReservation
|
|
382
|
+
? { sandboxCapacityLeaseId: capacityReservation.leaseId }
|
|
383
|
+
: {}),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
onLateFailure: async (lateError) => {
|
|
388
|
+
console.warn('[play-runner.modal] late_create_failed', {
|
|
389
|
+
error:
|
|
390
|
+
lateError instanceof Error
|
|
391
|
+
? lateError.message
|
|
392
|
+
: String(lateError),
|
|
393
|
+
});
|
|
394
|
+
// The provider has now rejected the timed-out create, proving no
|
|
395
|
+
// sandbox exists. Release the provisional lease instead of
|
|
396
|
+
// holding organization capacity until the crash-backstop TTL.
|
|
397
|
+
if (capacityReservation && callbacks?.releaseSandboxCapacity) {
|
|
398
|
+
await releaseModalSandboxCapacityAfterConfirmedTermination({
|
|
399
|
+
leaseId: capacityReservation.leaseId,
|
|
400
|
+
release: callbacks.releaseSandboxCapacity,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// The late rejection is provider-confirmed absence. Do not leave
|
|
404
|
+
// the pre-create selector pending merely because it crossed the
|
|
405
|
+
// caller deadline; otherwise the durable reconciler scans a fact
|
|
406
|
+
// we already know is safe.
|
|
407
|
+
await callbacks?.onAmbiguousModalCreateResolved?.();
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (
|
|
412
|
+
capacityReservation &&
|
|
413
|
+
!(error instanceof ModalSandboxCreateOutcomeUnknownError)
|
|
414
|
+
) {
|
|
415
|
+
await callbacks?.releaseSandboxCapacity?.(
|
|
416
|
+
capacityReservation.leaseId,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
// A settled rejection is known-safe; a timeout deliberately keeps the
|
|
420
|
+
// pre-create reconciliation obligation pending for recovery.
|
|
421
|
+
if (!(error instanceof ModalSandboxCreateOutcomeUnknownError)) {
|
|
422
|
+
try {
|
|
423
|
+
await callbacks?.onAmbiguousModalCreateResolved?.();
|
|
424
|
+
} catch (reconciliationError) {
|
|
425
|
+
console.warn(
|
|
426
|
+
'[play-runner.modal] create_reconciliation_resolve_failed',
|
|
427
|
+
{
|
|
428
|
+
error:
|
|
429
|
+
reconciliationError instanceof Error
|
|
430
|
+
? reconciliationError.message
|
|
431
|
+
: String(reconciliationError),
|
|
432
|
+
},
|
|
196
433
|
);
|
|
197
434
|
}
|
|
198
|
-
|
|
199
|
-
|
|
435
|
+
}
|
|
436
|
+
// A create rejection proves no sandbox and no customer code exist.
|
|
437
|
+
// Preserve that fact for the scheduler rather than converting it
|
|
438
|
+
// into a terminal runner result below.
|
|
439
|
+
throw recoveryForModalSandboxCreateFailure(error) ?? error;
|
|
440
|
+
}
|
|
200
441
|
const billingStartedAt = Date.now();
|
|
201
442
|
runtimeTiming.modalCreateMs = billingStartedAt - startedAt;
|
|
202
443
|
emitModalStage(config.context, 'create:done', {
|
|
@@ -237,6 +478,46 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
237
478
|
? { sandboxCapacityLeaseId: capacityReservation.leaseId }
|
|
238
479
|
: {}),
|
|
239
480
|
});
|
|
481
|
+
// Do not clear the pre-create obligation until the sandbox itself
|
|
482
|
+
// is durable. A process failure between Modal's success response and
|
|
483
|
+
// resource recording must remain reconciliable rather than looking
|
|
484
|
+
// like a known-safe rejection.
|
|
485
|
+
try {
|
|
486
|
+
await callbacks?.onAmbiguousModalCreateResolved?.();
|
|
487
|
+
} catch (reconciliationError) {
|
|
488
|
+
// The resource record is the authority now. Leaving this intent
|
|
489
|
+
// pending is conservative and observable; do not turn a settled
|
|
490
|
+
// resource into a fresh execution path just because the advisory
|
|
491
|
+
// intent cleanup was temporarily unavailable.
|
|
492
|
+
console.warn(
|
|
493
|
+
'[play-runner.modal] create_reconciliation_resolve_failed',
|
|
494
|
+
{
|
|
495
|
+
sandboxId: sandbox.sandboxId,
|
|
496
|
+
error:
|
|
497
|
+
reconciliationError instanceof Error
|
|
498
|
+
? reconciliationError.message
|
|
499
|
+
: String(reconciliationError),
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
// Circuit state is an operational hint, not part of the durable
|
|
504
|
+
// resource transaction. A successful resource record proves this
|
|
505
|
+
// provider path is healthy even if the best-effort circuit close
|
|
506
|
+
// cannot reach the scheduler control plane right now.
|
|
507
|
+
try {
|
|
508
|
+
await callbacks?.markSandboxProviderHealthy?.(
|
|
509
|
+
'modal',
|
|
510
|
+
providerCircuitClaim?.probeToken ?? null,
|
|
511
|
+
);
|
|
512
|
+
} catch (circuitError) {
|
|
513
|
+
console.warn('[play-runner.modal] recovery_circuit_close_failed', {
|
|
514
|
+
sandboxId: sandbox.sandboxId,
|
|
515
|
+
error:
|
|
516
|
+
circuitError instanceof Error
|
|
517
|
+
? circuitError.message
|
|
518
|
+
: String(circuitError),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
240
521
|
} catch (error) {
|
|
241
522
|
runtimeResourceRegistrationError = error;
|
|
242
523
|
throw error;
|
|
@@ -278,6 +559,10 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
278
559
|
cancellationPromise,
|
|
279
560
|
]);
|
|
280
561
|
runtimeTiming.modalExecuteMs = Date.now() - executeStartedAt;
|
|
562
|
+
// A readiness observation after the command is accepted is ambiguous:
|
|
563
|
+
// the detached runner may already have received its durable park and
|
|
564
|
+
// started customer code while this worker cannot read the scheduler.
|
|
565
|
+
// Never translate that ambiguity into a fresh sandbox retry.
|
|
281
566
|
await confirmDetachedModalRunnerReady({
|
|
282
567
|
readiness: readRunnerReadiness,
|
|
283
568
|
baselineHeartbeatAt,
|
|
@@ -372,7 +657,8 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
372
657
|
if (
|
|
373
658
|
error === runtimeResourceRegistrationError ||
|
|
374
659
|
error instanceof RuntimeResourceFenceLostError ||
|
|
375
|
-
isRuntimeSandboxCapacityLimitError(error)
|
|
660
|
+
isRuntimeSandboxCapacityLimitError(error) ||
|
|
661
|
+
isPlayRunRecoveryError(error)
|
|
376
662
|
) {
|
|
377
663
|
throw error;
|
|
378
664
|
}
|
|
@@ -431,6 +717,38 @@ export async function deleteModalSandboxById(input: {
|
|
|
431
717
|
}
|
|
432
718
|
}
|
|
433
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Read the provider inventory using the exact app + tag selector persisted
|
|
722
|
+
* before a Modal create call. This is deliberately a tiny provider adapter:
|
|
723
|
+
* the scheduler owns claim/fencing, resource recording, cleanup, and policy.
|
|
724
|
+
*/
|
|
725
|
+
export async function findModalSandboxIdsByTags(input: {
|
|
726
|
+
modalAppId: string;
|
|
727
|
+
tags: Record<string, string>;
|
|
728
|
+
environment: 'preview' | 'production';
|
|
729
|
+
}): Promise<string[]> {
|
|
730
|
+
const modalAppId = input.modalAppId.trim();
|
|
731
|
+
if (!modalAppId) throw new Error('Modal reconciliation requires modalAppId.');
|
|
732
|
+
const tags = Object.fromEntries(
|
|
733
|
+
Object.entries(input.tags).filter(
|
|
734
|
+
([key, value]) => key.trim() && value.trim(),
|
|
735
|
+
),
|
|
736
|
+
);
|
|
737
|
+
if (Object.keys(tags).length === 0) {
|
|
738
|
+
throw new Error('Modal reconciliation requires non-empty create tags.');
|
|
739
|
+
}
|
|
740
|
+
const { client } = await loadModalClientConfig();
|
|
741
|
+
const sandboxIds: string[] = [];
|
|
742
|
+
for await (const sandbox of client.sandboxes.list({
|
|
743
|
+
appId: modalAppId,
|
|
744
|
+
tags,
|
|
745
|
+
environment: input.environment,
|
|
746
|
+
})) {
|
|
747
|
+
if (sandbox.sandboxId?.trim()) sandboxIds.push(sandbox.sandboxId.trim());
|
|
748
|
+
}
|
|
749
|
+
return [...new Set(sandboxIds)];
|
|
750
|
+
}
|
|
751
|
+
|
|
434
752
|
export async function readDetachedModalRuntimeCompletion(input: {
|
|
435
753
|
sandboxId: string;
|
|
436
754
|
runtimeCompletedPath: string;
|
|
@@ -21,6 +21,13 @@ export type PlayRunnerRuntimeResource = {
|
|
|
21
21
|
modalAppId?: string;
|
|
22
22
|
billingStartedAt: number;
|
|
23
23
|
billingEndedAt?: number | null;
|
|
24
|
+
/**
|
|
25
|
+
* The provider create settled only after the owning scheduler attempt had
|
|
26
|
+
* already timed out. This resource is billed through its own durable
|
|
27
|
+
* adjustment session; it must never be silently folded into an already
|
|
28
|
+
* finalized run session.
|
|
29
|
+
*/
|
|
30
|
+
lateAcquired?: boolean;
|
|
24
31
|
/** Customer liability ceiling for this physical sandbox. */
|
|
25
32
|
maxBillingDurationSeconds?: number | null;
|
|
26
33
|
terminalReason?: RuntimeResourceTerminalReason | null;
|
|
@@ -32,6 +39,19 @@ export type PlayRunnerRuntimeResource = {
|
|
|
32
39
|
sandboxCapacityLeaseId?: string;
|
|
33
40
|
};
|
|
34
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Durable identity for a Modal create that can outlive the caller's timeout.
|
|
44
|
+
* These fields are only provider routing and correlation data; payloads,
|
|
45
|
+
* commands, and credentials never cross this boundary.
|
|
46
|
+
*/
|
|
47
|
+
export type PlayRunnerAmbiguousModalCreateIntent = {
|
|
48
|
+
modalAppId: string;
|
|
49
|
+
runtimeEnvironment: 'preview' | 'production';
|
|
50
|
+
tags: Record<string, string>;
|
|
51
|
+
/** The provisional soft-capacity reservation, if admission is enabled. */
|
|
52
|
+
sandboxCapacityLeaseId?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
35
55
|
export type RuntimeResourceTerminalReason =
|
|
36
56
|
| 'completed'
|
|
37
57
|
| 'suspended'
|
|
@@ -91,12 +111,42 @@ export interface PlayRunnerCallbacks {
|
|
|
91
111
|
onRuntimeResourceAcquired?: (
|
|
92
112
|
resource: PlayRunnerRuntimeResource,
|
|
93
113
|
) => void | Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* A provider create timed out locally, then later produced a sandbox which
|
|
116
|
+
* could not be deleted. Persist it as a cleanup obligation even though the
|
|
117
|
+
* original attempt may already be terminal; it must never be an invisible
|
|
118
|
+
* provider resource.
|
|
119
|
+
*/
|
|
120
|
+
onLateRuntimeResourceAcquired?: (
|
|
121
|
+
resource: PlayRunnerRuntimeResource,
|
|
122
|
+
) => void | Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Record a reconciliation obligation before Modal create begins. It is
|
|
125
|
+
* resolved only after a known rejection or a durably recorded sandbox.
|
|
126
|
+
*/
|
|
127
|
+
onAmbiguousModalCreateIntent?: (
|
|
128
|
+
intent: PlayRunnerAmbiguousModalCreateIntent,
|
|
129
|
+
) => void | Promise<void>;
|
|
130
|
+
onAmbiguousModalCreateResolved?: () => void | Promise<void>;
|
|
94
131
|
/** Reserve one physical-sandbox slot immediately before a provider create. */
|
|
95
132
|
reserveSandboxCapacity?: (
|
|
96
133
|
provider: 'daytona' | 'modal',
|
|
97
134
|
) => Promise<{ leaseId: string }>;
|
|
98
135
|
/** Release an unbound reservation when provider create never produced a resource. */
|
|
99
136
|
releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Scheduler-owned dependency circuit admission. A false result means no
|
|
139
|
+
* provider call is safe to make yet; the backend must return/throw a typed
|
|
140
|
+
* pre-code recovery outcome instead.
|
|
141
|
+
*/
|
|
142
|
+
shouldAttemptSandboxProvider?: (
|
|
143
|
+
provider: 'daytona' | 'modal',
|
|
144
|
+
) => Promise<{ allowed: boolean; probeToken: string | null }>;
|
|
145
|
+
/** A durably recorded resource may close only the probe that owns it. */
|
|
146
|
+
markSandboxProviderHealthy?: (
|
|
147
|
+
provider: 'daytona' | 'modal',
|
|
148
|
+
probeToken: string | null,
|
|
149
|
+
) => Promise<void>;
|
|
100
150
|
/**
|
|
101
151
|
* Durable scheduler-owned evidence for a provider create call. Backends
|
|
102
152
|
* await this callback at the create boundary; it is not best-effort logging.
|
|
@@ -105,8 +105,8 @@ import {
|
|
|
105
105
|
import {
|
|
106
106
|
parseRuntimeTestFaultCounts,
|
|
107
107
|
PLAY_RUNTIME_TEST_FAULT_HEADER,
|
|
108
|
-
type RuntimeTestFaultName,
|
|
109
108
|
} from './test-runtime-seams';
|
|
109
|
+
import type { RuntimeTestFaultName } from './runtime-incident-drills';
|
|
110
110
|
import { COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID } from './durable-receipt-execution';
|
|
111
111
|
import { vercelProtectionBypassHeader } from './vercel-protection';
|
|
112
112
|
import {
|