deepline 0.3.473 → 0.3.475
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-core.ts +4 -1
- package/dist/bundling-sources/sdk/src/play.ts +7 -2
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/billing-entitlements.ts +3 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +101 -2
- package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +62 -20
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +24 -8
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +19 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +196 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +40 -17
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal-resource-usage.ts +222 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +46 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +9 -5
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +39 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +18 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +64 -35
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +36 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +18 -8
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/sandbox-runtime-limits.ts +64 -16
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/billing-sales.ts +14 -1
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +40 -0
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +23 -1
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +18 -0
- package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +2 -1
- package/dist/cli/auth-7GVCW24N.js +41 -0
- package/dist/cli/{auth-KLJOQ4DZ.mjs → auth-CFDYJ7FZ.mjs} +5 -5
- package/dist/cli/{billing-DNDYNBQ7.mjs → billing-PEXWWLRT.mjs} +12 -10
- package/dist/cli/{billing-IQNJXKH5.js → billing-XLIL3FLK.js} +85 -83
- package/dist/cli/{chunk-4U3RNA3J.js → chunk-3MQJJ6YX.js} +51 -51
- package/dist/cli/{chunk-EUHTZV2E.mjs → chunk-3QSFSHCJ.mjs} +1 -1
- package/dist/cli/{chunk-CNCK2D6B.mjs → chunk-3WVA7E7O.mjs} +2 -2
- package/dist/cli/{chunk-NTFM7JQW.mjs → chunk-42F6LTBJ.mjs} +3 -3
- package/dist/cli/{chunk-V25CXRQ7.mjs → chunk-4QHAP3MR.mjs} +5 -5
- package/dist/cli/{chunk-3LFSIUFI.js → chunk-4QHQ3WYW.js} +21 -21
- package/dist/cli/{chunk-6ZTSHKJF.js → chunk-5QS4T4KR.js} +10 -3
- package/dist/cli/{chunk-VS7JKS22.mjs → chunk-6PRYHABU.mjs} +1 -1
- package/dist/cli/{chunk-63OOFT6B.js → chunk-7SWRXZ4A.js} +93 -93
- package/dist/cli/{chunk-6YOGOXOJ.mjs → chunk-7U3L2S7X.mjs} +10 -10
- package/dist/cli/{chunk-D4B3EV55.js → chunk-7XTBEYLS.js} +36 -36
- package/dist/cli/{chunk-QD6IW2HP.mjs → chunk-AD7KL6AI.mjs} +4 -4
- package/dist/cli/{chunk-YH5SFKHJ.js → chunk-B7NJ5JQ2.js} +4 -4
- package/dist/cli/{chunk-F4PUDYBY.js → chunk-BLW2K6YH.js} +3 -3
- package/dist/cli/{chunk-N4ZCMQ54.js → chunk-BSK2TGB4.js} +8 -8
- package/dist/cli/{chunk-QG3THSTN.mjs → chunk-BVAYLXMH.mjs} +2 -2
- package/dist/cli/{chunk-IC6AHOYS.js → chunk-CKSRTD45.js} +1 -1
- package/dist/cli/{chunk-RVRG57OB.js → chunk-D3Q4FUFS.js} +8 -8
- package/dist/cli/{chunk-ABLEU3QI.mjs → chunk-D6SXN34W.mjs} +4 -4
- package/dist/cli/{chunk-GTH7JORC.mjs → chunk-DIMKSDF6.mjs} +2 -2
- package/dist/cli/{chunk-Z52WEIBR.js → chunk-DKR7QPEO.js} +11 -7
- package/dist/cli/{chunk-YUNPYX5T.mjs → chunk-DUGZKPRK.mjs} +10 -3
- package/dist/cli/{chunk-6QS7522K.mjs → chunk-EA45HSEC.mjs} +2 -2
- package/dist/cli/{chunk-TUFQFTOC.js → chunk-F4FNMXI3.js} +53 -53
- package/dist/cli/{chunk-3OPQLFUK.mjs → chunk-FC4C7MZM.mjs} +4 -4
- package/dist/cli/{chunk-WGQKOLXH.js → chunk-G4SGYWKV.js} +6 -6
- package/dist/cli/{chunk-GISBRE2H.mjs → chunk-HALAYT3Y.mjs} +3 -3
- package/dist/cli/{chunk-GQBAJTQU.mjs → chunk-HMHH7JQD.mjs} +2 -2
- package/dist/cli/{chunk-PMJAGTSQ.js → chunk-KTWXY73Q.js} +5 -5
- package/dist/cli/{chunk-GAWP2YF5.mjs → chunk-M27SSAQ7.mjs} +1 -1
- package/dist/cli/{chunk-UON3WCBD.js → chunk-M4QO3GRD.js} +3 -3
- package/dist/cli/{chunk-36KEQFWZ.js → chunk-M7K5BCWP.js} +10 -10
- package/dist/cli/{chunk-SWAYZST7.mjs → chunk-MUDOASYL.mjs} +2 -2
- package/dist/cli/{chunk-D22QIUE3.js → chunk-NBVYS6VG.js} +9 -9
- package/dist/cli/{chunk-GE5GPLNM.mjs → chunk-NGOKBSBH.mjs} +1 -1
- package/dist/cli/{chunk-VGVSHQN5.mjs → chunk-NHQP4UPJ.mjs} +5 -5
- package/dist/cli/{chunk-3J2N7YF5.js → chunk-QKMWQTMI.js} +3 -3
- package/dist/cli/{chunk-B3BM3PAI.js → chunk-TLPW2SHS.js} +6 -6
- package/dist/cli/{chunk-QI73EL5C.js → chunk-TZRTT7EI.js} +26 -26
- package/dist/cli/{chunk-DD7E4ZVH.js → chunk-U3F63UWD.js} +7 -7
- package/dist/cli/{chunk-RZHUHL53.mjs → chunk-V4ILQ2DS.mjs} +11 -7
- package/dist/cli/{chunk-YHB6MASP.js → chunk-VBGPRRJ6.js} +2 -2
- package/dist/cli/{chunk-HQCTPAXP.mjs → chunk-VD3E3QMI.mjs} +1 -1
- package/dist/cli/{chunk-X6R5UNDZ.mjs → chunk-VFP3MFLD.mjs} +1 -1
- package/dist/cli/{chunk-EVLUSXQG.mjs → chunk-VTCXSICG.mjs} +2 -2
- package/dist/cli/{chunk-TBLBYIKO.mjs → chunk-YAAQNDP6.mjs} +1 -1
- package/dist/cli/{chunk-IFZ76E5H.mjs → chunk-YMS7VYFA.mjs} +1 -1
- package/dist/cli/{chunk-OHUGXZ7N.js → chunk-Z5VSBCFV.js} +16 -16
- package/dist/cli/{chunk-ZVLYUKJU.js → chunk-ZANUM4XV.js} +5 -5
- package/dist/cli/{chunk-MREO5LZD.js → chunk-ZQYBKDPA.js} +2 -2
- package/dist/cli/command-docs.js +2 -2
- package/dist/cli/command-docs.mjs +1 -1
- package/dist/cli/{csv-OVV3HWN4.js → csv-4YHSYV2C.js} +13 -13
- package/dist/cli/{csv-NCY5MWS2.mjs → csv-WC7ILC7P.mjs} +4 -4
- package/dist/cli/{db-BOELNDS4.js → db-APZNM2GI.js} +27 -27
- package/dist/cli/{db-VPH6JHUD.mjs → db-I5PV6NTC.mjs} +6 -6
- package/dist/cli/describe-OR7FZBCD.js +48 -0
- package/dist/cli/{describe-FFDCPYZH.mjs → describe-XH5F3X5W.mjs} +17 -17
- package/dist/cli/describe-core.d.mts +3 -3
- package/dist/cli/describe-core.d.ts +3 -3
- package/dist/cli/{executions-5GCCDENS.mjs → executions-PIY4XM7T.mjs} +6 -6
- package/dist/cli/{executions-RF44ANF5.js → executions-VIXFKZYU.js} +8 -8
- package/dist/cli/{feedback-OI2A5L6F.mjs → feedback-IGDHBWGT.mjs} +4 -4
- package/dist/cli/{feedback-NJM3V7QV.js → feedback-WCLIFQTR.js} +8 -8
- package/dist/cli/git-CKN2F32Y.js +25 -0
- package/dist/cli/{git-EOGSZ3T2.mjs → git-FRICPPTA.mjs} +5 -5
- package/dist/cli/index.js +139 -139
- package/dist/cli/index.mjs +41 -41
- package/dist/cli/{legacy-noops-5QM7XGGX.js → legacy-noops-DSFOPYDY.js} +8 -8
- package/dist/cli/{legacy-noops-UYKK7AAB.mjs → legacy-noops-ROAXEG6D.mjs} +5 -5
- package/dist/cli/{monitors-N3ITC5MP.mjs → monitors-FZ5PRLW6.mjs} +7 -7
- package/dist/cli/{monitors-52GW2V4V.js → monitors-KUCYQLU5.js} +91 -91
- package/dist/cli/{org-KUZUG33W.mjs → org-DRNND5XU.mjs} +6 -6
- package/dist/cli/{org-ADF47NPV.js → org-SRHMWKQR.js} +72 -72
- package/dist/cli/{pages-H3H7ODBN.js → pages-TEJQIHTU.js} +17 -17
- package/dist/cli/{pages-C3S5WU3S.mjs → pages-XNGA356T.mjs} +6 -6
- package/dist/cli/{play-YWZWBG32.js → play-GCGFQSFP.js} +662 -601
- package/dist/cli/{play-SZETTQPV.mjs → play-NE45ROIZ.mjs} +110 -49
- package/dist/cli/{providers-FSCD35HB.mjs → providers-57TJOSMV.mjs} +6 -6
- package/dist/cli/{providers-V3BP7ZB2.js → providers-VHMUR63R.js} +9 -9
- package/dist/cli/{quickstart-ICULVGAR.js → quickstart-BPXWO4UD.js} +6 -6
- package/dist/cli/{quickstart-SYZCT2WI.mjs → quickstart-K3SH4NXP.mjs} +4 -4
- package/dist/cli/{run-WUJSKGUM.js → run-553AAXDU.js} +36 -36
- package/dist/cli/{run-HA5B3AYQ.mjs → run-ZRWJUEN2.mjs} +22 -22
- package/dist/cli/{search-A27AGYDV.mjs → search-BW6XRS4I.mjs} +7 -7
- package/dist/cli/search-WF26AUDJ.js +32 -0
- package/dist/cli/{secrets-ODX56QBF.js → secrets-AMYQYUL5.js} +15 -15
- package/dist/cli/{secrets-2L4VRJ2F.mjs → secrets-OYYCFGZE.mjs} +6 -6
- package/dist/cli/{sessions-PRF36DHO.mjs → sessions-O4SVVOMO.mjs} +5 -5
- package/dist/cli/{sessions-DVZ4FECP.js → sessions-RYCJUMJ2.js} +6 -6
- package/dist/cli/{settings-RJT5DBWG.js → settings-7R5FQKII.js} +78 -78
- package/dist/cli/{settings-LUATIKJE.mjs → settings-QK7DHDU7.mjs} +11 -11
- package/dist/cli/{setup-FJSBKCLU.js → setup-OTQDQHDN.js} +44 -44
- package/dist/cli/{setup-IQXTYLRH.mjs → setup-VDAAVCKK.mjs} +9 -9
- package/dist/cli/{skills-XJH4ZC5Y.mjs → skills-HRK5O5JO.mjs} +7 -7
- package/dist/cli/skills-VNLMEOXW.js +31 -0
- package/dist/cli/tools-C3MUHL7G.js +55 -0
- package/dist/cli/{tools-IGXLZCKZ.mjs → tools-O7BM62QV.mjs} +16 -16
- package/dist/cli/update-TCKTU6QJ.js +33 -0
- package/dist/cli/{update-AH7G5VSL.mjs → update-UHKHCSTY.mjs} +9 -9
- package/dist/cli/{workflow-IQ2J7LWM.js → workflow-7Z5NCTGG.js} +17 -17
- package/dist/cli/{workflow-WOYSCWVD.mjs → workflow-T2EDR7L4.mjs} +7 -7
- package/dist/{client-C9bJ0QXu.d.mts → client-Bgl0_zXi.d.mts} +1 -1
- package/dist/{client-CuNH54w3.d.ts → client-_ah6IuhK.d.ts} +1 -1
- package/dist/{client-core-CSiXbe2n.d.mts → client-core-Bfsedm0_.d.mts} +44 -3
- package/dist/{client-core-C8Q6DKga.d.ts → client-core-C-ZF5vh9.d.ts} +44 -3
- package/dist/{compiler-manifest-Zwm9yqOA.d.mts → compiler-manifest-CCOqDGjU.d.mts} +12 -10
- package/dist/{compiler-manifest-Zwm9yqOA.d.ts → compiler-manifest-CCOqDGjU.d.ts} +12 -10
- package/dist/{errors-nEsqNLL8.d.mts → errors-BLccD7tG.d.mts} +1 -1
- package/dist/{errors-CsYwlpn4.d.ts → errors-Cy3kPnsi.d.ts} +1 -1
- package/dist/{index-CXF2jtza.d.mts → index-4-j44HuT.d.mts} +1 -1
- package/dist/{index-9pUZqkPH.d.ts → index-D5e4h1-H.d.ts} +1 -1
- package/dist/index.d.mts +14 -9
- package/dist/index.d.ts +14 -9
- package/dist/index.js +9 -9
- package/dist/index.mjs +6 -6
- package/dist/install-integrity.json +111 -110
- package/dist/plays/bundle-play-file.d.mts +3 -3
- package/dist/plays/bundle-play-file.d.ts +3 -3
- package/dist/plays/bundle-play-file.mjs +1 -1
- package/dist/plays/play-description.d.mts +2 -2
- package/dist/plays/play-description.d.ts +2 -2
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +5 -5
- package/dist/server.mjs +3 -3
- package/package.json +1 -1
- package/dist/cli/auth-IJOWKNSB.js +0 -41
- package/dist/cli/describe-7SFESEFL.js +0 -48
- package/dist/cli/git-4V5ZS5SE.js +0 -25
- package/dist/cli/search-XATDPNEM.js +0 -32
- package/dist/cli/skills-N6CPBF6T.js +0 -31
- package/dist/cli/tools-YMVEVRCE.js +0 -55
- package/dist/cli/update-MUCWGTXH.js +0 -33
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
3
|
+
MAX_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
4
|
+
validatePlaySandboxRuntimeLimits,
|
|
5
|
+
type PlaySandboxRuntimeLimits,
|
|
6
|
+
} from '../../../plays/sandbox-runtime-limits';
|
|
2
7
|
import { RUNTIME_SANDBOX_MEMORY_ABORT_CODE } from '../../run-failure';
|
|
3
8
|
import { PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER } from '../../runtime-contract';
|
|
4
9
|
import { RUNTIME_CLAIM_HEARTBEAT_DIVISOR } from '../../runtime-constants';
|
|
@@ -11,7 +16,7 @@ import {
|
|
|
11
16
|
/** Exit code reserved for the external runner watchdog's runtime deadline. */
|
|
12
17
|
export const DAYTONA_RUNTIME_WATCHDOG_EXIT_CODE = 124;
|
|
13
18
|
|
|
14
|
-
/** The cgroup memory envelope
|
|
19
|
+
/** The standard profile's cgroup memory envelope, in MiB. */
|
|
15
20
|
export const STANDARD_PLAY_SANDBOX_MEMORY_MIB =
|
|
16
21
|
STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS.memoryGiB * 1024;
|
|
17
22
|
|
|
@@ -75,6 +80,31 @@ export const PLAY_RUNNER_NODE_EXEC_ARGV: readonly string[] = [
|
|
|
75
80
|
'--single-threaded-gc',
|
|
76
81
|
];
|
|
77
82
|
|
|
83
|
+
/** Select the V8 heap envelope from the sandbox profile that will run the Play. */
|
|
84
|
+
export function playRunnerNodeExecArgvForLimits(
|
|
85
|
+
limits: PlaySandboxRuntimeLimits,
|
|
86
|
+
memoryLimitMiB?: number,
|
|
87
|
+
): readonly string[] {
|
|
88
|
+
const validated = validatePlaySandboxRuntimeLimits(limits);
|
|
89
|
+
const reservationMemoryMiB = validated.memoryGiB * 1024;
|
|
90
|
+
const sandboxMemoryMiB = memoryLimitMiB ?? reservationMemoryMiB;
|
|
91
|
+
if (
|
|
92
|
+
!Number.isSafeInteger(sandboxMemoryMiB) ||
|
|
93
|
+
sandboxMemoryMiB < reservationMemoryMiB ||
|
|
94
|
+
sandboxMemoryMiB > MAX_PLAY_SANDBOX_RUNTIME_LIMITS.memoryGiB * 1024
|
|
95
|
+
) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid runner memory ceiling: ${sandboxMemoryMiB} MiB for a ${reservationMemoryMiB} MiB reservation.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
`--max-old-space-size=${Math.floor(sandboxMemoryMiB * STANDARD_PLAY_RUNNER_OLD_SPACE_SHARE)}`,
|
|
102
|
+
`--max-semi-space-size=${STANDARD_PLAY_RUNNER_MAX_SEMI_SPACE_SIZE_MIB}`,
|
|
103
|
+
'--expose-gc',
|
|
104
|
+
'--single-threaded-gc',
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
/**
|
|
79
109
|
* libuv threadpool size for the customer runner. The default is 4 and it is
|
|
80
110
|
* sized for a machine, not a one-core cgroup: zlib and fs work on four threads
|
|
@@ -128,6 +158,85 @@ export const RUNNER_SANDBOX_STALL_REPORT_MS = 5_000;
|
|
|
128
158
|
export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
|
|
129
159
|
RUNTIME_SANDBOX_MEMORY_ABORT_CODE;
|
|
130
160
|
|
|
161
|
+
/** Keep anonymous memory, active cache and dirty/writeback pages in the guard. */
|
|
162
|
+
export function cgroupMemoryWorkingSet(
|
|
163
|
+
usageBytes: number,
|
|
164
|
+
stat: string,
|
|
165
|
+
version: 'v1' | 'v2',
|
|
166
|
+
): { workingSetBytes: number; reclaimableFileBytes: number } {
|
|
167
|
+
const values = new Map(
|
|
168
|
+
stat
|
|
169
|
+
.trim()
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map((line) => {
|
|
172
|
+
const [key, value] = line.trim().split(/\s+/);
|
|
173
|
+
return [key, /^\d+$/.test(value ?? '') ? Number(value) : NaN] as const;
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
// v1 usage includes descendants; subtract only its matching total counters.
|
|
177
|
+
const keys =
|
|
178
|
+
version === 'v2'
|
|
179
|
+
? ['inactive_file', 'file_dirty', 'file_writeback']
|
|
180
|
+
: ['total_inactive_file', 'total_dirty', 'total_writeback'];
|
|
181
|
+
const [inactive, dirty, writeback] = keys.map((key) => values.get(key));
|
|
182
|
+
if (
|
|
183
|
+
!Number.isSafeInteger(usageBytes) ||
|
|
184
|
+
usageBytes < 0 ||
|
|
185
|
+
[inactive, dirty, writeback].some(
|
|
186
|
+
(value) =>
|
|
187
|
+
value === undefined || !Number.isSafeInteger(value) || value < 0,
|
|
188
|
+
) ||
|
|
189
|
+
inactive! > usageBytes
|
|
190
|
+
) {
|
|
191
|
+
return { workingSetBytes: usageBytes, reclaimableFileBytes: 0 };
|
|
192
|
+
}
|
|
193
|
+
const reclaimableFileBytes = Math.max(0, inactive! - dirty! - writeback!);
|
|
194
|
+
return {
|
|
195
|
+
workingSetBytes: usageBytes - reclaimableFileBytes,
|
|
196
|
+
reclaimableFileBytes,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type SampledCgroupMemoryUsage = {
|
|
201
|
+
memoryGiBSeconds: number;
|
|
202
|
+
sampledDurationSeconds: number;
|
|
203
|
+
peakUsageBytes: number;
|
|
204
|
+
peakWorkingSetBytes: number;
|
|
205
|
+
samples: number;
|
|
206
|
+
lastSample: { monotonicAtMs: number; usageBytes: number } | null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Fixed-size diagnostic estimate; a missing sample breaks measured coverage. */
|
|
210
|
+
export function accumulateSampledCgroupMemoryUsage(
|
|
211
|
+
state: SampledCgroupMemoryUsage,
|
|
212
|
+
sample: { usageBytes: number; workingSetBytes: number } | null,
|
|
213
|
+
monotonicAtMs: number,
|
|
214
|
+
reservationMemoryBytes: number,
|
|
215
|
+
): SampledCgroupMemoryUsage {
|
|
216
|
+
if (!sample) return { ...state, lastSample: null };
|
|
217
|
+
const durationSeconds = state.lastSample
|
|
218
|
+
? Math.max(0, monotonicAtMs - state.lastSample.monotonicAtMs) / 1_000
|
|
219
|
+
: 0;
|
|
220
|
+
return {
|
|
221
|
+
memoryGiBSeconds:
|
|
222
|
+
state.memoryGiBSeconds +
|
|
223
|
+
(durationSeconds *
|
|
224
|
+
Math.max(
|
|
225
|
+
reservationMemoryBytes,
|
|
226
|
+
state.lastSample?.usageBytes ?? reservationMemoryBytes,
|
|
227
|
+
)) /
|
|
228
|
+
1024 ** 3,
|
|
229
|
+
sampledDurationSeconds: state.sampledDurationSeconds + durationSeconds,
|
|
230
|
+
peakUsageBytes: Math.max(state.peakUsageBytes, sample.usageBytes),
|
|
231
|
+
peakWorkingSetBytes: Math.max(
|
|
232
|
+
state.peakWorkingSetBytes,
|
|
233
|
+
sample.workingSetBytes,
|
|
234
|
+
),
|
|
235
|
+
samples: state.samples + 1,
|
|
236
|
+
lastSample: { monotonicAtMs, usageBytes: sample.usageBytes },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
131
240
|
/**
|
|
132
241
|
* Build the parent process that owns runner liveness, the hard runtime
|
|
133
242
|
* deadline, and the sandbox memory envelope outside the customer-code Node
|
|
@@ -141,14 +250,28 @@ export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
|
|
|
141
250
|
* 9 terminationDiagnosticPath, 10 memoryLedgerPath, 11 memoryPressurePath,
|
|
142
251
|
* 12 cgroupRoot, 13 runnerPidPath.
|
|
143
252
|
*/
|
|
144
|
-
export function buildDaytonaRuntimeWatchdogSource(
|
|
253
|
+
export function buildDaytonaRuntimeWatchdogSource(
|
|
254
|
+
sandboxRuntimeLimits: PlaySandboxRuntimeLimits = STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
255
|
+
memoryLimitMiB?: number,
|
|
256
|
+
): string {
|
|
257
|
+
const nodeExecArgv = playRunnerNodeExecArgvForLimits(
|
|
258
|
+
sandboxRuntimeLimits,
|
|
259
|
+
memoryLimitMiB,
|
|
260
|
+
);
|
|
145
261
|
return `
|
|
146
262
|
const fs = require('node:fs');
|
|
147
263
|
const { spawn } = require('node:child_process');
|
|
264
|
+
const cgroupMemoryWorkingSet = ${cgroupMemoryWorkingSet.toString()};
|
|
265
|
+
const accumulateSampledCgroupMemoryUsage = ${accumulateSampledCgroupMemoryUsage.toString()};
|
|
266
|
+
const configuredMemoryLimitBytes = ${memoryLimitMiB === undefined ? 'null' : memoryLimitMiB * 1024 * 1024};
|
|
148
267
|
const runnerPath = process.argv[1];
|
|
149
268
|
const configPath = process.argv[2];
|
|
150
269
|
const runtimeStartedPath = process.argv[3];
|
|
151
270
|
const runtimeCompletedPath = process.argv[4];
|
|
271
|
+
const sampledMemoryUsagePath = configuredMemoryLimitBytes === null
|
|
272
|
+
? ''
|
|
273
|
+
: runtimeCompletedPath + '.memory-usage.json';
|
|
274
|
+
const reservationMemoryBytes = ${sandboxRuntimeLimits.memoryGiB * 1024 ** 3};
|
|
152
275
|
const limitMs = Math.max(1, Number.parseInt(process.argv[5] || '', 10) || 1);
|
|
153
276
|
const startupLimitMs = Math.max(1, Number.parseInt(process.argv[6] || '', 10) || 1);
|
|
154
277
|
const terminalGraceMs = Math.max(1, Number.parseInt(process.argv[7] || '', 10) || 1);
|
|
@@ -161,7 +284,7 @@ const runnerPidPath = process.argv[13] || '';
|
|
|
161
284
|
const runtimeLimitSeconds = Math.max(1, Math.ceil(limitMs / 1_000));
|
|
162
285
|
const child = spawn(
|
|
163
286
|
process.execPath,
|
|
164
|
-
[${
|
|
287
|
+
[${nodeExecArgv.map((flag) => JSON.stringify(flag)).join(', ')}, runnerPath, configPath],
|
|
165
288
|
{
|
|
166
289
|
env: {
|
|
167
290
|
...process.env,
|
|
@@ -555,6 +678,47 @@ function markRuntimeLimit() {
|
|
|
555
678
|
let memorySource = null;
|
|
556
679
|
let memoryWatchDisabledReported = false;
|
|
557
680
|
let lastMemoryPressureSignalAt = 0;
|
|
681
|
+
let sampledMemoryUsage = {
|
|
682
|
+
memoryGiBSeconds: 0,
|
|
683
|
+
sampledDurationSeconds: 0,
|
|
684
|
+
peakUsageBytes: 0,
|
|
685
|
+
peakWorkingSetBytes: 0,
|
|
686
|
+
samples: 0,
|
|
687
|
+
lastSample: null,
|
|
688
|
+
};
|
|
689
|
+
let memoryUsageWriteFailureReported = false;
|
|
690
|
+
|
|
691
|
+
// This is an internal estimate, not an authoritative Modal invoice. Raw cgroup
|
|
692
|
+
// usage includes cache; only the pressure guard subtracts reclaimable pages.
|
|
693
|
+
// A separate parent process observes Python peaks even during spawnSync.
|
|
694
|
+
function recordSampledMemoryUsage(sample) {
|
|
695
|
+
if (!sampledMemoryUsagePath) return;
|
|
696
|
+
sampledMemoryUsage = accumulateSampledCgroupMemoryUsage(
|
|
697
|
+
sampledMemoryUsage, sample, performance.now(), reservationMemoryBytes,
|
|
698
|
+
);
|
|
699
|
+
try {
|
|
700
|
+
fs.writeFileSync(sampledMemoryUsagePath + '.tmp', JSON.stringify({
|
|
701
|
+
schemaVersion: 1,
|
|
702
|
+
source: 'sampled_cgroup_estimate',
|
|
703
|
+
memoryGiBSeconds: sampledMemoryUsage.memoryGiBSeconds,
|
|
704
|
+
sampledDurationSeconds: sampledMemoryUsage.sampledDurationSeconds,
|
|
705
|
+
peakUsageBytes: sampledMemoryUsage.peakUsageBytes,
|
|
706
|
+
peakWorkingSetBytes: sampledMemoryUsage.peakWorkingSetBytes,
|
|
707
|
+
samples: sampledMemoryUsage.samples,
|
|
708
|
+
reservationMemoryMiB: reservationMemoryBytes / ${1024 ** 2},
|
|
709
|
+
observedAt: Date.now(),
|
|
710
|
+
}));
|
|
711
|
+
fs.renameSync(sampledMemoryUsagePath + '.tmp', sampledMemoryUsagePath);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (memoryUsageWriteFailureReported) return;
|
|
714
|
+
memoryUsageWriteFailureReported = true;
|
|
715
|
+
process.stderr.write('[play-runner.memory_usage] ' + JSON.stringify({
|
|
716
|
+
event: 'sampled_memory_usage_write_failed',
|
|
717
|
+
errorName: error && typeof error.name === 'string' ? error.name : 'Error',
|
|
718
|
+
errorCode: error && typeof error.code === 'string' ? error.code : null,
|
|
719
|
+
}) + '\\n');
|
|
720
|
+
}
|
|
721
|
+
}
|
|
558
722
|
|
|
559
723
|
function readMemoryNumberFile(path) {
|
|
560
724
|
try {
|
|
@@ -574,16 +738,22 @@ function memorySourceCandidates() {
|
|
|
574
738
|
kind: 'cgroup_v2',
|
|
575
739
|
usagePath: cgroupRoot + '/memory.current',
|
|
576
740
|
limitPath: cgroupRoot + '/memory.max',
|
|
741
|
+
statPath: cgroupRoot + '/memory.stat',
|
|
742
|
+
version: 'v2',
|
|
577
743
|
},
|
|
578
744
|
{
|
|
579
745
|
kind: 'cgroup_v1',
|
|
580
746
|
usagePath: cgroupRoot + '/memory.usage_in_bytes',
|
|
581
747
|
limitPath: cgroupRoot + '/memory.limit_in_bytes',
|
|
748
|
+
statPath: cgroupRoot + '/memory.stat',
|
|
749
|
+
version: 'v1',
|
|
582
750
|
},
|
|
583
751
|
{
|
|
584
752
|
kind: 'cgroup_v1_nested',
|
|
585
753
|
usagePath: cgroupRoot + '/memory/memory.usage_in_bytes',
|
|
586
754
|
limitPath: cgroupRoot + '/memory/memory.limit_in_bytes',
|
|
755
|
+
statPath: cgroupRoot + '/memory/memory.stat',
|
|
756
|
+
version: 'v1',
|
|
587
757
|
},
|
|
588
758
|
];
|
|
589
759
|
}
|
|
@@ -593,16 +763,24 @@ function readCgroupMemory() {
|
|
|
593
763
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
594
764
|
const candidate = candidates[index];
|
|
595
765
|
const usageBytes = readMemoryNumberFile(candidate.usagePath);
|
|
596
|
-
const
|
|
597
|
-
if (usageBytes === null ||
|
|
766
|
+
const providerLimitBytes = readMemoryNumberFile(candidate.limitPath);
|
|
767
|
+
if (usageBytes === null || providerLimitBytes === null) continue;
|
|
768
|
+
const limitBytes = configuredMemoryLimitBytes === null
|
|
769
|
+
? providerLimitBytes
|
|
770
|
+
: Math.min(providerLimitBytes, configuredMemoryLimitBytes);
|
|
598
771
|
if (!(limitBytes > 0)) continue;
|
|
599
772
|
if (limitBytes >= ${RUNNER_MEMORY_CGROUP_UNLIMITED_BYTES}) continue;
|
|
600
773
|
memorySource = candidate;
|
|
774
|
+
let stat = '';
|
|
775
|
+
try { stat = fs.readFileSync(candidate.statPath, 'utf8'); } catch {}
|
|
776
|
+
const workingSet = cgroupMemoryWorkingSet(usageBytes, stat, candidate.version);
|
|
601
777
|
return {
|
|
602
778
|
source: candidate.kind,
|
|
603
779
|
usageBytes: usageBytes,
|
|
604
780
|
limitBytes: limitBytes,
|
|
605
781
|
ratio: usageBytes / limitBytes,
|
|
782
|
+
...workingSet,
|
|
783
|
+
workingSetRatio: workingSet.workingSetBytes / limitBytes,
|
|
606
784
|
};
|
|
607
785
|
}
|
|
608
786
|
return null;
|
|
@@ -641,6 +819,8 @@ function publishMemoryPressure(sample) {
|
|
|
641
819
|
usageBytes: sample.usageBytes,
|
|
642
820
|
limitBytes: sample.limitBytes,
|
|
643
821
|
ratio: sample.ratio,
|
|
822
|
+
workingSetBytes: sample.workingSetBytes,
|
|
823
|
+
reclaimableFileBytes: sample.reclaimableFileBytes,
|
|
644
824
|
at: Date.now(),
|
|
645
825
|
}),
|
|
646
826
|
);
|
|
@@ -650,11 +830,12 @@ function publishMemoryPressure(sample) {
|
|
|
650
830
|
const memoryWatch = setInterval(() => {
|
|
651
831
|
if (settled || memoryAborted) return;
|
|
652
832
|
const sample = readCgroupMemory();
|
|
833
|
+
recordSampledMemoryUsage(sample);
|
|
653
834
|
if (!sample) {
|
|
654
835
|
reportMemoryWatchDisabled();
|
|
655
836
|
return;
|
|
656
837
|
}
|
|
657
|
-
if (sample.
|
|
838
|
+
if (sample.workingSetRatio >= ${RUNNER_MEMORY_ABORT_RATIO}) {
|
|
658
839
|
memoryAborted = true;
|
|
659
840
|
clearInterval(memoryWatch);
|
|
660
841
|
process.stderr.write(
|
|
@@ -669,11 +850,13 @@ const memoryWatch = setInterval(() => {
|
|
|
669
850
|
memoryUsageBytes: sample.usageBytes,
|
|
670
851
|
memoryLimitBytes: sample.limitBytes,
|
|
671
852
|
memoryUsageRatio: Math.round(sample.ratio * 10_000) / 10_000,
|
|
853
|
+
memoryWorkingSetBytes: sample.workingSetBytes,
|
|
854
|
+
memoryReclaimableFileBytes: sample.reclaimableFileBytes,
|
|
672
855
|
memoryLedger: readMemoryLedger(),
|
|
673
856
|
});
|
|
674
857
|
return;
|
|
675
858
|
}
|
|
676
|
-
if (sample.
|
|
859
|
+
if (sample.workingSetRatio >= ${RUNNER_MEMORY_PRESSURE_RATIO}) {
|
|
677
860
|
const now = Date.now();
|
|
678
861
|
if (now - lastMemoryPressureSignalAt < ${RUNNER_MEMORY_PRESSURE_SIGNAL_INTERVAL_MS}) {
|
|
679
862
|
return;
|
|
@@ -743,8 +926,9 @@ const startupTimer = setTimeout(() => {
|
|
|
743
926
|
if (fs.existsSync(runtimeStartedPath)) armRuntimeDeadline();
|
|
744
927
|
if (fs.existsSync(runtimeCompletedPath)) armTerminalGrace();
|
|
745
928
|
|
|
746
|
-
function finish(code) {
|
|
929
|
+
function finish(code, signal) {
|
|
747
930
|
if (settled) return;
|
|
931
|
+
recordSampledMemoryUsage(readCgroupMemory());
|
|
748
932
|
settled = true;
|
|
749
933
|
clearInterval(runtimeBoundaryPoll);
|
|
750
934
|
clearInterval(memoryWatch);
|
|
@@ -764,6 +948,8 @@ function finish(code) {
|
|
|
764
948
|
? 1
|
|
765
949
|
: typeof code === 'number'
|
|
766
950
|
? code
|
|
951
|
+
: signal === 'SIGABRT'
|
|
952
|
+
? 134
|
|
767
953
|
: 1;
|
|
768
954
|
}
|
|
769
955
|
|
|
@@ -779,7 +965,7 @@ child.once('exit', (code, signal) => {
|
|
|
779
965
|
childExitCode: typeof code === 'number' ? code : null,
|
|
780
966
|
childSignal: typeof signal === 'string' ? signal : null,
|
|
781
967
|
});
|
|
782
|
-
finish(code);
|
|
968
|
+
finish(code, signal);
|
|
783
969
|
});
|
|
784
970
|
process.once('SIGTERM', () =>
|
|
785
971
|
stopChild('watchdog_signal', { watchdogSignal: 'SIGTERM' }),
|
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
PLAY_RUNNER_TERMINAL_GRACE_SECONDS,
|
|
41
41
|
STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
|
|
42
42
|
} from '../../runtime-constants';
|
|
43
|
+
import {
|
|
44
|
+
STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
45
|
+
validatePlaySandboxRuntimeLimits,
|
|
46
|
+
} from '../../../plays/sandbox-runtime-limits';
|
|
43
47
|
import {
|
|
44
48
|
loadDaytonaRequiredConfig,
|
|
45
49
|
loadDaytonaRunnerPathsConfig,
|
|
@@ -1842,6 +1846,20 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
1842
1846
|
) {
|
|
1843
1847
|
let sandboxForAttempt: DaytonaSandbox | null = null;
|
|
1844
1848
|
let resourceAccounting: Promise<void> | null = null;
|
|
1849
|
+
const trackResourceAccounting = (pending: Promise<void>) => {
|
|
1850
|
+
resourceAccountingPending = true;
|
|
1851
|
+
resourceAccounting = pending;
|
|
1852
|
+
void pending.then(
|
|
1853
|
+
() => {
|
|
1854
|
+
resourceAccountingPending = false;
|
|
1855
|
+
},
|
|
1856
|
+
() => {
|
|
1857
|
+
resourceAccountingPending = false;
|
|
1858
|
+
resourceAccountingFailed = true;
|
|
1859
|
+
},
|
|
1860
|
+
);
|
|
1861
|
+
return pending;
|
|
1862
|
+
};
|
|
1845
1863
|
// How far this sandbox's start got. The retry decision below reads it.
|
|
1846
1864
|
let commandSent = false;
|
|
1847
1865
|
let parkStarted = false;
|
|
@@ -1871,28 +1889,33 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
1871
1889
|
acquiredSandbox.billingStartedAt - startedAt;
|
|
1872
1890
|
const sandbox = acquiredSandbox.sandbox;
|
|
1873
1891
|
sandboxForAttempt = sandbox;
|
|
1892
|
+
const sandboxLimits = validatePlaySandboxRuntimeLimits(
|
|
1893
|
+
config.context.sandboxRuntimeLimits ?? {
|
|
1894
|
+
...STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
1895
|
+
},
|
|
1896
|
+
);
|
|
1874
1897
|
// Payload upload and session allocation do not execute customer code.
|
|
1875
1898
|
// Overlap them with accounting, but join every resource ACK before
|
|
1876
1899
|
// command dispatch, retry, or cancellation cleanup.
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
// Observe settlement immediately; the joins below propagate failures.
|
|
1886
|
-
void resourceAccounting.then(
|
|
1887
|
-
() => {
|
|
1888
|
-
resourceAccountingPending = false;
|
|
1889
|
-
},
|
|
1890
|
-
() => {
|
|
1891
|
-
resourceAccountingPending = false;
|
|
1892
|
-
resourceAccountingFailed = true;
|
|
1893
|
-
},
|
|
1900
|
+
const resourceAccountingForAttempt = trackResourceAccounting(
|
|
1901
|
+
(async () => {
|
|
1902
|
+
await reportRuntimeResource(acquiredSandbox);
|
|
1903
|
+
await sandboxLifecycle.settlePendingCreates();
|
|
1904
|
+
for (const acquired of sandboxLifecycle.acquiredSandboxes()) {
|
|
1905
|
+
await reportRuntimeResource(acquired);
|
|
1906
|
+
}
|
|
1907
|
+
})(),
|
|
1894
1908
|
);
|
|
1895
1909
|
throwIfCancellationRequested();
|
|
1910
|
+
if (
|
|
1911
|
+
sandbox.cpu !== sandboxLimits.cpu ||
|
|
1912
|
+
sandbox.memory !== sandboxLimits.memoryGiB
|
|
1913
|
+
) {
|
|
1914
|
+
await resourceAccountingForAttempt;
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
`RUNTIME_SANDBOX_PROFILE_MISMATCH: Daytona created ${sandbox.cpu} CPU / ${sandbox.memory}GiB memory for a ${sandboxLimits.cpu} CPU / ${sandboxLimits.memoryGiB}GiB Play profile requested at create.`,
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1896
1919
|
|
|
1897
1920
|
const { workdir: workDir } = loadDaytonaRunnerPathsConfig();
|
|
1898
1921
|
emitDaytonaStage(callbacks, config.context, 'workdir:resolved', {
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts
CHANGED
|
@@ -477,7 +477,10 @@ export const localProcessPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
477
477
|
'node',
|
|
478
478
|
[
|
|
479
479
|
'-e',
|
|
480
|
-
buildDaytonaRuntimeWatchdogSource(
|
|
480
|
+
buildDaytonaRuntimeWatchdogSource(
|
|
481
|
+
config.context.sandboxRuntimeLimits ??
|
|
482
|
+
STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
|
|
483
|
+
),
|
|
481
484
|
runnerPath,
|
|
482
485
|
configPath,
|
|
483
486
|
runtimeStartedPath,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { ModalClient } from 'modal';
|
|
2
|
+
import { loadModalClientConfig } from '../../modal-runtime-config';
|
|
3
|
+
import { MODAL_SANDBOX_COMPUTE_PRICING_USD } from '../../worker-api-types';
|
|
4
|
+
|
|
5
|
+
type ResourceUsageClient = {
|
|
6
|
+
cpClient: Pick<
|
|
7
|
+
ModalClient['cpClient'],
|
|
8
|
+
'sandboxList' | 'sandboxGetResourceUsage'
|
|
9
|
+
>;
|
|
10
|
+
environmentName: ModalClient['environmentName'];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ResourceUsageIdentity = {
|
|
14
|
+
source: 'provider_resource_usage';
|
|
15
|
+
sandboxId: string;
|
|
16
|
+
appId: string;
|
|
17
|
+
readStartedAt: number;
|
|
18
|
+
observedAt: number;
|
|
19
|
+
/** The provider exposes neither a finality marker nor a sample horizon. */
|
|
20
|
+
partial: true;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ModalSandboxResourceUsage = ResourceUsageIdentity &
|
|
24
|
+
(
|
|
25
|
+
| {
|
|
26
|
+
status: 'observed' | 'empty';
|
|
27
|
+
raw: {
|
|
28
|
+
cpuCoreNanosecs: number;
|
|
29
|
+
memGibNanosecs: number;
|
|
30
|
+
gpuNanosecs: number;
|
|
31
|
+
gpuType: string | null;
|
|
32
|
+
};
|
|
33
|
+
cpuCoreSeconds: number;
|
|
34
|
+
memoryGiBSeconds: number;
|
|
35
|
+
/** CPU/memory counter cost, not a bill or billing-adjusted usage. */
|
|
36
|
+
reportedResourceCostEstimateUsd: number;
|
|
37
|
+
billingAdjustment: 'unknown';
|
|
38
|
+
gpuCostIncluded: false;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
status: 'unavailable';
|
|
42
|
+
reason:
|
|
43
|
+
| 'deadline'
|
|
44
|
+
| 'ownership_not_observed'
|
|
45
|
+
| 'ownership_mismatch'
|
|
46
|
+
| 'invalid_provider_response'
|
|
47
|
+
| 'provider_read_failed';
|
|
48
|
+
providerErrorCode?: number;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Preview diagnostics only. Modal's installed SDK has no high-level usage
|
|
54
|
+
* reader, so keep its typed, unsupported control-plane RPC at this edge.
|
|
55
|
+
* These counters have undocumented billing adjustment and ingestion lag;
|
|
56
|
+
* they must never become an irrevocable customer settlement fact.
|
|
57
|
+
*/
|
|
58
|
+
export async function readModalSandboxResourceUsage(input: {
|
|
59
|
+
sandboxId: string;
|
|
60
|
+
expectedAppId: string;
|
|
61
|
+
/** Exact selector retained by this diagnostic's sandbox creator. */
|
|
62
|
+
ownershipTags: Record<string, string>;
|
|
63
|
+
environment: 'preview';
|
|
64
|
+
/** Modal's environment name is independent of Deepline's Preview lane. */
|
|
65
|
+
modalEnvironment?: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
client?: ResourceUsageClient;
|
|
68
|
+
}): Promise<ModalSandboxResourceUsage> {
|
|
69
|
+
const sandboxId = input.sandboxId.trim();
|
|
70
|
+
const appId = input.expectedAppId.trim();
|
|
71
|
+
const tags = Object.entries(input.ownershipTags);
|
|
72
|
+
const timeoutMs = input.timeoutMs ?? 10_000;
|
|
73
|
+
if (input.environment !== 'preview') {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'Modal resource usage diagnostics are restricted to Preview.',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
!sandboxId ||
|
|
80
|
+
!appId ||
|
|
81
|
+
tags.length === 0 ||
|
|
82
|
+
tags.some(([key, value]) => !key.trim() || !value.trim())
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Modal usage diagnostics require a sandbox id, app id, and exact non-empty creation tags.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'Modal usage diagnostic timeout must be positive milliseconds.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const readStartedAt = Date.now();
|
|
95
|
+
const identity = (): ResourceUsageIdentity => ({
|
|
96
|
+
source: 'provider_resource_usage',
|
|
97
|
+
sandboxId,
|
|
98
|
+
appId,
|
|
99
|
+
readStartedAt,
|
|
100
|
+
observedAt: Date.now(),
|
|
101
|
+
partial: true,
|
|
102
|
+
});
|
|
103
|
+
const unavailable = (
|
|
104
|
+
reason: Extract<
|
|
105
|
+
ModalSandboxResourceUsage,
|
|
106
|
+
{ status: 'unavailable' }
|
|
107
|
+
>['reason'],
|
|
108
|
+
): Extract<ModalSandboxResourceUsage, { status: 'unavailable' }> => ({
|
|
109
|
+
...identity(),
|
|
110
|
+
status: 'unavailable',
|
|
111
|
+
reason,
|
|
112
|
+
});
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
const deadline = new Promise<ModalSandboxResourceUsage>((resolve) => {
|
|
116
|
+
timer = setTimeout(() => {
|
|
117
|
+
controller.abort();
|
|
118
|
+
resolve(unavailable('deadline'));
|
|
119
|
+
}, timeoutMs);
|
|
120
|
+
});
|
|
121
|
+
const read = async (): Promise<ModalSandboxResourceUsage> => {
|
|
122
|
+
try {
|
|
123
|
+
const client = input.client ?? (await loadModalClientConfig()).client;
|
|
124
|
+
const options = {
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
timeoutMs,
|
|
127
|
+
retries: 0,
|
|
128
|
+
};
|
|
129
|
+
let beforeTimestamp = 0;
|
|
130
|
+
for (;;) {
|
|
131
|
+
controller.signal.throwIfAborted();
|
|
132
|
+
const page = await client.cpClient.sandboxList(
|
|
133
|
+
{
|
|
134
|
+
appId,
|
|
135
|
+
environmentName: client.environmentName(input.modalEnvironment),
|
|
136
|
+
includeFinished: true,
|
|
137
|
+
tags: tags.map(([tagName, tagValue]) => ({ tagName, tagValue })),
|
|
138
|
+
beforeTimestamp,
|
|
139
|
+
},
|
|
140
|
+
options,
|
|
141
|
+
);
|
|
142
|
+
controller.signal.throwIfAborted();
|
|
143
|
+
const sandbox = page.sandboxes.find((entry) => entry.id === sandboxId);
|
|
144
|
+
if (sandbox) {
|
|
145
|
+
const observedTags = new Map(
|
|
146
|
+
sandbox.tags.map((tag) => [tag.tagName, tag.tagValue]),
|
|
147
|
+
);
|
|
148
|
+
if (
|
|
149
|
+
sandbox.appId !== appId ||
|
|
150
|
+
tags.some(([key, value]) => observedTags.get(key) !== value)
|
|
151
|
+
) {
|
|
152
|
+
return unavailable('ownership_mismatch');
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if (page.sandboxes.length === 0) {
|
|
157
|
+
return unavailable('ownership_not_observed');
|
|
158
|
+
}
|
|
159
|
+
const oldest = Math.min(
|
|
160
|
+
...page.sandboxes.map((entry) => entry.createdAt),
|
|
161
|
+
);
|
|
162
|
+
if (
|
|
163
|
+
!Number.isFinite(oldest) ||
|
|
164
|
+
oldest <= 0 ||
|
|
165
|
+
(beforeTimestamp !== 0 && oldest >= beforeTimestamp)
|
|
166
|
+
) {
|
|
167
|
+
return unavailable('invalid_provider_response');
|
|
168
|
+
}
|
|
169
|
+
beforeTimestamp = oldest;
|
|
170
|
+
}
|
|
171
|
+
const raw = await client.cpClient.sandboxGetResourceUsage(
|
|
172
|
+
{ sandboxId },
|
|
173
|
+
options,
|
|
174
|
+
);
|
|
175
|
+
controller.signal.throwIfAborted();
|
|
176
|
+
const counters = [
|
|
177
|
+
raw.cpuCoreNanosecs,
|
|
178
|
+
raw.memGibNanosecs,
|
|
179
|
+
raw.gpuNanosecs,
|
|
180
|
+
];
|
|
181
|
+
if (counters.some((value) => !Number.isSafeInteger(value) || value < 0)) {
|
|
182
|
+
return unavailable('invalid_provider_response');
|
|
183
|
+
}
|
|
184
|
+
const cpuCoreSeconds = raw.cpuCoreNanosecs / 1_000_000_000;
|
|
185
|
+
const memoryGiBSeconds = raw.memGibNanosecs / 1_000_000_000;
|
|
186
|
+
return {
|
|
187
|
+
...identity(),
|
|
188
|
+
status: counters.every((value) => value === 0) ? 'empty' : 'observed',
|
|
189
|
+
raw: {
|
|
190
|
+
cpuCoreNanosecs: raw.cpuCoreNanosecs,
|
|
191
|
+
memGibNanosecs: raw.memGibNanosecs,
|
|
192
|
+
gpuNanosecs: raw.gpuNanosecs,
|
|
193
|
+
gpuType: raw.gpuType ?? null,
|
|
194
|
+
},
|
|
195
|
+
cpuCoreSeconds,
|
|
196
|
+
memoryGiBSeconds,
|
|
197
|
+
reportedResourceCostEstimateUsd: Number(
|
|
198
|
+
(
|
|
199
|
+
cpuCoreSeconds *
|
|
200
|
+
MODAL_SANDBOX_COMPUTE_PRICING_USD.physicalCoreSecond +
|
|
201
|
+
memoryGiBSeconds * MODAL_SANDBOX_COMPUTE_PRICING_USD.memoryGiBSecond
|
|
202
|
+
).toFixed(12),
|
|
203
|
+
),
|
|
204
|
+
billingAdjustment: 'unknown',
|
|
205
|
+
gpuCostIncluded: false,
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const result = unavailable(
|
|
209
|
+
controller.signal.aborted ? 'deadline' : 'provider_read_failed',
|
|
210
|
+
);
|
|
211
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
212
|
+
return typeof code === 'number' && Number.isFinite(code)
|
|
213
|
+
? { ...result, providerErrorCode: code }
|
|
214
|
+
: result;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
return await Promise.race([read(), deadline]);
|
|
219
|
+
} finally {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
}
|
|
222
|
+
}
|