deepline 0.2.0 → 0.2.2
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 +17 -3
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-job-errors.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/backend.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +177 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-modal-fallback.ts +218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +26 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +380 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +240 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +17 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +188 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +77 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +39 -0
- package/dist/cli/index.js +50 -26
- package/dist/cli/index.mjs +50 -26
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +50 -5
- package/dist/index.mjs +50 -5
- 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 +7 -1
- package/dist/{tool-execution-error-YDz7UMl-.d.mts → tool-execution-error-4-rhemLQ.d.mts} +7 -1
- package/dist/{tool-execution-error-YDz7UMl-.d.ts → tool-execution-error-4-rhemLQ.d.ts} +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import type { ModalClient } from 'modal';
|
|
2
|
+
import type { PlayRunnerBackend, PlayRunnerCallbacks } from '../types';
|
|
3
|
+
import { RuntimeResourceFenceLostError } from '../types';
|
|
4
|
+
import { buildPlayRunnerBundle } from '../bundle';
|
|
5
|
+
import type {
|
|
6
|
+
PlayRunnerExecutionConfig,
|
|
7
|
+
PlayRunnerResult,
|
|
8
|
+
PlayRunnerRuntimeTiming,
|
|
9
|
+
} from '@shared_libs/play-runtime/protocol';
|
|
10
|
+
import {
|
|
11
|
+
PLAY_RUNNER_STARTUP_GRACE_SECONDS,
|
|
12
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
13
|
+
} from '@shared_libs/play-runtime/runtime-constants';
|
|
14
|
+
import {
|
|
15
|
+
MODAL_SANDBOX_CPU_CORES,
|
|
16
|
+
MODAL_SANDBOX_MEMORY_MIB,
|
|
17
|
+
loadModalClientConfig,
|
|
18
|
+
loadModalRequiredConfig,
|
|
19
|
+
} from '@shared_libs/play-runtime/modal-runtime-config';
|
|
20
|
+
import { validateDaytonaExecutionContext } from './daytona-lifecycle';
|
|
21
|
+
import { stageRunnerPayload } from './daytona-payload-transport';
|
|
22
|
+
import { captureDetachedDaytonaRunnerReadinessBaseline } from './daytona-session-execution';
|
|
23
|
+
|
|
24
|
+
const MODAL_RUNNER_READY_TIMEOUT_MS = 30_000;
|
|
25
|
+
type ModalSandbox = Awaited<ReturnType<ModalClient['sandboxes']['create']>>;
|
|
26
|
+
|
|
27
|
+
function failed(
|
|
28
|
+
config: PlayRunnerExecutionConfig,
|
|
29
|
+
error: unknown,
|
|
30
|
+
): PlayRunnerResult {
|
|
31
|
+
return {
|
|
32
|
+
status: 'failed',
|
|
33
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34
|
+
logs: [],
|
|
35
|
+
stats: {},
|
|
36
|
+
steps: [],
|
|
37
|
+
checkpoint: config.checkpoint ?? null,
|
|
38
|
+
tableNamespace: null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function emitModalStage(
|
|
43
|
+
context: PlayRunnerExecutionConfig['context'],
|
|
44
|
+
stage: string,
|
|
45
|
+
extra: Record<string, unknown> = {},
|
|
46
|
+
): void {
|
|
47
|
+
console.info(
|
|
48
|
+
'[play-runner.modal.stage]',
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
workflowId: context.workflowId ?? null,
|
|
51
|
+
runId: context.runId ?? null,
|
|
52
|
+
playName: context.playName ?? null,
|
|
53
|
+
stage,
|
|
54
|
+
...extra,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function confirmDetachedModalRunnerReady(input: {
|
|
60
|
+
readiness: NonNullable<PlayRunnerCallbacks['readDetachedRunnerReadiness']>;
|
|
61
|
+
baselineHeartbeatAt: string | null;
|
|
62
|
+
cancellation?: Promise<never>;
|
|
63
|
+
}): Promise<void> {
|
|
64
|
+
const cancellation = input.cancellation ?? new Promise<never>(() => {});
|
|
65
|
+
const deadline = Date.now() + MODAL_RUNNER_READY_TIMEOUT_MS;
|
|
66
|
+
let delayMs = 50;
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
const response = await Promise.race([
|
|
69
|
+
input.readiness(input.baselineHeartbeatAt).catch(() => null),
|
|
70
|
+
cancellation,
|
|
71
|
+
]);
|
|
72
|
+
if (response?.ready) return;
|
|
73
|
+
await Promise.race([
|
|
74
|
+
new Promise<void>((resolve) => setTimeout(resolve, delayMs)),
|
|
75
|
+
cancellation,
|
|
76
|
+
]);
|
|
77
|
+
delayMs = Math.min(1_000, delayMs * 2);
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
`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.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function terminateModalSandbox(sandbox: ModalSandbox): Promise<void> {
|
|
85
|
+
await sandbox.terminate().catch((error) => {
|
|
86
|
+
console.warn('[play-runner.modal.terminate_failed]', {
|
|
87
|
+
sandboxId: sandbox.sandboxId,
|
|
88
|
+
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const modalPlayRunnerBackend: PlayRunnerBackend = {
|
|
94
|
+
async execute(config, callbacks) {
|
|
95
|
+
let runtimeResourceRegistrationError: unknown;
|
|
96
|
+
try {
|
|
97
|
+
validateDaytonaExecutionContext(config.context);
|
|
98
|
+
const push = config.context.runnerPushExecution;
|
|
99
|
+
if (!push) {
|
|
100
|
+
return failed(
|
|
101
|
+
config,
|
|
102
|
+
'Modal runner backend requires push-execution config (context.runnerPushExecution).',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const readRunnerReadiness = callbacks?.readDetachedRunnerReadiness;
|
|
106
|
+
if (!readRunnerReadiness) {
|
|
107
|
+
return failed(
|
|
108
|
+
config,
|
|
109
|
+
'Modal runner backend requires the scheduler-owned detached-runner readiness port.',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const startedAt = Date.now();
|
|
114
|
+
const runtimeTiming: PlayRunnerRuntimeTiming = { backend: 'modal' };
|
|
115
|
+
emitModalStage(config.context, 'create:start');
|
|
116
|
+
const modalConfig = await loadModalRequiredConfig({
|
|
117
|
+
runtimeSchedulerSchema: config.context.runtimeSchedulerSchema,
|
|
118
|
+
limits: config.context.sandboxRuntimeLimits,
|
|
119
|
+
});
|
|
120
|
+
const app = await modalConfig.client.apps.fromName(modalConfig.appName, {
|
|
121
|
+
createIfMissing: true,
|
|
122
|
+
});
|
|
123
|
+
const image = modalConfig.client.images.fromRegistry(modalConfig.image);
|
|
124
|
+
const sandbox = await modalConfig.client.sandboxes.create(app, image, {
|
|
125
|
+
cpu: MODAL_SANDBOX_CPU_CORES,
|
|
126
|
+
cpuLimit: MODAL_SANDBOX_CPU_CORES,
|
|
127
|
+
memoryMiB: MODAL_SANDBOX_MEMORY_MIB,
|
|
128
|
+
memoryLimitMiB: MODAL_SANDBOX_MEMORY_MIB,
|
|
129
|
+
timeoutMs:
|
|
130
|
+
(modalConfig.limits.timeoutSeconds +
|
|
131
|
+
PLAY_RUNNER_STARTUP_GRACE_SECONDS +
|
|
132
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS) *
|
|
133
|
+
1_000,
|
|
134
|
+
idleTimeoutMs:
|
|
135
|
+
(modalConfig.limits.timeoutSeconds +
|
|
136
|
+
PLAY_RUNNER_STARTUP_GRACE_SECONDS +
|
|
137
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS) *
|
|
138
|
+
1_000,
|
|
139
|
+
workdir: modalConfig.workdir,
|
|
140
|
+
tags: {
|
|
141
|
+
source: 'deepline-play-runner',
|
|
142
|
+
orgId: config.context.orgId ?? 'unknown',
|
|
143
|
+
workflowId: config.context.workflowId ?? 'unknown',
|
|
144
|
+
runId: config.context.runId ?? 'unknown',
|
|
145
|
+
},
|
|
146
|
+
...(modalConfig.outboundCidrAllowlist
|
|
147
|
+
? { outboundCidrAllowlist: modalConfig.outboundCidrAllowlist }
|
|
148
|
+
: {}),
|
|
149
|
+
});
|
|
150
|
+
const billingStartedAt = Date.now();
|
|
151
|
+
runtimeTiming.modalCreateMs = billingStartedAt - startedAt;
|
|
152
|
+
emitModalStage(config.context, 'create:done', {
|
|
153
|
+
sandboxId: sandbox.sandboxId,
|
|
154
|
+
elapsedMs: runtimeTiming.modalCreateMs,
|
|
155
|
+
});
|
|
156
|
+
let detached = false;
|
|
157
|
+
let cancelExecution!: (error: Error) => void;
|
|
158
|
+
const cancellationPromise = new Promise<never>((_resolve, reject) => {
|
|
159
|
+
cancelExecution = reject;
|
|
160
|
+
});
|
|
161
|
+
const onCancel = () =>
|
|
162
|
+
cancelExecution(new Error('Modal play runner cancelled'));
|
|
163
|
+
callbacks?.cancellationSignal?.addEventListener('abort', onCancel, {
|
|
164
|
+
once: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (callbacks?.cancellationSignal?.aborted) onCancel();
|
|
169
|
+
try {
|
|
170
|
+
await callbacks?.onRuntimeResourceAcquired?.({
|
|
171
|
+
kind: 'modal_sandbox',
|
|
172
|
+
sandboxId: sandbox.sandboxId,
|
|
173
|
+
runtimeEnvironment:
|
|
174
|
+
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
175
|
+
? 'preview'
|
|
176
|
+
: 'production',
|
|
177
|
+
modalAppId: app.appId,
|
|
178
|
+
billingStartedAt,
|
|
179
|
+
maxBillingDurationSeconds:
|
|
180
|
+
modalConfig.limits.timeoutSeconds +
|
|
181
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
182
|
+
cpu: MODAL_SANDBOX_CPU_CORES,
|
|
183
|
+
memoryGiB: MODAL_SANDBOX_MEMORY_MIB / 1024,
|
|
184
|
+
diskGiB: 0,
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
runtimeResourceRegistrationError = error;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const uploadStartedAt = Date.now();
|
|
192
|
+
const payload = await Promise.race([
|
|
193
|
+
stageRunnerPayload({
|
|
194
|
+
sandbox: {
|
|
195
|
+
id: sandbox.sandboxId,
|
|
196
|
+
uploadFile: (content, path) =>
|
|
197
|
+
sandbox.filesystem.writeBytes(content, path),
|
|
198
|
+
},
|
|
199
|
+
bundlePromise: buildPlayRunnerBundle(),
|
|
200
|
+
config,
|
|
201
|
+
workDir: modalConfig.workdir,
|
|
202
|
+
startedAt,
|
|
203
|
+
emitStage: (stage, extra) =>
|
|
204
|
+
emitModalStage(config.context, stage, extra),
|
|
205
|
+
}),
|
|
206
|
+
cancellationPromise,
|
|
207
|
+
]);
|
|
208
|
+
runtimeTiming.modalUploadMs = Date.now() - uploadStartedAt;
|
|
209
|
+
const baselineHeartbeatAt = await Promise.race([
|
|
210
|
+
captureDetachedDaytonaRunnerReadinessBaseline({
|
|
211
|
+
readiness: readRunnerReadiness,
|
|
212
|
+
}),
|
|
213
|
+
cancellationPromise,
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
emitModalStage(config.context, 'execute:start', {
|
|
217
|
+
sandboxId: sandbox.sandboxId,
|
|
218
|
+
timeoutSeconds: modalConfig.limits.timeoutSeconds,
|
|
219
|
+
mode: 'detached',
|
|
220
|
+
});
|
|
221
|
+
const executeStartedAt = Date.now();
|
|
222
|
+
await Promise.race([
|
|
223
|
+
sandbox.exec(['bash', '-lc', payload.command]),
|
|
224
|
+
cancellationPromise,
|
|
225
|
+
]);
|
|
226
|
+
runtimeTiming.modalExecuteMs = Date.now() - executeStartedAt;
|
|
227
|
+
await confirmDetachedModalRunnerReady({
|
|
228
|
+
readiness: readRunnerReadiness,
|
|
229
|
+
baselineHeartbeatAt,
|
|
230
|
+
cancellation: cancellationPromise,
|
|
231
|
+
});
|
|
232
|
+
if (callbacks?.cancellationSignal?.aborted) {
|
|
233
|
+
throw new Error('Modal play runner cancelled');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
detached = true;
|
|
237
|
+
sandbox.detach();
|
|
238
|
+
const runnerAttempt = Math.max(
|
|
239
|
+
0,
|
|
240
|
+
Math.floor(config.context.runAttempt ?? 0),
|
|
241
|
+
);
|
|
242
|
+
emitModalStage(config.context, 'execute:detached', {
|
|
243
|
+
sandboxId: sandbox.sandboxId,
|
|
244
|
+
runnerAttempt,
|
|
245
|
+
elapsedMs: Date.now() - startedAt,
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
status: 'suspended',
|
|
249
|
+
suspension: {
|
|
250
|
+
kind: 'detached_runner',
|
|
251
|
+
boundaryId: `detached-runner:${push.runId}:${runnerAttempt}`,
|
|
252
|
+
runnerAttempt,
|
|
253
|
+
sandboxProvider: 'modal',
|
|
254
|
+
runtimeSandboxRef: {
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
provider: 'modal',
|
|
257
|
+
resourceId: sandbox.sandboxId,
|
|
258
|
+
routingDomain: app.appId,
|
|
259
|
+
},
|
|
260
|
+
sandboxId: sandbox.sandboxId,
|
|
261
|
+
sessionId: 'modal',
|
|
262
|
+
cmdId: sandbox.sandboxId,
|
|
263
|
+
outputPath: payload.outputPath,
|
|
264
|
+
exitCodePath: payload.exitCodePath,
|
|
265
|
+
runtimeCompletedPath: payload.runtimeCompletedPath,
|
|
266
|
+
startedAtMs: Date.now(),
|
|
267
|
+
heartbeatTimeoutMs: push.leaseSeconds * 1_000,
|
|
268
|
+
ceilingMs:
|
|
269
|
+
(modalConfig.limits.timeoutSeconds +
|
|
270
|
+
PLAY_RUNNER_TERMINAL_GRACE_SECONDS) *
|
|
271
|
+
1_000,
|
|
272
|
+
},
|
|
273
|
+
logs: [],
|
|
274
|
+
stats: {},
|
|
275
|
+
steps: [],
|
|
276
|
+
checkpoint: config.checkpoint ?? {
|
|
277
|
+
completedBatches: {},
|
|
278
|
+
completedToolBatches: {},
|
|
279
|
+
resolvedWaterfalls: {},
|
|
280
|
+
resolvedBoundaries: {},
|
|
281
|
+
},
|
|
282
|
+
tableNamespace: null,
|
|
283
|
+
runtimeTiming,
|
|
284
|
+
};
|
|
285
|
+
} finally {
|
|
286
|
+
callbacks?.cancellationSignal?.removeEventListener('abort', onCancel);
|
|
287
|
+
if (!detached) await terminateModalSandbox(sandbox);
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (
|
|
291
|
+
error === runtimeResourceRegistrationError ||
|
|
292
|
+
error instanceof RuntimeResourceFenceLostError
|
|
293
|
+
) {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
emitModalStage(config.context, 'execute:error', {
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
});
|
|
299
|
+
return failed(config, error);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export async function deleteModalSandboxById(input: {
|
|
305
|
+
sandboxId: string;
|
|
306
|
+
expectedAppId?: string | null;
|
|
307
|
+
}): Promise<
|
|
308
|
+
| { kind: 'deleted' | 'already_absent'; appId: string }
|
|
309
|
+
| { kind: 'failed'; appId: string | null; code: string; detail: string }
|
|
310
|
+
> {
|
|
311
|
+
const expectedAppId = input.expectedAppId?.trim() || null;
|
|
312
|
+
if (!expectedAppId) {
|
|
313
|
+
console.warn('[play-runner.modal.reclaim_sandbox_delete_blocked]', {
|
|
314
|
+
sandboxId: input.sandboxId,
|
|
315
|
+
reason: 'missing_routing_domain',
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
kind: 'failed',
|
|
319
|
+
appId: null,
|
|
320
|
+
code: 'missing_routing_domain',
|
|
321
|
+
detail: 'Modal sandbox cleanup requires its persisted app id.',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const { client } = await loadModalClientConfig();
|
|
326
|
+
for await (const sandbox of client.sandboxes.list({
|
|
327
|
+
appId: expectedAppId,
|
|
328
|
+
})) {
|
|
329
|
+
if (sandbox.sandboxId !== input.sandboxId) continue;
|
|
330
|
+
await sandbox.terminate();
|
|
331
|
+
return { kind: 'deleted', appId: expectedAppId };
|
|
332
|
+
}
|
|
333
|
+
// Cleanup means ensure absent. An exact app-scoped inventory proving the
|
|
334
|
+
// id is absent satisfies the obligation without an unsafe cross-app get.
|
|
335
|
+
return { kind: 'already_absent', appId: expectedAppId };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn('[play-runner.modal.reclaim_sandbox_delete_failed]', {
|
|
338
|
+
sandboxId: input.sandboxId,
|
|
339
|
+
expectedAppId,
|
|
340
|
+
error: error instanceof Error ? error.message : String(error),
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
kind: 'failed',
|
|
344
|
+
appId: expectedAppId,
|
|
345
|
+
code: 'provider_delete_failed',
|
|
346
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function readDetachedModalRuntimeCompletion(input: {
|
|
352
|
+
sandboxId: string;
|
|
353
|
+
runtimeCompletedPath: string;
|
|
354
|
+
expectedAppId?: string | null;
|
|
355
|
+
}): Promise<number | null> {
|
|
356
|
+
const expectedAppId = input.expectedAppId?.trim() || null;
|
|
357
|
+
if (!expectedAppId) return null;
|
|
358
|
+
try {
|
|
359
|
+
const { client } = await loadModalClientConfig();
|
|
360
|
+
for await (const sandbox of client.sandboxes.list({
|
|
361
|
+
appId: expectedAppId,
|
|
362
|
+
})) {
|
|
363
|
+
if (sandbox.sandboxId !== input.sandboxId) continue;
|
|
364
|
+
const marker = JSON.parse(
|
|
365
|
+
await sandbox.filesystem.readText(input.runtimeCompletedPath),
|
|
366
|
+
) as { at?: unknown };
|
|
367
|
+
return typeof marker.at === 'number' && Number.isFinite(marker.at)
|
|
368
|
+
? marker.at
|
|
369
|
+
: null;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.warn('[play-runner.modal.runtime_completion_marker_unavailable]', {
|
|
374
|
+
sandboxId: input.sandboxId,
|
|
375
|
+
expectedAppId,
|
|
376
|
+
error: error instanceof Error ? error.message : String(error),
|
|
377
|
+
});
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import type { PlayRunnerBackend } from './types';
|
|
2
2
|
import { daytonaPlayRunnerBackend } from './backends/daytona';
|
|
3
|
+
import {
|
|
4
|
+
daytonaModalFallbackPlayRunnerBackend,
|
|
5
|
+
daytonaOnlyPlayRunnerBackend,
|
|
6
|
+
modalOnlyPlayRunnerBackend,
|
|
7
|
+
resolveDefaultRuntimeSandboxPlacementBackend,
|
|
8
|
+
} from './backends/daytona-modal-fallback';
|
|
3
9
|
import { localProcessPlayRunnerBackend } from './backends/local-process';
|
|
10
|
+
import { modalPlayRunnerBackend } from './backends/modal';
|
|
4
11
|
import {
|
|
5
12
|
PLAY_RUNTIME_BACKENDS,
|
|
6
13
|
normalizePlayRuntimeBackend,
|
|
7
14
|
type PlayRuntimeBackendId,
|
|
8
15
|
} from '@shared_libs/play-runtime/backend';
|
|
16
|
+
import {
|
|
17
|
+
runtimeSandboxPlacementPolicyForBackend,
|
|
18
|
+
type RuntimeSandboxPlacementPolicyId,
|
|
19
|
+
} from '@shared_libs/play-runtime/runtime-sandbox-placement-policy';
|
|
20
|
+
|
|
21
|
+
export function resolveRuntimeSandboxPlacementBackend(
|
|
22
|
+
policyId: RuntimeSandboxPlacementPolicyId | string,
|
|
23
|
+
): PlayRunnerBackend {
|
|
24
|
+
return resolveDefaultRuntimeSandboxPlacementBackend(policyId);
|
|
25
|
+
}
|
|
9
26
|
|
|
10
27
|
export function resolvePlayRunnerBackend(
|
|
11
28
|
backendId?: PlayRuntimeBackendId | string | null,
|
|
@@ -13,8 +30,9 @@ export function resolvePlayRunnerBackend(
|
|
|
13
30
|
const backend = normalizePlayRuntimeBackend(
|
|
14
31
|
backendId ?? process.env.DEEPLINE_PLAY_RUNNER_BACKEND,
|
|
15
32
|
);
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
const placementPolicy = runtimeSandboxPlacementPolicyForBackend(backend);
|
|
34
|
+
if (placementPolicy) {
|
|
35
|
+
return resolveRuntimeSandboxPlacementBackend(placementPolicy.id);
|
|
18
36
|
}
|
|
19
37
|
if (backend === PLAY_RUNTIME_BACKENDS.localProcess) {
|
|
20
38
|
return localProcessPlayRunnerBackend;
|
|
@@ -22,5 +40,12 @@ export function resolvePlayRunnerBackend(
|
|
|
22
40
|
throw new Error(`Unsupported play runner backend: ${backend}`);
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
export {
|
|
43
|
+
export {
|
|
44
|
+
daytonaPlayRunnerBackend,
|
|
45
|
+
daytonaModalFallbackPlayRunnerBackend,
|
|
46
|
+
daytonaOnlyPlayRunnerBackend,
|
|
47
|
+
localProcessPlayRunnerBackend,
|
|
48
|
+
modalOnlyPlayRunnerBackend,
|
|
49
|
+
modalPlayRunnerBackend,
|
|
50
|
+
};
|
|
26
51
|
export type { PlayRunnerBackend };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteDaytonaSandboxByIdWithOutcome,
|
|
3
|
+
readDetachedDaytonaRuntimeCompletion,
|
|
4
|
+
} from './backends/daytona';
|
|
5
|
+
import {
|
|
6
|
+
deleteModalSandboxById,
|
|
7
|
+
readDetachedModalRuntimeCompletion,
|
|
8
|
+
} from './backends/modal';
|
|
9
|
+
import type { DetachedRunnerSuspension } from '@shared_libs/play-runtime/suspension';
|
|
10
|
+
|
|
11
|
+
export type RuntimeSandboxProviderId = 'daytona' | 'modal';
|
|
12
|
+
|
|
13
|
+
export type RuntimeSandboxRef = {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
provider: RuntimeSandboxProviderId;
|
|
16
|
+
resourceId: string;
|
|
17
|
+
routingDomain: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RuntimeSandboxReconciliationAdapter = {
|
|
21
|
+
provider: RuntimeSandboxProviderId;
|
|
22
|
+
delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
|
|
23
|
+
readWorkCompletedAt(
|
|
24
|
+
ref: RuntimeSandboxRef,
|
|
25
|
+
path: string,
|
|
26
|
+
): Promise<number | null>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RuntimeSandboxReconciliation = {
|
|
30
|
+
delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
|
|
31
|
+
readWorkCompletedAt(
|
|
32
|
+
ref: RuntimeSandboxRef,
|
|
33
|
+
path: string,
|
|
34
|
+
): Promise<number | null>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RuntimeSandboxDeleteOutcome =
|
|
38
|
+
| {
|
|
39
|
+
kind: 'deleted' | 'already_absent';
|
|
40
|
+
routingDomain: string | null;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
kind: 'timed_out' | 'rate_limited' | 'failed';
|
|
44
|
+
routingDomain: string | null;
|
|
45
|
+
code: string;
|
|
46
|
+
detail: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function runtimeSandboxDeleteSucceeded(
|
|
50
|
+
outcome: RuntimeSandboxDeleteOutcome,
|
|
51
|
+
): outcome is Extract<
|
|
52
|
+
RuntimeSandboxDeleteOutcome,
|
|
53
|
+
{ kind: 'deleted' | 'already_absent' }
|
|
54
|
+
> {
|
|
55
|
+
return outcome.kind === 'deleted' || outcome.kind === 'already_absent';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stable identity for deduplication and task idempotency at provider seams. */
|
|
59
|
+
export function runtimeSandboxRefIdentityKey(ref: RuntimeSandboxRef): string {
|
|
60
|
+
return JSON.stringify([ref.provider, ref.resourceId, ref.routingDomain]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function registry(
|
|
64
|
+
adapters: readonly RuntimeSandboxReconciliationAdapter[],
|
|
65
|
+
): Map<RuntimeSandboxProviderId, RuntimeSandboxReconciliationAdapter> {
|
|
66
|
+
const result = new Map<
|
|
67
|
+
RuntimeSandboxProviderId,
|
|
68
|
+
RuntimeSandboxReconciliationAdapter
|
|
69
|
+
>();
|
|
70
|
+
for (const adapter of adapters) {
|
|
71
|
+
if (result.has(adapter.provider)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Duplicate Runtime Sandbox Adapter: ${adapter.provider}.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
result.set(adapter.provider, adapter);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createRuntimeSandboxReconciliation(input: {
|
|
82
|
+
adapters: readonly RuntimeSandboxReconciliationAdapter[];
|
|
83
|
+
}): RuntimeSandboxReconciliation {
|
|
84
|
+
const adapters = registry(input.adapters);
|
|
85
|
+
const resolve = (ref: RuntimeSandboxRef) => {
|
|
86
|
+
const adapter = adapters.get(ref.provider);
|
|
87
|
+
if (!adapter) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`No Runtime Sandbox Adapter is registered for ${ref.provider}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return adapter;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
delete: (ref) => resolve(ref).delete(ref),
|
|
96
|
+
readWorkCompletedAt: (ref, path) =>
|
|
97
|
+
resolve(ref).readWorkCompletedAt(ref, path),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function runtimeSandboxRefFromResource(
|
|
102
|
+
value: unknown,
|
|
103
|
+
): RuntimeSandboxRef | null {
|
|
104
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
105
|
+
const resource = value as Record<string, unknown>;
|
|
106
|
+
const resourceId =
|
|
107
|
+
typeof resource.sandboxId === 'string' ? resource.sandboxId.trim() : '';
|
|
108
|
+
if (!resourceId) return null;
|
|
109
|
+
if (resource.kind === 'daytona_sandbox') {
|
|
110
|
+
return {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
provider: 'daytona',
|
|
113
|
+
resourceId,
|
|
114
|
+
routingDomain:
|
|
115
|
+
typeof resource.daytonaOrganizationId === 'string' &&
|
|
116
|
+
resource.daytonaOrganizationId.trim()
|
|
117
|
+
? resource.daytonaOrganizationId.trim()
|
|
118
|
+
: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (resource.kind === 'modal_sandbox') {
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
provider: 'modal',
|
|
125
|
+
resourceId,
|
|
126
|
+
routingDomain:
|
|
127
|
+
typeof resource.modalAppId === 'string' && resource.modalAppId.trim()
|
|
128
|
+
? resource.modalAppId.trim()
|
|
129
|
+
: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function runtimeSandboxRefFromSuspension(
|
|
136
|
+
suspension: DetachedRunnerSuspension,
|
|
137
|
+
): RuntimeSandboxRef {
|
|
138
|
+
const storedRef = (suspension as { runtimeSandboxRef?: unknown })
|
|
139
|
+
.runtimeSandboxRef;
|
|
140
|
+
if (storedRef !== undefined) {
|
|
141
|
+
if (
|
|
142
|
+
!storedRef ||
|
|
143
|
+
typeof storedRef !== 'object' ||
|
|
144
|
+
Array.isArray(storedRef)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error('Persisted Runtime Sandbox Ref is not an object.');
|
|
147
|
+
}
|
|
148
|
+
const candidate = storedRef as Record<string, unknown>;
|
|
149
|
+
const resourceId =
|
|
150
|
+
typeof candidate.resourceId === 'string'
|
|
151
|
+
? candidate.resourceId.trim()
|
|
152
|
+
: '';
|
|
153
|
+
const provider = candidate.provider;
|
|
154
|
+
const routingDomain = candidate.routingDomain;
|
|
155
|
+
if (
|
|
156
|
+
candidate.schemaVersion !== 1 ||
|
|
157
|
+
(provider !== 'daytona' && provider !== 'modal') ||
|
|
158
|
+
!resourceId ||
|
|
159
|
+
resourceId !== suspension.sandboxId.trim() ||
|
|
160
|
+
(routingDomain !== null && typeof routingDomain !== 'string') ||
|
|
161
|
+
(typeof routingDomain === 'string' && !routingDomain.trim()) ||
|
|
162
|
+
(suspension.sandboxProvider !== undefined &&
|
|
163
|
+
suspension.sandboxProvider !== provider)
|
|
164
|
+
) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Persisted Runtime Sandbox Ref is invalid or inconsistent for sandbox ${suspension.sandboxId}.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
provider,
|
|
172
|
+
resourceId,
|
|
173
|
+
routingDomain:
|
|
174
|
+
typeof routingDomain === 'string' ? routingDomain.trim() : null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
suspension.sandboxProvider !== undefined &&
|
|
179
|
+
suspension.sandboxProvider !== 'daytona' &&
|
|
180
|
+
suspension.sandboxProvider !== 'modal'
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Legacy detached runner has unsupported sandbox provider ${String(suspension.sandboxProvider)}.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
schemaVersion: 1,
|
|
188
|
+
provider: suspension.sandboxProvider === 'modal' ? 'modal' : 'daytona',
|
|
189
|
+
resourceId: suspension.sandboxId,
|
|
190
|
+
// Legacy suspensions never persisted ownership. Cleanup still uses the
|
|
191
|
+
// provider-scoped credential; resource-backed cleanup carries the domain.
|
|
192
|
+
routingDomain: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const defaultRuntimeSandboxReconciliation =
|
|
197
|
+
createRuntimeSandboxReconciliation({
|
|
198
|
+
adapters: [
|
|
199
|
+
{
|
|
200
|
+
provider: 'daytona',
|
|
201
|
+
delete: async (ref) => {
|
|
202
|
+
const outcome = await deleteDaytonaSandboxByIdWithOutcome({
|
|
203
|
+
sandboxId: ref.resourceId,
|
|
204
|
+
timeoutSeconds: 30,
|
|
205
|
+
expectedOrganizationId: ref.routingDomain,
|
|
206
|
+
allowUnscopedAlreadyAbsent: ref.routingDomain === null,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
...outcome,
|
|
210
|
+
routingDomain: outcome.organizationId,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
readWorkCompletedAt: (ref, path) =>
|
|
214
|
+
readDetachedDaytonaRuntimeCompletion({
|
|
215
|
+
sandboxId: ref.resourceId,
|
|
216
|
+
runtimeCompletedPath: path,
|
|
217
|
+
expectedOrganizationId: ref.routingDomain,
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
provider: 'modal',
|
|
222
|
+
delete: async (ref) => {
|
|
223
|
+
const outcome = await deleteModalSandboxById({
|
|
224
|
+
sandboxId: ref.resourceId,
|
|
225
|
+
expectedAppId: ref.routingDomain,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
...outcome,
|
|
229
|
+
routingDomain: outcome.appId,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
readWorkCompletedAt: (ref, path) =>
|
|
233
|
+
readDetachedModalRuntimeCompletion({
|
|
234
|
+
sandboxId: ref.resourceId,
|
|
235
|
+
runtimeCompletedPath: path,
|
|
236
|
+
expectedAppId: ref.routingDomain,
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|