deepline 0.1.206 → 0.1.207
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/apps/play-runner-workers/src/entry.ts +25 -16
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +96 -12
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +51 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-lifecycle.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +321 -175
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +13 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +16 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +161 -81
- package/dist/bundling-sources/shared_libs/play-runtime/governor/coordinator-rate-state-backend.ts +26 -9
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +109 -1
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-scope.ts +256 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +17 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +103 -18
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-pg-driver-pg.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +74 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -12
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -1
- package/dist/bundling-sources/shared_libs/runtime-env.ts +55 -0
- package/dist/cli/index.js +27 -6
- package/dist/cli/index.mjs +27 -6
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRuntimeExecutionCapability,
|
|
3
|
+
type RuntimeExecutionCapability,
|
|
4
|
+
} from './execution-capabilities';
|
|
5
|
+
|
|
6
|
+
export type RunExecutionPlacementKind =
|
|
7
|
+
| 'root'
|
|
8
|
+
| 'inline_child'
|
|
9
|
+
| 'scheduled_child';
|
|
10
|
+
|
|
11
|
+
export type RunExecutionAuthorityInput = {
|
|
12
|
+
readonly orgId: string;
|
|
13
|
+
readonly workflowId?: string;
|
|
14
|
+
readonly billingRunId?: string | null;
|
|
15
|
+
readonly maxCreditsPerRun?: number | null;
|
|
16
|
+
readonly integrationMode?: 'live' | 'eval_stub' | 'fixture' | null;
|
|
17
|
+
readonly synthetic?: boolean;
|
|
18
|
+
readonly capabilities: readonly RuntimeExecutionCapability[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RunExecutionScope = Readonly<{
|
|
22
|
+
logical: Readonly<{
|
|
23
|
+
runId: string;
|
|
24
|
+
playId: string;
|
|
25
|
+
parentRunId: string | null;
|
|
26
|
+
parentPlayId: string | null;
|
|
27
|
+
rootRunId: string;
|
|
28
|
+
rootPlayId: string;
|
|
29
|
+
}>;
|
|
30
|
+
receipt: Readonly<{
|
|
31
|
+
ownerRunId: string;
|
|
32
|
+
ownerAttempt: number;
|
|
33
|
+
namespace: string;
|
|
34
|
+
}>;
|
|
35
|
+
placement: Readonly<{
|
|
36
|
+
kind: RunExecutionPlacementKind;
|
|
37
|
+
executorRunId: string;
|
|
38
|
+
executorPlayId: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** Persistable runtime authority facts. Bearer tokens and secret names do not belong here. */
|
|
41
|
+
authority: Readonly<{
|
|
42
|
+
orgId: string;
|
|
43
|
+
workflowId: string;
|
|
44
|
+
runId: string;
|
|
45
|
+
playId: string;
|
|
46
|
+
billingRunId: string | null;
|
|
47
|
+
maxCreditsPerRun: number | null;
|
|
48
|
+
integrationMode: 'live' | 'eval_stub' | 'fixture' | null;
|
|
49
|
+
synthetic: boolean;
|
|
50
|
+
capabilities: readonly RuntimeExecutionCapability[];
|
|
51
|
+
}>;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
export type RootRunExecutionScopeInput = {
|
|
55
|
+
readonly runId: string;
|
|
56
|
+
readonly playId: string;
|
|
57
|
+
readonly attempt: number;
|
|
58
|
+
readonly receiptNamespace: string;
|
|
59
|
+
readonly authority: RunExecutionAuthorityInput;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ChildRunExecutionScopeInput =
|
|
63
|
+
| {
|
|
64
|
+
readonly placement: 'inline';
|
|
65
|
+
readonly runId: string;
|
|
66
|
+
readonly playId: string;
|
|
67
|
+
readonly receiptNamespace: string;
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
readonly placement: 'scheduled';
|
|
71
|
+
readonly runId: string;
|
|
72
|
+
readonly playId: string;
|
|
73
|
+
readonly attempt: number;
|
|
74
|
+
readonly receiptNamespace: string;
|
|
75
|
+
readonly authority?: Partial<Omit<RunExecutionAuthorityInput, 'orgId'>>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function requireIdentity(value: string, field: string): string {
|
|
79
|
+
if (
|
|
80
|
+
typeof value !== 'string' ||
|
|
81
|
+
value.length === 0 ||
|
|
82
|
+
value !== value.trim()
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(`${field} must be a non-empty, trimmed string.`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function requireAttempt(value: number, field: string): number {
|
|
90
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
91
|
+
throw new Error(`${field} must be a non-negative safe integer.`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function requireMaxCredits(value: number | null | undefined): number | null {
|
|
97
|
+
if (value == null) return null;
|
|
98
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'authority.maxCreditsPerRun must be a non-negative number.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requireCapabilities(
|
|
107
|
+
values: readonly RuntimeExecutionCapability[],
|
|
108
|
+
): readonly RuntimeExecutionCapability[] {
|
|
109
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'authority.capabilities must contain at least one capability.',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const capabilities = [...new Set(values)];
|
|
115
|
+
if (!capabilities.every(isRuntimeExecutionCapability)) {
|
|
116
|
+
throw new Error('authority.capabilities contains an unknown capability.');
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze(capabilities.sort());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function authorityMetadata(input: {
|
|
122
|
+
runId: string;
|
|
123
|
+
playId: string;
|
|
124
|
+
orgId: string;
|
|
125
|
+
authority: Omit<RunExecutionAuthorityInput, 'orgId'>;
|
|
126
|
+
}): RunExecutionScope['authority'] {
|
|
127
|
+
return Object.freeze({
|
|
128
|
+
orgId: requireIdentity(input.orgId, 'authority.orgId'),
|
|
129
|
+
workflowId: requireIdentity(
|
|
130
|
+
input.authority.workflowId ?? input.runId,
|
|
131
|
+
'authority.workflowId',
|
|
132
|
+
),
|
|
133
|
+
runId: input.runId,
|
|
134
|
+
playId: input.playId,
|
|
135
|
+
billingRunId:
|
|
136
|
+
input.authority.billingRunId == null
|
|
137
|
+
? null
|
|
138
|
+
: requireIdentity(
|
|
139
|
+
input.authority.billingRunId,
|
|
140
|
+
'authority.billingRunId',
|
|
141
|
+
),
|
|
142
|
+
maxCreditsPerRun: requireMaxCredits(input.authority.maxCreditsPerRun),
|
|
143
|
+
integrationMode: input.authority.integrationMode ?? null,
|
|
144
|
+
synthetic: input.authority.synthetic === true,
|
|
145
|
+
capabilities: requireCapabilities(input.authority.capabilities),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function freezeScope(input: RunExecutionScope): RunExecutionScope {
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
logical: Object.freeze(input.logical),
|
|
152
|
+
receipt: Object.freeze(input.receipt),
|
|
153
|
+
placement: Object.freeze(input.placement),
|
|
154
|
+
authority: input.authority,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function createRootRunExecutionScope(
|
|
159
|
+
input: RootRunExecutionScopeInput,
|
|
160
|
+
): RunExecutionScope {
|
|
161
|
+
const runId = requireIdentity(input.runId, 'runId');
|
|
162
|
+
const playId = requireIdentity(input.playId, 'playId');
|
|
163
|
+
const attempt = requireAttempt(input.attempt, 'attempt');
|
|
164
|
+
const namespace = requireIdentity(input.receiptNamespace, 'receiptNamespace');
|
|
165
|
+
|
|
166
|
+
return freezeScope({
|
|
167
|
+
logical: {
|
|
168
|
+
runId,
|
|
169
|
+
playId,
|
|
170
|
+
parentRunId: null,
|
|
171
|
+
parentPlayId: null,
|
|
172
|
+
rootRunId: runId,
|
|
173
|
+
rootPlayId: playId,
|
|
174
|
+
},
|
|
175
|
+
receipt: { ownerRunId: runId, ownerAttempt: attempt, namespace },
|
|
176
|
+
placement: { kind: 'root', executorRunId: runId, executorPlayId: playId },
|
|
177
|
+
authority: authorityMetadata({
|
|
178
|
+
runId,
|
|
179
|
+
playId,
|
|
180
|
+
orgId: input.authority.orgId,
|
|
181
|
+
authority: input.authority,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function deriveChildRunExecutionScope(
|
|
187
|
+
parent: RunExecutionScope,
|
|
188
|
+
input: ChildRunExecutionScopeInput,
|
|
189
|
+
): RunExecutionScope {
|
|
190
|
+
const runId = requireIdentity(input.runId, 'runId');
|
|
191
|
+
const playId = requireIdentity(input.playId, 'playId');
|
|
192
|
+
const namespace = requireIdentity(input.receiptNamespace, 'receiptNamespace');
|
|
193
|
+
if (runId === parent.logical.runId) {
|
|
194
|
+
throw new Error('A child runId must differ from its parent runId.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const logical: RunExecutionScope['logical'] = {
|
|
198
|
+
runId,
|
|
199
|
+
playId,
|
|
200
|
+
parentRunId: parent.logical.runId,
|
|
201
|
+
parentPlayId: parent.logical.playId,
|
|
202
|
+
rootRunId: parent.logical.rootRunId,
|
|
203
|
+
rootPlayId: parent.logical.rootPlayId,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (input.placement === 'inline') {
|
|
207
|
+
return freezeScope({
|
|
208
|
+
logical,
|
|
209
|
+
receipt: {
|
|
210
|
+
ownerRunId: parent.receipt.ownerRunId,
|
|
211
|
+
ownerAttempt: parent.receipt.ownerAttempt,
|
|
212
|
+
namespace,
|
|
213
|
+
},
|
|
214
|
+
placement: {
|
|
215
|
+
kind: 'inline_child',
|
|
216
|
+
executorRunId: parent.placement.executorRunId,
|
|
217
|
+
executorPlayId: parent.placement.executorPlayId,
|
|
218
|
+
},
|
|
219
|
+
authority: parent.authority,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const attempt = requireAttempt(input.attempt, 'attempt');
|
|
224
|
+
return freezeScope({
|
|
225
|
+
logical,
|
|
226
|
+
receipt: { ownerRunId: runId, ownerAttempt: attempt, namespace },
|
|
227
|
+
placement: {
|
|
228
|
+
kind: 'scheduled_child',
|
|
229
|
+
executorRunId: runId,
|
|
230
|
+
executorPlayId: playId,
|
|
231
|
+
},
|
|
232
|
+
authority: authorityMetadata({
|
|
233
|
+
runId,
|
|
234
|
+
playId,
|
|
235
|
+
orgId: parent.authority.orgId,
|
|
236
|
+
authority: {
|
|
237
|
+
workflowId: input.authority?.workflowId,
|
|
238
|
+
billingRunId:
|
|
239
|
+
input.authority?.billingRunId === undefined
|
|
240
|
+
? parent.authority.billingRunId
|
|
241
|
+
: input.authority.billingRunId,
|
|
242
|
+
maxCreditsPerRun:
|
|
243
|
+
input.authority?.maxCreditsPerRun === undefined
|
|
244
|
+
? parent.authority.maxCreditsPerRun
|
|
245
|
+
: input.authority.maxCreditsPerRun,
|
|
246
|
+
integrationMode:
|
|
247
|
+
input.authority?.integrationMode === undefined
|
|
248
|
+
? parent.authority.integrationMode
|
|
249
|
+
: input.authority.integrationMode,
|
|
250
|
+
synthetic: input.authority?.synthetic ?? parent.authority.synthetic,
|
|
251
|
+
capabilities:
|
|
252
|
+
input.authority?.capabilities ?? parent.authority.capabilities,
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
}
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -39,12 +39,15 @@ export type DaytonaStageEmitter = (
|
|
|
39
39
|
export type AcquiredDaytonaSandbox = {
|
|
40
40
|
sandbox: DaytonaSandbox;
|
|
41
41
|
billingStartedAt: number;
|
|
42
|
+
billingEndedAt?: number;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export type OneShotDaytonaSandboxLifecycle = {
|
|
45
46
|
startedAt: number;
|
|
46
47
|
acquiredSandboxPromise: Promise<AcquiredDaytonaSandbox>;
|
|
47
48
|
createFreshSandbox: () => Promise<AcquiredDaytonaSandbox>;
|
|
49
|
+
acquiredSandboxes: () => readonly AcquiredDaytonaSandbox[];
|
|
50
|
+
settlePendingCreates: () => Promise<void>;
|
|
48
51
|
dispose: () => Promise<void>;
|
|
49
52
|
};
|
|
50
53
|
|
|
@@ -269,8 +272,9 @@ async function acquireOneShotDaytonaSandbox(input: {
|
|
|
269
272
|
);
|
|
270
273
|
}
|
|
271
274
|
const billingStartedAt = Date.now();
|
|
275
|
+
const sandbox = result.sandbox;
|
|
272
276
|
input.emitStage('create:done', {
|
|
273
|
-
sandboxId:
|
|
277
|
+
sandboxId: sandbox.id,
|
|
274
278
|
attempt: result.attempt,
|
|
275
279
|
elapsedMs: billingStartedAt - input.startedAt,
|
|
276
280
|
attemptElapsedMs: result.attemptElapsedMs,
|
|
@@ -278,10 +282,7 @@ async function acquireOneShotDaytonaSandbox(input: {
|
|
|
278
282
|
memoryGiB: granted.memoryGiB,
|
|
279
283
|
diskGiB: granted.diskGiB,
|
|
280
284
|
});
|
|
281
|
-
return {
|
|
282
|
-
sandbox: result.sandbox,
|
|
283
|
-
billingStartedAt,
|
|
284
|
-
};
|
|
285
|
+
return { sandbox, billingStartedAt };
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
@@ -293,6 +294,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
|
293
294
|
const orgId = validateDaytonaExecutionContext(input.context);
|
|
294
295
|
const startedAt = input.startedAt ?? Date.now();
|
|
295
296
|
let disposed = false;
|
|
297
|
+
const acquiredSandboxes = new Map<string, AcquiredDaytonaSandbox>();
|
|
296
298
|
let latestAcquiredSandboxPromise: Promise<AcquiredDaytonaSandbox>;
|
|
297
299
|
const createFreshSandbox = () => {
|
|
298
300
|
latestAcquiredSandboxPromise = acquireOneShotDaytonaSandbox({
|
|
@@ -301,6 +303,9 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
|
301
303
|
context: input.context,
|
|
302
304
|
emitStage: input.emitStage,
|
|
303
305
|
startedAt,
|
|
306
|
+
}).then((acquired) => {
|
|
307
|
+
acquiredSandboxes.set(acquired.sandbox.id, acquired);
|
|
308
|
+
return acquired;
|
|
304
309
|
});
|
|
305
310
|
return latestAcquiredSandboxPromise;
|
|
306
311
|
};
|
|
@@ -310,6 +315,13 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
|
310
315
|
startedAt,
|
|
311
316
|
acquiredSandboxPromise,
|
|
312
317
|
createFreshSandbox,
|
|
318
|
+
acquiredSandboxes: () => [...acquiredSandboxes.values()],
|
|
319
|
+
settlePendingCreates: async () => {
|
|
320
|
+
await latestAcquiredSandboxPromise.then(
|
|
321
|
+
() => undefined,
|
|
322
|
+
() => undefined,
|
|
323
|
+
);
|
|
324
|
+
},
|
|
313
325
|
dispose: async () => {
|
|
314
326
|
if (disposed) return;
|
|
315
327
|
disposed = true;
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
PlayRunnerPreparedExecution,
|
|
5
5
|
PlayRunnerPrepareInput,
|
|
6
6
|
} from '../types';
|
|
7
|
+
import { RuntimeResourceFenceLostError } from '../types';
|
|
7
8
|
import { buildPlayRunnerBundle } from '../bundle';
|
|
8
9
|
import {
|
|
9
10
|
findPlayRunnerResult,
|
|
@@ -113,6 +114,8 @@ export async function withDaytonaOperationDeadline<T>(input: {
|
|
|
113
114
|
}
|
|
114
115
|
const RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN =
|
|
115
116
|
/\bRuntime Postgres\b.*\b(connection timed out|connect timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|Connection terminated|Connection ended unexpectedly)\b/i;
|
|
117
|
+
const RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN =
|
|
118
|
+
/AppRuntimeApiTransportError: App runtime API transport exhausted action=(?:get|claim|mark|complete|fail|heartbeat|release|skip)_runtime_step_receipts?/i;
|
|
116
119
|
const RUNTIME_API_TRANSPORT_RETRY_PATTERN =
|
|
117
120
|
/\bRuntime API request to .+ failed before receiving a response:\s*(?:fetch failed|connection (?:terminated|timed out|closed|reset)|ECONNRESET|ETIMEDOUT|ECONNREFUSED)\b/i;
|
|
118
121
|
const DAYTONA_INFRASTRUCTURE_RETRY_PATTERN =
|
|
@@ -498,11 +501,18 @@ function createDaytonaFailedResult(input: {
|
|
|
498
501
|
|
|
499
502
|
function retryableRuntimeInitializationFailureReason(
|
|
500
503
|
result: PlayRunnerResult,
|
|
501
|
-
):
|
|
504
|
+
):
|
|
505
|
+
| 'runtime_postgres_connect'
|
|
506
|
+
| 'runtime_api_transport'
|
|
507
|
+
| 'runtime_receipt_gateway_transport'
|
|
508
|
+
| null {
|
|
502
509
|
if (result.status !== 'failed') return null;
|
|
503
510
|
if (RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN.test(result.error)) {
|
|
504
511
|
return 'runtime_postgres_connect';
|
|
505
512
|
}
|
|
513
|
+
if (RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN.test(result.error)) {
|
|
514
|
+
return 'runtime_receipt_gateway_transport';
|
|
515
|
+
}
|
|
506
516
|
const rowsProcessed = Number(result.stats?.rowsProcessed ?? 0);
|
|
507
517
|
if (
|
|
508
518
|
rowsProcessed === 0 &&
|
|
@@ -617,13 +627,69 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
617
627
|
};
|
|
618
628
|
let cancellationRequested = false;
|
|
619
629
|
let cancellationCleanupStarted = false;
|
|
630
|
+
let activeAcquiredResource: {
|
|
631
|
+
sandbox: DaytonaSandbox;
|
|
632
|
+
billingStartedAt: number;
|
|
633
|
+
billingEndedAt?: number;
|
|
634
|
+
} | null = null;
|
|
620
635
|
const uploadDeadlineBreaches: DaytonaUploadAttemptTiming[] = [];
|
|
636
|
+
const reportedSandboxIds = new Set<string>();
|
|
637
|
+
const reportedSandboxEndTimes = new Map<string, number | null>();
|
|
638
|
+
const runtimeResourceReportErrors = new Set<unknown>();
|
|
639
|
+
const reportRuntimeResource = async (acquired: {
|
|
640
|
+
sandbox: DaytonaSandbox;
|
|
641
|
+
billingStartedAt: number;
|
|
642
|
+
billingEndedAt?: number;
|
|
643
|
+
}) => {
|
|
644
|
+
const sandbox = acquired.sandbox;
|
|
645
|
+
const billingEndedAt = acquired.billingEndedAt ?? null;
|
|
646
|
+
if (
|
|
647
|
+
reportedSandboxIds.has(sandbox.id) &&
|
|
648
|
+
reportedSandboxEndTimes.get(sandbox.id) === billingEndedAt
|
|
649
|
+
) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
await callbacks?.onRuntimeResourceAcquired?.({
|
|
654
|
+
kind: 'daytona_sandbox',
|
|
655
|
+
sandboxId: sandbox.id,
|
|
656
|
+
billingStartedAt: acquired.billingStartedAt,
|
|
657
|
+
billingEndedAt,
|
|
658
|
+
cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
|
|
659
|
+
memoryGiB: typeof sandbox.memory === 'number' ? sandbox.memory : null,
|
|
660
|
+
diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : null,
|
|
661
|
+
});
|
|
662
|
+
} catch (error) {
|
|
663
|
+
runtimeResourceReportErrors.add(error);
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
reportedSandboxIds.add(sandbox.id);
|
|
667
|
+
reportedSandboxEndTimes.set(sandbox.id, billingEndedAt);
|
|
668
|
+
};
|
|
669
|
+
const reportRetiringRuntimeResource = async (acquired: {
|
|
670
|
+
sandbox: DaytonaSandbox;
|
|
671
|
+
billingStartedAt: number;
|
|
672
|
+
billingEndedAt?: number;
|
|
673
|
+
}) => {
|
|
674
|
+
try {
|
|
675
|
+
await reportRuntimeResource(acquired);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error instanceof RuntimeResourceFenceLostError) throw error;
|
|
678
|
+
console.warn('[play-runner.daytona.resource_report_deferred]', {
|
|
679
|
+
sandboxId: acquired.sandbox.id,
|
|
680
|
+
error: error instanceof Error ? error.message : String(error),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
};
|
|
621
684
|
let cancelExecution!: (error: Error) => void;
|
|
622
685
|
const cancellationPromise = new Promise<never>((_resolve, reject) => {
|
|
623
686
|
cancelExecution = reject;
|
|
624
687
|
});
|
|
625
688
|
const onCancel = () => {
|
|
626
689
|
cancellationRequested = true;
|
|
690
|
+
if (activeAcquiredResource && !activeAcquiredResource.billingEndedAt) {
|
|
691
|
+
activeAcquiredResource.billingEndedAt = Date.now();
|
|
692
|
+
}
|
|
627
693
|
if (cancellationCleanupStarted) {
|
|
628
694
|
cancelExecution(new Error(DAYTONA_CANCELLED_ERROR));
|
|
629
695
|
return;
|
|
@@ -663,28 +729,20 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
663
729
|
acquiredSandboxPromise,
|
|
664
730
|
cancellationPromise,
|
|
665
731
|
]);
|
|
732
|
+
activeAcquiredResource = acquiredSandbox;
|
|
666
733
|
sandboxCleanup.activate(acquiredSandbox);
|
|
667
734
|
runtimeTiming.daytonaCreateMs =
|
|
668
735
|
acquiredSandbox.billingStartedAt - startedAt;
|
|
669
736
|
const sandbox = acquiredSandbox.sandbox;
|
|
670
737
|
sandboxForAttempt = sandbox;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : null,
|
|
680
|
-
},
|
|
681
|
-
);
|
|
682
|
-
if (resourceAcquiredNotice) {
|
|
683
|
-
await withDaytonaOperationDeadline({
|
|
684
|
-
operation: 'onRuntimeResourceAcquired',
|
|
685
|
-
deadlineMs: DAYTONA_API_CALL_DEADLINE_MS,
|
|
686
|
-
promise: resourceAcquiredNotice,
|
|
687
|
-
});
|
|
738
|
+
await reportRuntimeResource(acquiredSandbox);
|
|
739
|
+
// Account for every acquired retry sandbox before customer code runs.
|
|
740
|
+
// This keeps resource persistence failures on the pre-side-effect
|
|
741
|
+
// side of the execution boundary and makes cancellation billing
|
|
742
|
+
// complete without a post-terminal write race.
|
|
743
|
+
await sandboxLifecycle.settlePendingCreates();
|
|
744
|
+
for (const acquired of sandboxLifecycle.acquiredSandboxes()) {
|
|
745
|
+
await reportRuntimeResource(acquired);
|
|
688
746
|
}
|
|
689
747
|
throwIfCancellationRequested();
|
|
690
748
|
|
|
@@ -748,6 +806,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
748
806
|
elapsedMs: Date.now() - startedAt,
|
|
749
807
|
});
|
|
750
808
|
if (executionAttempt < DAYTONA_UPLOAD_MAX_ATTEMPTS) {
|
|
809
|
+
acquiredSandbox.billingEndedAt = Date.now();
|
|
810
|
+
await reportRetiringRuntimeResource(acquiredSandbox);
|
|
751
811
|
sandboxCleanup.stashActiveSandboxForRetry();
|
|
752
812
|
emitDaytonaStage(callbacks, config.context, 'recreate:start', {
|
|
753
813
|
previousSandboxId: sandbox.id,
|
|
@@ -893,6 +953,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
893
953
|
error: result.error,
|
|
894
954
|
elapsedMs: Date.now() - startedAt,
|
|
895
955
|
});
|
|
956
|
+
acquiredSandbox.billingEndedAt = Date.now();
|
|
957
|
+
await reportRetiringRuntimeResource(acquiredSandbox);
|
|
896
958
|
sandboxCleanup.stashActiveSandboxForRetry();
|
|
897
959
|
acquiredSandboxPromise = sandboxLifecycle.createFreshSandbox();
|
|
898
960
|
continue;
|
|
@@ -964,6 +1026,8 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
964
1026
|
elapsedMs: Date.now() - startedAt,
|
|
965
1027
|
});
|
|
966
1028
|
if (sandboxForAttempt) {
|
|
1029
|
+
activeAcquiredResource!.billingEndedAt = Date.now();
|
|
1030
|
+
await reportRetiringRuntimeResource(activeAcquiredResource!);
|
|
967
1031
|
sandboxCleanup.stashActiveSandboxForRetry();
|
|
968
1032
|
}
|
|
969
1033
|
acquiredSandboxPromise = sandboxLifecycle.createFreshSandbox();
|
|
@@ -981,6 +1045,11 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
981
1045
|
}),
|
|
982
1046
|
);
|
|
983
1047
|
} catch (error) {
|
|
1048
|
+
// Resource persistence belongs to the scheduler control plane. Preserve
|
|
1049
|
+
// its typed capacity/fence errors so Absurd can defer or fence the
|
|
1050
|
+
// attempt; converting them into a runner failure would terminally fail a
|
|
1051
|
+
// play that never began executing customer code.
|
|
1052
|
+
if (runtimeResourceReportErrors.has(error)) throw error;
|
|
984
1053
|
emitDaytonaStage(callbacks, config.context, 'execute:error', {
|
|
985
1054
|
sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
|
|
986
1055
|
error: formatDaytonaError(error),
|
|
@@ -999,6 +1068,22 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
999
1068
|
);
|
|
1000
1069
|
} finally {
|
|
1001
1070
|
callbacks?.cancellationSignal?.removeEventListener('abort', onCancel);
|
|
1071
|
+
if (cancellationRequested) {
|
|
1072
|
+
await sandboxLifecycle.settlePendingCreates();
|
|
1073
|
+
for (const acquired of sandboxLifecycle.acquiredSandboxes()) {
|
|
1074
|
+
try {
|
|
1075
|
+
await reportRuntimeResource(acquired);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
console.error(
|
|
1078
|
+
'[play-runner.daytona.cancel_resource_report_failed]',
|
|
1079
|
+
{
|
|
1080
|
+
sandboxId: acquired.sandbox.id,
|
|
1081
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1002
1087
|
}
|
|
1003
1088
|
},
|
|
1004
1089
|
};
|
|
@@ -14,11 +14,19 @@ export type PlayRunnerRuntimeResource =
|
|
|
14
14
|
kind: 'daytona_sandbox';
|
|
15
15
|
sandboxId: string;
|
|
16
16
|
billingStartedAt: number;
|
|
17
|
+
billingEndedAt?: number | null;
|
|
17
18
|
cpu?: number | null;
|
|
18
19
|
memoryGiB?: number | null;
|
|
19
20
|
diskGiB?: number | null;
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
export class RuntimeResourceFenceLostError extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'RuntimeResourceFenceLostError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
export interface PlayRunnerCallbacks {
|
|
23
31
|
onLog?: (event: PlayRunnerLogEvent) => void;
|
|
24
32
|
onCheckpoint?: (checkpoint: PlayCheckpoint) => void;
|
|
@@ -45,6 +45,11 @@ const pgRuntimePoolFactory: RuntimePoolFactory = (input): RuntimePool => {
|
|
|
45
45
|
idleTimeoutMillis: input.idleTimeoutMs ?? 15_000,
|
|
46
46
|
connectionTimeoutMillis: input.connectTimeoutMs ?? 10_000,
|
|
47
47
|
});
|
|
48
|
+
pool.on('error', (error) => {
|
|
49
|
+
console.warn(
|
|
50
|
+
`[warn] runtime Postgres discarded an idle client after an error: ${error.message}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
48
53
|
return wrapPgPool(pool);
|
|
49
54
|
};
|
|
50
55
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveDaytonaSandboxComputeItem,
|
|
3
|
+
type ComputeBillingItem,
|
|
4
|
+
} from './worker-api-types';
|
|
5
|
+
|
|
6
|
+
function finiteNonNegative(value: unknown): number | null {
|
|
7
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
8
|
+
? Math.max(0, value)
|
|
9
|
+
: null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Project executor resource facts into one idempotent item per real sandbox. */
|
|
13
|
+
export function resolveDaytonaRuntimeComputeItems(input: {
|
|
14
|
+
resources: readonly unknown[];
|
|
15
|
+
endedAt: number;
|
|
16
|
+
}): ComputeBillingItem[] {
|
|
17
|
+
const resourcesBySandboxId = new Map<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
startedAt: number;
|
|
21
|
+
endedAt: number | null;
|
|
22
|
+
cpu: number | null;
|
|
23
|
+
memoryGiB: number | null;
|
|
24
|
+
diskGiB: number | null;
|
|
25
|
+
}
|
|
26
|
+
>();
|
|
27
|
+
const items: ComputeBillingItem[] = [];
|
|
28
|
+
for (const resource of input.resources) {
|
|
29
|
+
if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const value = resource as Record<string, unknown>;
|
|
33
|
+
const sandboxId =
|
|
34
|
+
typeof value.sandboxId === 'string' ? value.sandboxId.trim() : '';
|
|
35
|
+
if (value.kind !== 'daytona_sandbox' || !sandboxId) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const billingStartedAt = finiteNonNegative(value.billingStartedAt);
|
|
39
|
+
if (billingStartedAt === null) continue;
|
|
40
|
+
const billingEndedAt = finiteNonNegative(value.billingEndedAt);
|
|
41
|
+
const existing = resourcesBySandboxId.get(sandboxId);
|
|
42
|
+
resourcesBySandboxId.set(sandboxId, {
|
|
43
|
+
startedAt: Math.min(
|
|
44
|
+
existing?.startedAt ?? billingStartedAt,
|
|
45
|
+
billingStartedAt,
|
|
46
|
+
),
|
|
47
|
+
endedAt:
|
|
48
|
+
billingEndedAt === null
|
|
49
|
+
? existing?.endedAt ?? null
|
|
50
|
+
: Math.max(existing?.endedAt ?? billingEndedAt, billingEndedAt),
|
|
51
|
+
cpu: finiteNonNegative(value.cpu) ?? existing?.cpu ?? null,
|
|
52
|
+
memoryGiB:
|
|
53
|
+
finiteNonNegative(value.memoryGiB) ?? existing?.memoryGiB ?? null,
|
|
54
|
+
diskGiB: finiteNonNegative(value.diskGiB) ?? existing?.diskGiB ?? null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const [sandboxId, resource] of resourcesBySandboxId) {
|
|
58
|
+
const endedAt = resource.endedAt ?? input.endedAt;
|
|
59
|
+
items.push(
|
|
60
|
+
resolveDaytonaSandboxComputeItem({
|
|
61
|
+
itemId: `daytona:${sandboxId}`,
|
|
62
|
+
source: 'daytona',
|
|
63
|
+
wallTimeSeconds: Math.max(0, endedAt - resource.startedAt) / 1_000,
|
|
64
|
+
cpu: resource.cpu ?? 1,
|
|
65
|
+
memoryGiB: resource.memoryGiB ?? 1,
|
|
66
|
+
diskGiB: resource.diskGiB ?? 0,
|
|
67
|
+
sandboxId,
|
|
68
|
+
startedAt: resource.startedAt,
|
|
69
|
+
endedAt,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getRuntimeEnv } from '../runtime-env';
|
|
2
|
+
|
|
1
3
|
export const PLAY_RUNTIME_TEST_FAULT_HEADER = 'x-deepline-test-fault';
|
|
2
4
|
|
|
3
5
|
export type RuntimeTestFaultName =
|
|
@@ -82,15 +84,6 @@ const RUNTIME_TEST_POLICY_WORK_BUDGET_KEYS = new Set([
|
|
|
82
84
|
const MAX_RUNTIME_TEST_POLICY_MS = 10 * 60_000;
|
|
83
85
|
const MAX_RUNTIME_TEST_POLICY_YIELD_LIMIT = 1_000_000;
|
|
84
86
|
|
|
85
|
-
function isProductionRuntime(): boolean {
|
|
86
|
-
const deeplineEnv = process.env.DEEPLINE_ENV?.trim().toLowerCase();
|
|
87
|
-
if (deeplineEnv === 'production' || deeplineEnv === 'prod') return true;
|
|
88
|
-
const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
|
|
89
|
-
if (vercelEnv === 'production') return true;
|
|
90
|
-
if (vercelEnv === 'preview' || vercelEnv === 'development') return false;
|
|
91
|
-
return process.env.NODE_ENV === 'production';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
87
|
function testRuntimeSeamsEnabled(): boolean {
|
|
95
88
|
return process.env.DEEPLINE_TEST_RUNTIME_SEAMS === '1';
|
|
96
89
|
}
|
|
@@ -243,7 +236,7 @@ export function validateRuntimeTestPolicyOverrides(input: {
|
|
|
243
236
|
if (input.value === undefined || input.value === null) {
|
|
244
237
|
return { ok: true, overrides: null };
|
|
245
238
|
}
|
|
246
|
-
if (!runtimeTestSeamsAuthorized(input) ||
|
|
239
|
+
if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
|
|
247
240
|
return {
|
|
248
241
|
ok: false,
|
|
249
242
|
status: 403,
|
|
@@ -270,7 +263,7 @@ export function readRuntimeTestFaultRegistry(input: {
|
|
|
270
263
|
if (!headerValue) {
|
|
271
264
|
return { ok: true, registry: null };
|
|
272
265
|
}
|
|
273
|
-
if (!runtimeTestSeamsAuthorized(input) ||
|
|
266
|
+
if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
|
|
274
267
|
return {
|
|
275
268
|
ok: false,
|
|
276
269
|
status: 403,
|
|
@@ -323,7 +316,7 @@ export function parseRuntimeTestFaultCounts(input: {
|
|
|
323
316
|
if (!headerValue) {
|
|
324
317
|
return { ok: true, counts: null, headerValue: null };
|
|
325
318
|
}
|
|
326
|
-
if (!runtimeTestSeamsAuthorized(input) ||
|
|
319
|
+
if (!runtimeTestSeamsAuthorized(input) || getRuntimeEnv() === 'prod') {
|
|
327
320
|
return {
|
|
328
321
|
ok: false,
|
|
329
322
|
status: 403,
|