deepline 0.2.0 → 0.2.1
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 +13 -2
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- 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 +106 -3
- 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 +33 -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 +5 -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 +4 -2
- package/dist/index.d.ts +4 -2
- 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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '@shared_libs/play-runtime/daytona-runtime-config';
|
|
25
25
|
import {
|
|
26
26
|
DAYTONA_CANCELLED_ERROR,
|
|
27
|
+
DaytonaSandboxAcquisitionUnavailableError,
|
|
27
28
|
createDaytonaSandboxCleanupManager,
|
|
28
29
|
createOneShotDaytonaSandboxLifecycle,
|
|
29
30
|
type DaytonaClient,
|
|
@@ -290,12 +291,29 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
290
291
|
export async function readDetachedDaytonaRuntimeCompletion(input: {
|
|
291
292
|
sandboxId: string;
|
|
292
293
|
runtimeCompletedPath: string;
|
|
294
|
+
expectedOrganizationId?: string | null;
|
|
293
295
|
}): Promise<number | null> {
|
|
294
296
|
try {
|
|
295
297
|
const { clientOptions } = loadDaytonaRequiredConfig();
|
|
296
298
|
const sandbox = (await daytonaSdkClientFactory
|
|
297
299
|
.createFull(clientOptions)
|
|
298
300
|
.get(input.sandboxId)) as DaytonaSandbox;
|
|
301
|
+
const expectedOrganizationId = input.expectedOrganizationId?.trim() || null;
|
|
302
|
+
const observedOrganizationId = sandbox.organizationId?.trim() || null;
|
|
303
|
+
if (
|
|
304
|
+
expectedOrganizationId &&
|
|
305
|
+
observedOrganizationId !== expectedOrganizationId
|
|
306
|
+
) {
|
|
307
|
+
console.warn(
|
|
308
|
+
'[play-runner.daytona.runtime_completion_wrong_routing_domain]',
|
|
309
|
+
{
|
|
310
|
+
sandboxId: input.sandboxId,
|
|
311
|
+
expectedOrganizationId,
|
|
312
|
+
observedOrganizationId,
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
299
317
|
const marker = JSON.parse(
|
|
300
318
|
(await sandbox.fs.downloadFile(input.runtimeCompletedPath, 5)).toString(
|
|
301
319
|
'utf-8',
|
|
@@ -790,6 +808,10 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
790
808
|
await callbacks?.onRuntimeResourceAcquired?.({
|
|
791
809
|
kind: 'daytona_sandbox',
|
|
792
810
|
sandboxId: sandbox.id,
|
|
811
|
+
runtimeEnvironment:
|
|
812
|
+
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
813
|
+
? 'preview'
|
|
814
|
+
: 'production',
|
|
793
815
|
daytonaEnvironment:
|
|
794
816
|
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
795
817
|
? 'preview'
|
|
@@ -1031,6 +1053,14 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
1031
1053
|
kind: 'detached_runner',
|
|
1032
1054
|
boundaryId: `detached-runner:${push.runId}:${runnerAttempt}`,
|
|
1033
1055
|
runnerAttempt,
|
|
1056
|
+
sandboxProvider: 'daytona',
|
|
1057
|
+
runtimeSandboxRef: {
|
|
1058
|
+
schemaVersion: 1,
|
|
1059
|
+
provider: 'daytona',
|
|
1060
|
+
resourceId: sandbox.id,
|
|
1061
|
+
routingDomain:
|
|
1062
|
+
activeAcquiredResource?.daytonaOrganizationId ?? null,
|
|
1063
|
+
},
|
|
1034
1064
|
sandboxId: sandbox.id,
|
|
1035
1065
|
sessionId: start.sessionId,
|
|
1036
1066
|
cmdId: start.cmdId,
|
|
@@ -1121,6 +1151,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
1121
1151
|
}
|
|
1122
1152
|
throw error;
|
|
1123
1153
|
}
|
|
1154
|
+
if (error instanceof DaytonaSandboxAcquisitionUnavailableError) {
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1124
1157
|
emitDaytonaStage(callbacks, config.context, 'execute:error', {
|
|
1125
1158
|
sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
|
|
1126
1159
|
error: formatDaytonaError(error),
|
|
@@ -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 };
|