import type { ModalClient } from 'modal'; import type { PlayRunnerBackend, PlayRunnerCallbacks } from '../types'; import { isRuntimeSandboxCapacityLimitError, RuntimeResourceFenceLostError, } from '../types'; import { buildPlayRunnerBundle } from '../bundle'; import type { PlayRunnerExecutionConfig, PlayRunnerResult, PlayRunnerRuntimeTiming, } from '@shared_libs/play-runtime/protocol'; import { PLAY_RUNNER_STARTUP_GRACE_SECONDS, PLAY_RUNNER_TERMINAL_GRACE_SECONDS, } from '@shared_libs/play-runtime/runtime-constants'; import { MODAL_SANDBOX_CPU_CORES, MODAL_SANDBOX_MEMORY_MIB, loadModalClientConfig, loadModalRequiredConfig, } from '@shared_libs/play-runtime/modal-runtime-config'; import type { PlaySandboxRuntimeLimits } from '@shared_libs/play-runtime/sandbox-runtime-limits'; import { validateDaytonaExecutionContext } from './daytona-lifecycle'; import { stageRunnerPayload } from './daytona-payload-transport'; import { captureDetachedDaytonaRunnerReadinessBaseline } from './daytona-session-execution'; import { isPlayRunRecoveryError, recoveryForOpenModalCircuit, recoveryForModalSandboxCreateFailure, } from '@shared_libs/play-runtime/play-run-recovery-policy'; import { shouldInjectModalSandboxCreateResourceExhausted } from '@shared_libs/play-runtime/test-runtime-seams'; import { RUNTIME_RELIABILITY_POLICY } from '@shared_libs/play-runtime/runtime-reliability-policy'; const MODAL_RUNNER_READY_TIMEOUT_MS = RUNTIME_RELIABILITY_POLICY.sandbox.runnerReadyTimeoutMs; const MODAL_SANDBOX_CREATE_TIMEOUT_MS = RUNTIME_RELIABILITY_POLICY.sandbox.modalSandboxCreateTimeoutMs; const MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const; const MODAL_LATE_CREATE_PERSIST_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const; type ModalSandbox = Awaited>; /** * A create RPC that outlives its client deadline is fundamentally ambiguous: * Modal may still create the sandbox after the caller stops waiting. It is * therefore neither a safe fresh retry nor a capacity rejection. The caller * must terminalize/reconcile it and retain any local capacity reservation * until a later provider-confirmed cleanup can release it. */ export class ModalSandboxCreateOutcomeUnknownError extends Error { constructor(readonly timeoutMs: number) { super( `RUNTIME_MODAL_SANDBOX_CREATE_OUTCOME_UNKNOWN: Modal SandboxCreate did not settle within ${timeoutMs}ms. ` + 'No fresh sandbox retry is safe because the provider may still create the requested sandbox.', ); this.name = 'ModalSandboxCreateOutcomeUnknownError'; } } export async function awaitModalSandboxCreate(input: { create: Promise; timeoutMs?: number; onLateSuccess?: (value: T) => Promise | void; onLateFailure?: (error: unknown) => Promise | void; }): Promise { const timeoutMs = input.timeoutMs ?? MODAL_SANDBOX_CREATE_TIMEOUT_MS; let settled = false; let timer: ReturnType | undefined; return new Promise((resolve, reject) => { timer = setTimeout(() => { settled = true; reject(new ModalSandboxCreateOutcomeUnknownError(timeoutMs)); }, timeoutMs); void input.create.then( (value) => { if (!settled) { settled = true; if (timer) clearTimeout(timer); resolve(value); return; } void Promise.resolve(input.onLateSuccess?.(value)).catch((error) => { console.error('[play-runner.modal] late_create_cleanup_failed', { error: error instanceof Error ? error.message : String(error), }); }); }, (error) => { if (!settled) { settled = true; if (timer) clearTimeout(timer); reject(error); return; } void Promise.resolve(input.onLateFailure?.(error)).catch( (callbackError) => { console.error( '[play-runner.modal] late_create_failure_cleanup_failed', { error: callbackError instanceof Error ? callbackError.message : String(callbackError), }, ); }, ); }, ); }); } export function modalSandboxLifetimeMs( limits: PlaySandboxRuntimeLimits, ): number { return ( (limits.timeoutSeconds + PLAY_RUNNER_STARTUP_GRACE_SECONDS + PLAY_RUNNER_TERMINAL_GRACE_SECONDS) * 1_000 ); } export function modalDetachedRunnerCeilingMs( limits: PlaySandboxRuntimeLimits, ): number { return (limits.timeoutSeconds + PLAY_RUNNER_TERMINAL_GRACE_SECONDS) * 1_000; } function failed( config: PlayRunnerExecutionConfig, error: unknown, ): PlayRunnerResult { return { status: 'failed', error: error instanceof Error ? error.message : String(error), logs: [], stats: {}, steps: [], checkpoint: config.checkpoint ?? null, tableNamespace: null, }; } function emitModalStage( context: PlayRunnerExecutionConfig['context'], stage: string, extra: Record = {}, ): void { console.info( '[play-runner.modal.stage]', JSON.stringify({ workflowId: context.workflowId ?? null, runId: context.runId ?? null, playName: context.playName ?? null, stage, ...extra, }), ); } async function confirmDetachedModalRunnerReady(input: { readiness: NonNullable; baselineHeartbeatAt: string | null; cancellation?: Promise; }): Promise { const cancellation = input.cancellation ?? new Promise(() => {}); const deadline = Date.now() + MODAL_RUNNER_READY_TIMEOUT_MS; let delayMs = 50; while (Date.now() < deadline) { const response = await Promise.race([ input.readiness(input.baselineHeartbeatAt).catch(() => null), cancellation, ]); if (response?.ready) return; await Promise.race([ new Promise((resolve) => setTimeout(resolve, delayMs)), cancellation, ]); delayMs = Math.min(1_000, delayMs * 2); } throw new Error( `RUNTIME_SANDBOX_START_FAILED: Modal accepted the runner command, but the scheduler did not observe play-runner liveness within ${MODAL_RUNNER_READY_TIMEOUT_MS}ms. Customer play execution was not parked.`, ); } async function terminateModalSandbox(sandbox: ModalSandbox): Promise { try { // The default terminate call only acknowledges the request. Capacity may // be released only after Modal confirms the sandbox itself has stopped. await sandbox.terminate({ wait: true }); return true; } catch (error) { console.warn('[play-runner.modal.terminate_failed]', { sandboxId: sandbox.sandboxId, error: error instanceof Error ? error.message : String(error), }); return false; } } async function releaseModalSandboxCapacityAfterConfirmedTermination(input: { leaseId: string; release: NonNullable; }): Promise { let lastError: unknown; for (const delayMs of MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS) { if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } try { await input.release(input.leaseId); return; } catch (error) { lastError = error; } } throw lastError; } /** * A create that settles after its caller timed out is already an external * fact. Persist it before attempting any further provider operation: the * durable cleanup controller then owns deletion, capacity release, and the * late billing adjustment. A short retry absorbs a transient scheduler-store * outage without turning this into a fresh Play attempt. */ async function persistLateModalSandboxResource(input: { resource: Parameters< NonNullable >[0]; record: NonNullable; }): Promise { let lastError: unknown; for (const delayMs of MODAL_LATE_CREATE_PERSIST_RETRY_DELAYS_MS) { if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } try { await input.record(input.resource); return; } catch (error) { lastError = error; } } throw new AggregateError( [lastError], 'RUNTIME_MODAL_LATE_CREATE_PERSIST_FAILED: Modal created a sandbox after the caller deadline, but its durable cleanup obligation could not be recorded.', ); } export const modalPlayRunnerBackend: PlayRunnerBackend = { async execute(config, callbacks) { let runtimeResourceRegistrationError: unknown; try { validateDaytonaExecutionContext(config.context); const push = config.context.runnerPushExecution; if (!push) { return failed( config, 'Modal runner backend requires push-execution config (context.runnerPushExecution).', ); } const readRunnerReadiness = callbacks?.readDetachedRunnerReadiness; if (!readRunnerReadiness) { return failed( config, 'Modal runner backend requires the scheduler-owned detached-runner readiness port.', ); } const startedAt = Date.now(); const runtimeTiming: PlayRunnerRuntimeTiming = { backend: 'modal' }; emitModalStage(config.context, 'create:start'); const providerCircuitClaim = await callbacks?.shouldAttemptSandboxProvider?.('modal'); if (providerCircuitClaim && !providerCircuitClaim.allowed) { throw recoveryForOpenModalCircuit(); } const modalConfig = await loadModalRequiredConfig({ runtimeSchedulerSchema: config.context.runtimeSchedulerSchema, limits: config.context.sandboxRuntimeLimits, }); const app = await modalConfig.client.apps.fromName(modalConfig.appName, { createIfMissing: true, }); const image = modalConfig.client.images.fromRegistry(modalConfig.image); const capacityReservation = await callbacks?.reserveSandboxCapacity?.('modal'); const createTags = { source: 'deepline-play-runner', orgId: config.context.orgId ?? 'unknown', workflowId: config.context.workflowId ?? 'unknown', runId: config.context.runId ?? 'unknown', attempt: String(config.context.runAttempt ?? 0), }; let sandbox: ModalSandbox; try { // This is deliberately adjacent to the real provider boundary. The // reservation proves the scheduler observes the same no-resource // cleanup path as a genuine Modal rejection; a later persisted // attempt calls the real Modal SDK normally. if ( shouldInjectModalSandboxCreateResourceExhausted({ runtimeTestFaultHeader: config.context.runtimeTestFaultHeader, runAttempt: config.context.runAttempt, }) ) { throw Object.assign( new Error( 'SandboxCreate RESOURCE_EXHAUSTED: injected preview runtime test fault', ), { code: 8, details: 'RESOURCE_EXHAUSTED' }, ); } const modalCreateRequestedAt = Date.now(); // A client timeout does not cancel Modal's create RPC. Persist the // provider-scoped lookup key before beginning it, so a worker loss or // failed late callback leaves an actionable reconciliation obligation // rather than an invisible sandbox/capacity leak. if (!callbacks?.onAmbiguousModalCreateIntent) { throw new Error( 'RUNTIME_MODAL_CREATE_RECONCILIATION_UNAVAILABLE: Modal create requires a durable ambiguity recorder.', ); } await callbacks.onAmbiguousModalCreateIntent({ modalAppId: app.appId, runtimeEnvironment: process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview' ? 'preview' : 'production', tags: createTags, ...(capacityReservation ? { sandboxCapacityLeaseId: capacityReservation.leaseId } : {}), }); sandbox = await awaitModalSandboxCreate({ create: modalConfig.client.sandboxes.create(app, image, { cpu: MODAL_SANDBOX_CPU_CORES, cpuLimit: MODAL_SANDBOX_CPU_CORES, memoryMiB: MODAL_SANDBOX_MEMORY_MIB, memoryLimitMiB: MODAL_SANDBOX_MEMORY_MIB, timeoutMs: modalSandboxLifetimeMs(modalConfig.limits), idleTimeoutMs: modalSandboxLifetimeMs(modalConfig.limits), workdir: modalConfig.workdir, tags: createTags, ...(modalConfig.outboundCidrAllowlist ? { outboundCidrAllowlist: modalConfig.outboundCidrAllowlist } : {}), }), onLateSuccess: async (lateSandbox) => { // Do not directly delete a late sandbox. First store it as a // durable cleanup obligation, then let the normal cleanup worker // perform provider-confirmed deletion, capacity release, and // billing settlement. That remains recoverable if this process // exits between any of those operations. const record = callbacks?.onLateRuntimeResourceAcquired; if (!record) { throw new Error( 'RUNTIME_MODAL_LATE_CREATE_PERSIST_UNAVAILABLE: Modal created a sandbox after the caller deadline, but no durable resource recorder is configured.', ); } await persistLateModalSandboxResource({ record, resource: { kind: 'modal_sandbox', sandboxId: lateSandbox.sandboxId, runtimeEnvironment: process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview' ? 'preview' : 'production', modalAppId: app.appId, billingStartedAt: modalCreateRequestedAt, lateAcquired: true, maxBillingDurationSeconds: modalConfig.limits.timeoutSeconds + PLAY_RUNNER_TERMINAL_GRACE_SECONDS, cpu: MODAL_SANDBOX_CPU_CORES, memoryGiB: MODAL_SANDBOX_MEMORY_MIB / 1024, diskGiB: 0, ...(capacityReservation ? { sandboxCapacityLeaseId: capacityReservation.leaseId } : {}), }, }); }, onLateFailure: async (lateError) => { console.warn('[play-runner.modal] late_create_failed', { error: lateError instanceof Error ? lateError.message : String(lateError), }); // The provider has now rejected the timed-out create, proving no // sandbox exists. Release the provisional lease instead of // holding organization capacity until the crash-backstop TTL. if (capacityReservation && callbacks?.releaseSandboxCapacity) { await releaseModalSandboxCapacityAfterConfirmedTermination({ leaseId: capacityReservation.leaseId, release: callbacks.releaseSandboxCapacity, }); } // The late rejection is provider-confirmed absence. Do not leave // the pre-create selector pending merely because it crossed the // caller deadline; otherwise the durable reconciler scans a fact // we already know is safe. await callbacks?.onAmbiguousModalCreateResolved?.(); }, }); } catch (error) { if ( capacityReservation && !(error instanceof ModalSandboxCreateOutcomeUnknownError) ) { await callbacks?.releaseSandboxCapacity?.( capacityReservation.leaseId, ); } // A settled rejection is known-safe; a timeout deliberately keeps the // pre-create reconciliation obligation pending for recovery. if (!(error instanceof ModalSandboxCreateOutcomeUnknownError)) { try { await callbacks?.onAmbiguousModalCreateResolved?.(); } catch (reconciliationError) { console.warn( '[play-runner.modal] create_reconciliation_resolve_failed', { error: reconciliationError instanceof Error ? reconciliationError.message : String(reconciliationError), }, ); } } // A create rejection proves no sandbox and no customer code exist. // Preserve that fact for the scheduler rather than converting it // into a terminal runner result below. throw recoveryForModalSandboxCreateFailure(error) ?? error; } const billingStartedAt = Date.now(); runtimeTiming.modalCreateMs = billingStartedAt - startedAt; emitModalStage(config.context, 'create:done', { sandboxId: sandbox.sandboxId, elapsedMs: runtimeTiming.modalCreateMs, }); let detached = false; let setupError: unknown; let cancelExecution!: (error: Error) => void; const cancellationPromise = new Promise((_resolve, reject) => { cancelExecution = reject; }); const onCancel = () => cancelExecution(new Error('Modal play runner cancelled')); callbacks?.cancellationSignal?.addEventListener('abort', onCancel, { once: true, }); try { if (callbacks?.cancellationSignal?.aborted) onCancel(); try { await callbacks?.onRuntimeResourceAcquired?.({ kind: 'modal_sandbox', sandboxId: sandbox.sandboxId, runtimeEnvironment: process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview' ? 'preview' : 'production', modalAppId: app.appId, billingStartedAt, maxBillingDurationSeconds: modalConfig.limits.timeoutSeconds + PLAY_RUNNER_TERMINAL_GRACE_SECONDS, cpu: MODAL_SANDBOX_CPU_CORES, memoryGiB: MODAL_SANDBOX_MEMORY_MIB / 1024, diskGiB: 0, ...(capacityReservation ? { sandboxCapacityLeaseId: capacityReservation.leaseId } : {}), }); // Do not clear the pre-create obligation until the sandbox itself // is durable. A process failure between Modal's success response and // resource recording must remain reconciliable rather than looking // like a known-safe rejection. try { await callbacks?.onAmbiguousModalCreateResolved?.(); } catch (reconciliationError) { // The resource record is the authority now. Leaving this intent // pending is conservative and observable; do not turn a settled // resource into a fresh execution path just because the advisory // intent cleanup was temporarily unavailable. console.warn( '[play-runner.modal] create_reconciliation_resolve_failed', { sandboxId: sandbox.sandboxId, error: reconciliationError instanceof Error ? reconciliationError.message : String(reconciliationError), }, ); } // Circuit state is an operational hint, not part of the durable // resource transaction. A successful resource record proves this // provider path is healthy even if the best-effort circuit close // cannot reach the scheduler control plane right now. try { await callbacks?.markSandboxProviderHealthy?.( 'modal', providerCircuitClaim?.probeToken ?? null, ); } catch (circuitError) { console.warn('[play-runner.modal] recovery_circuit_close_failed', { sandboxId: sandbox.sandboxId, error: circuitError instanceof Error ? circuitError.message : String(circuitError), }); } } catch (error) { runtimeResourceRegistrationError = error; throw error; } const uploadStartedAt = Date.now(); const payload = await Promise.race([ stageRunnerPayload({ sandbox: { id: sandbox.sandboxId, uploadFile: (content, path) => sandbox.filesystem.writeBytes(content, path), }, bundlePromise: buildPlayRunnerBundle(), config, workDir: modalConfig.workdir, startedAt, emitStage: (stage, extra) => emitModalStage(config.context, stage, extra), }), cancellationPromise, ]); runtimeTiming.modalUploadMs = Date.now() - uploadStartedAt; const baselineHeartbeatAt = await Promise.race([ captureDetachedDaytonaRunnerReadinessBaseline({ readiness: readRunnerReadiness, }), cancellationPromise, ]); emitModalStage(config.context, 'execute:start', { sandboxId: sandbox.sandboxId, timeoutSeconds: modalConfig.limits.timeoutSeconds, mode: 'detached', }); const executeStartedAt = Date.now(); await Promise.race([ sandbox.exec(['bash', '-lc', payload.command]), cancellationPromise, ]); runtimeTiming.modalExecuteMs = Date.now() - executeStartedAt; // A readiness observation after the command is accepted is ambiguous: // the detached runner may already have received its durable park and // started customer code while this worker cannot read the scheduler. // Never translate that ambiguity into a fresh sandbox retry. await confirmDetachedModalRunnerReady({ readiness: readRunnerReadiness, baselineHeartbeatAt, cancellation: cancellationPromise, }); if (callbacks?.cancellationSignal?.aborted) { throw new Error('Modal play runner cancelled'); } detached = true; sandbox.detach(); const runnerAttempt = Math.max( 0, Math.floor(config.context.runAttempt ?? 0), ); emitModalStage(config.context, 'execute:detached', { sandboxId: sandbox.sandboxId, runnerAttempt, elapsedMs: Date.now() - startedAt, }); return { status: 'suspended', suspension: { kind: 'detached_runner', boundaryId: `detached-runner:${push.runId}:${runnerAttempt}`, runnerAttempt, sandboxProvider: 'modal', runtimeSandboxRef: { schemaVersion: 1, provider: 'modal', resourceId: sandbox.sandboxId, routingDomain: app.appId, }, sandboxId: sandbox.sandboxId, sessionId: 'modal', cmdId: sandbox.sandboxId, outputPath: payload.outputPath, exitCodePath: payload.exitCodePath, runtimeCompletedPath: payload.runtimeCompletedPath, startedAtMs: Date.now(), heartbeatTimeoutMs: push.leaseSeconds * 1_000, runtimeLimitSeconds: modalConfig.limits.timeoutSeconds, ceilingMs: modalDetachedRunnerCeilingMs(modalConfig.limits), }, logs: [], stats: {}, steps: [], checkpoint: config.checkpoint ?? { completedBatches: {}, completedToolBatches: {}, resolvedWaterfalls: {}, resolvedBoundaries: {}, }, tableNamespace: null, runtimeTiming, }; } catch (error) { setupError = error; throw error; } finally { callbacks?.cancellationSignal?.removeEventListener('abort', onCancel); if (!detached && (await terminateModalSandbox(sandbox))) { // This branch synchronously confirms that the just-created resource // is gone. Do not keep its admission lease until the cleanup loop // sees a resource that no longer exists. if (capacityReservation && callbacks?.releaseSandboxCapacity) { try { await releaseModalSandboxCapacityAfterConfirmedTermination({ leaseId: capacityReservation.leaseId, release: callbacks.releaseSandboxCapacity, }); } catch (releaseError) { // The scheduler fence is the primary correctness signal. Keep // it intact if its best-effort early capacity release is // transiently unavailable; durable cleanup will retry release. if (setupError) { console.error('[play-runner.modal.capacity_release_failed]', { sandboxId: sandbox.sandboxId, error: releaseError instanceof Error ? releaseError.message : String(releaseError), }); } else { throw releaseError; } } } } } } catch (error) { if ( error === runtimeResourceRegistrationError || error instanceof RuntimeResourceFenceLostError || isRuntimeSandboxCapacityLimitError(error) || isPlayRunRecoveryError(error) ) { throw error; } emitModalStage(config.context, 'execute:error', { error: error instanceof Error ? error.message : String(error), }); return failed(config, error); } }, }; export async function deleteModalSandboxById(input: { sandboxId: string; expectedAppId?: string | null; }): Promise< | { kind: 'deleted' | 'already_absent'; appId: string } | { kind: 'failed'; appId: string | null; code: string; detail: string } > { const expectedAppId = input.expectedAppId?.trim() || null; if (!expectedAppId) { console.warn('[play-runner.modal.reclaim_sandbox_delete_blocked]', { sandboxId: input.sandboxId, reason: 'missing_routing_domain', }); return { kind: 'failed', appId: null, code: 'missing_routing_domain', detail: 'Modal sandbox cleanup requires its persisted app id.', }; } try { const { client } = await loadModalClientConfig(); for await (const sandbox of client.sandboxes.list({ appId: expectedAppId, })) { if (sandbox.sandboxId !== input.sandboxId) continue; await sandbox.terminate({ wait: true }); return { kind: 'deleted', appId: expectedAppId }; } // Cleanup means ensure absent. An exact app-scoped inventory proving the // id is absent satisfies the obligation without an unsafe cross-app get. return { kind: 'already_absent', appId: expectedAppId }; } catch (error) { console.warn('[play-runner.modal.reclaim_sandbox_delete_failed]', { sandboxId: input.sandboxId, expectedAppId, error: error instanceof Error ? error.message : String(error), }); return { kind: 'failed', appId: expectedAppId, code: 'provider_delete_failed', detail: error instanceof Error ? error.message : String(error), }; } } /** * Read the provider inventory using the exact app + tag selector persisted * before a Modal create call. This is deliberately a tiny provider adapter: * the scheduler owns claim/fencing, resource recording, cleanup, and policy. */ export async function findModalSandboxIdsByTags(input: { modalAppId: string; tags: Record; environment: 'preview' | 'production'; }): Promise { const modalAppId = input.modalAppId.trim(); if (!modalAppId) throw new Error('Modal reconciliation requires modalAppId.'); const tags = Object.fromEntries( Object.entries(input.tags).filter( ([key, value]) => key.trim() && value.trim(), ), ); if (Object.keys(tags).length === 0) { throw new Error('Modal reconciliation requires non-empty create tags.'); } const { client } = await loadModalClientConfig(); const sandboxIds: string[] = []; for await (const sandbox of client.sandboxes.list({ appId: modalAppId, tags, environment: input.environment, })) { if (sandbox.sandboxId?.trim()) sandboxIds.push(sandbox.sandboxId.trim()); } return [...new Set(sandboxIds)]; } export async function readDetachedModalRuntimeCompletion(input: { sandboxId: string; runtimeCompletedPath: string; expectedAppId?: string | null; }): Promise { const expectedAppId = input.expectedAppId?.trim() || null; if (!expectedAppId) return null; try { const { client } = await loadModalClientConfig(); for await (const sandbox of client.sandboxes.list({ appId: expectedAppId, })) { if (sandbox.sandboxId !== input.sandboxId) continue; const marker = JSON.parse( await sandbox.filesystem.readText(input.runtimeCompletedPath), ) as { at?: unknown }; return typeof marker.at === 'number' && Number.isFinite(marker.at) ? marker.at : null; } return null; } catch (error) { console.warn('[play-runner.modal.runtime_completion_marker_unavailable]', { sandboxId: input.sandboxId, expectedAppId, error: error instanceof Error ? error.message : String(error), }); return null; } }