deepline 0.3.476 → 0.3.478
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 +5 -4
- package/dist/bundling-sources/sdk/src/http-core.ts +84 -3
- package/dist/bundling-sources/sdk/src/http.ts +2 -0
- package/dist/bundling-sources/sdk/src/play.ts +8 -2
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +10 -2
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +77 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +77 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +83 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +215 -22
- package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +42 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +13 -12
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +5 -2
- package/dist/bundling-sources/shared_libs/plays/sandbox-runtime-limits.ts +37 -12
- package/dist/bundling-sources/shared_libs/product-notifications/billing-sales.ts +1 -0
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +29 -0
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +15 -0
- package/dist/cli/{auth-A22724UX.mjs → auth-SA4DJCSO.mjs} +5 -5
- package/dist/cli/auth-U5SNVC5F.js +41 -0
- package/dist/cli/{billing-T5Q6OO6M.js → billing-462SAHWX.js} +107 -87
- package/dist/cli/{billing-IIBEZ6J7.mjs → billing-Z4D35MV5.mjs} +34 -14
- package/dist/cli/{chunk-NPNQCPFP.mjs → chunk-2FTUPGTN.mjs} +2 -2
- package/dist/cli/{chunk-2WEPQSA2.mjs → chunk-2JS5DNVX.mjs} +5 -5
- package/dist/cli/{chunk-L45IECBZ.js → chunk-2MSKG4ER.js} +7 -6
- package/dist/cli/{chunk-JU2KBLOR.mjs → chunk-3LTLQAU4.mjs} +4 -4
- package/dist/cli/{chunk-ZV4OWYLZ.mjs → chunk-3V7INABZ.mjs} +2 -2
- package/dist/cli/{chunk-VPAVNEDQ.mjs → chunk-4C3TMIF3.mjs} +4 -4
- package/dist/cli/{chunk-H3GOAZHV.mjs → chunk-4RJRGQ5Y.mjs} +2 -2
- package/dist/cli/{chunk-AS3VECCY.js → chunk-4RV7U3RP.js} +7 -7
- package/dist/cli/{chunk-UHTJWDYB.mjs → chunk-645RNF2Y.mjs} +1 -1
- package/dist/cli/{chunk-TEJSRJNH.js → chunk-6EEMW7YP.js} +8 -8
- package/dist/cli/{chunk-ZOXV6WFI.mjs → chunk-75DAGCEX.mjs} +1 -1
- package/dist/cli/{chunk-NTJDZURV.mjs → chunk-7HTWT3Z4.mjs} +2 -2
- package/dist/cli/chunk-7I4B6V2S.mjs +127 -0
- package/dist/cli/{chunk-MZSLFWOT.js → chunk-7UFFWPQ2.js} +13 -9
- package/dist/cli/{chunk-5FBCMLJW.js → chunk-A5LPDB6N.js} +6 -6
- package/dist/cli/{chunk-4YWYLHED.mjs → chunk-ABBP5ZCI.mjs} +3 -3
- package/dist/cli/{chunk-IC3BZJR7.js → chunk-AQYSZYNE.js} +8 -8
- package/dist/cli/{chunk-WTPRRKZA.js → chunk-AYW242XF.js} +37 -37
- package/dist/cli/{chunk-CFSCHLNQ.mjs → chunk-BLAZVZVD.mjs} +23 -5
- package/dist/cli/{chunk-KGLIDLJ2.js → chunk-D2LKEQKI.js} +5 -5
- package/dist/cli/{chunk-4WMIMSSI.mjs → chunk-F2SJYUBW.mjs} +5 -5
- package/dist/cli/{chunk-KCP6XNRR.mjs → chunk-F44O4PEP.mjs} +1 -1
- package/dist/cli/{chunk-EBWYNNTJ.js → chunk-HMUBLAT4.js} +5 -5
- package/dist/cli/{chunk-UYA7LJGC.mjs → chunk-IL5AF6AR.mjs} +4 -2
- package/dist/cli/{chunk-B7PEXXRW.js → chunk-IWYWZXCA.js} +53 -53
- package/dist/cli/{chunk-PGNLD6L6.js → chunk-J3ZYRN5L.js} +10 -10
- package/dist/cli/{chunk-VKBZLV2K.mjs → chunk-JH775FF6.mjs} +1 -1
- package/dist/cli/{chunk-5AEEINTU.js → chunk-KBBZXWXF.js} +63 -11
- package/dist/cli/{chunk-FVPT44IJ.mjs → chunk-M5USVQ33.mjs} +55 -3
- package/dist/cli/{chunk-2YHDNHRR.mjs → chunk-N573OK4W.mjs} +4 -4
- package/dist/cli/{chunk-WFVEMDQL.js → chunk-OKAZHDYN.js} +4 -2
- package/dist/cli/{chunk-J4FCTFQS.mjs → chunk-PHBOI24Y.mjs} +10 -10
- package/dist/cli/{chunk-D7EQ4PI5.mjs → chunk-PLJV6SX4.mjs} +1 -1
- package/dist/cli/{chunk-YQOWMQEW.mjs → chunk-PPHHPAQV.mjs} +1 -1
- package/dist/cli/{chunk-CBVCCXIY.js → chunk-PZHRNSMU.js} +21 -21
- package/dist/cli/{chunk-LJFIUT5R.mjs → chunk-QB3Q73LP.mjs} +2 -2
- package/dist/cli/{chunk-ULKGYVYP.js → chunk-QVZNUQAP.js} +2 -2
- package/dist/cli/{chunk-A6BETJ2A.js → chunk-S3BKOCVB.js} +1 -1
- package/dist/cli/{chunk-F5PYBZ2H.mjs → chunk-S7UEKKOX.mjs} +2 -1
- package/dist/cli/{chunk-W3BHZUPL.js → chunk-SCHE5QRN.js} +3 -3
- package/dist/cli/{chunk-YEJDFZJP.js → chunk-SXY4DOVA.js} +16 -16
- package/dist/cli/{chunk-LZCYDU6Q.mjs → chunk-TLF3KGWG.mjs} +2 -2
- package/dist/cli/{chunk-5PYSYW63.js → chunk-TQWI7LJX.js} +26 -26
- package/dist/cli/{chunk-7HAUVMRF.js → chunk-TW3TTKW5.js} +3 -3
- package/dist/cli/{chunk-FK7SBMSZ.js → chunk-UXQVSDFP.js} +3 -3
- package/dist/cli/{chunk-HSANVBVL.js → chunk-V7I65ME7.js} +2 -2
- package/dist/cli/{chunk-6YZYCKWD.js → chunk-VE25VUTE.js} +70 -52
- package/dist/cli/{chunk-VLYUFZUT.js → chunk-XFY3JOP2.js} +9 -9
- package/dist/cli/{chunk-MYYOCJZ2.js → chunk-XOORR6P3.js} +93 -93
- package/dist/cli/chunk-YGCKMI5P.js +127 -0
- package/dist/cli/{chunk-3A7TDUZV.mjs → chunk-ZBSUXXZS.mjs} +13 -9
- package/dist/cli/{chunk-NREO7TT4.mjs → chunk-ZSU3C4ST.mjs} +2 -2
- package/dist/cli/command-docs.js +2 -2
- package/dist/cli/command-docs.mjs +1 -1
- package/dist/cli/{csv-GPVCETS2.mjs → csv-3SNWBO5F.mjs} +4 -4
- package/dist/cli/{csv-FXOQ2PBE.js → csv-LAYTDYIF.js} +13 -13
- package/dist/cli/{db-F5UJTBGP.mjs → db-72JKGQSK.mjs} +6 -6
- package/dist/cli/{db-LXYV4GPZ.js → db-OOUOSURL.js} +27 -27
- package/dist/cli/describe-7HYV44MO.js +48 -0
- package/dist/cli/{describe-GYWCI7I6.mjs → describe-KDPNOEUJ.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-6NL2YW76.mjs → executions-O6JS3UKX.mjs} +6 -6
- package/dist/cli/{executions-6SJKTY2L.js → executions-RJNMGJEE.js} +8 -8
- package/dist/cli/{feedback-E3HULSQD.js → feedback-FXB6NV3Y.js} +8 -8
- package/dist/cli/{feedback-7AMQCNTB.mjs → feedback-T3JH3CWC.mjs} +4 -4
- package/dist/cli/git-5LTKPXGN.js +25 -0
- package/dist/cli/{git-74IS2EFH.mjs → git-6KAQSH5J.mjs} +5 -5
- package/dist/cli/index.js +161 -142
- package/dist/cli/index.mjs +65 -46
- package/dist/cli/{legacy-noops-52FFRTRZ.mjs → legacy-noops-7HDGEGTK.mjs} +5 -5
- package/dist/cli/{legacy-noops-ALWUQPKS.js → legacy-noops-PAOGRKUK.js} +8 -8
- package/dist/cli/{monitors-Z4ADOGTY.js → monitors-KQFI7TVJ.js} +91 -91
- package/dist/cli/{monitors-VTJIWWW2.mjs → monitors-LE2AYK3Z.mjs} +7 -7
- package/dist/cli/{org-2BTEKDF5.js → org-J6XFHDJE.js} +72 -72
- package/dist/cli/{org-7GU6H7JL.mjs → org-Z4DMJ72I.mjs} +6 -6
- package/dist/cli/{pages-I6PFQGEU.mjs → pages-5LYS6Y4I.mjs} +6 -6
- package/dist/cli/{pages-MG36E7RY.js → pages-FIXBZQJG.js} +17 -17
- package/dist/cli/{play-VN52S3RV.js → play-D7WPXADH.js} +604 -680
- package/dist/cli/{play-7KFSLJX2.mjs → play-GILXXIN7.mjs} +45 -121
- package/dist/cli/{providers-W5XLII66.js → providers-KN7YACT6.js} +9 -9
- package/dist/cli/{providers-D46JDEL2.mjs → providers-OQWL4Q4W.mjs} +6 -6
- package/dist/cli/{quickstart-4GTXBZHY.mjs → quickstart-2LRIORFM.mjs} +4 -4
- package/dist/cli/{quickstart-TIZAC63S.js → quickstart-M2OBP3W3.js} +6 -6
- package/dist/cli/{run-FOVEMYYX.js → run-6YZZO7VM.js} +36 -36
- package/dist/cli/{run-LE3D4LVU.mjs → run-GYRBKQZT.mjs} +22 -22
- package/dist/cli/search-OVKWA4AV.js +32 -0
- package/dist/cli/{search-6YL6WJMQ.mjs → search-VUPK6BNU.mjs} +7 -7
- package/dist/cli/{secrets-CSTTTMZH.js → secrets-RE3UMZB7.js} +15 -15
- package/dist/cli/{secrets-D66U6RTT.mjs → secrets-ULRGU7NM.mjs} +6 -6
- package/dist/cli/{sessions-XIO3OUER.mjs → sessions-2ZZVJ3P2.mjs} +5 -5
- package/dist/cli/{sessions-45M2NRAA.js → sessions-T4UMHNDW.js} +6 -6
- package/dist/cli/{settings-N42CVKSK.js → settings-BRV6AYFM.js} +78 -78
- package/dist/cli/{settings-R33X2D5T.mjs → settings-PDY4TJU6.mjs} +11 -11
- package/dist/cli/{setup-CBBC4FQP.mjs → setup-CXL6C5Z5.mjs} +9 -9
- package/dist/cli/{setup-HREDVNF2.js → setup-JONYJYBK.js} +44 -44
- package/dist/cli/{skills-4X6WS2PT.mjs → skills-TOMVQD5O.mjs} +7 -7
- package/dist/cli/skills-UK5AK35I.js +33 -0
- package/dist/cli/{tools-YR46YFR2.mjs → tools-B2LJK4MM.mjs} +16 -16
- package/dist/cli/tools-HH6E5G7J.js +55 -0
- package/dist/cli/{update-5CYAZX5X.mjs → update-CD6T2REC.mjs} +9 -9
- package/dist/cli/update-EGXT3EIJ.js +33 -0
- package/dist/cli/{workflow-M7AA722W.js → workflow-PZML43E2.js} +17 -17
- package/dist/cli/{workflow-NKQ3CWPK.mjs → workflow-ZAOBP6I7.mjs} +7 -7
- package/dist/{client-AjmfA5nR.d.mts → client-B1s1yJdT.d.mts} +1 -1
- package/dist/{client-DMyY-QL8.d.ts → client-DqUseRzd.d.ts} +1 -1
- package/dist/{client-core-C705vFyV.d.ts → client-core-Bd7tP9Uz.d.ts} +45 -6
- package/dist/{client-core-CpBbvLEX.d.mts → client-core-BijQEQ70.d.mts} +45 -6
- package/dist/{compiler-manifest-hqIAukR0.d.mts → compiler-manifest-B1m5tRzz.d.mts} +23 -17
- package/dist/{compiler-manifest-hqIAukR0.d.ts → compiler-manifest-B1m5tRzz.d.ts} +23 -17
- package/dist/{errors-CRO2qQ6k.d.ts → errors-3g71OMXy.d.ts} +1 -1
- package/dist/{errors-DIHVetNY.d.mts → errors-QiC6am-q.d.mts} +1 -1
- package/dist/{index-DVZ_X4Gb.d.mts → index-Biz5toO4.d.mts} +1 -1
- package/dist/{index-B8DVoeYt.d.ts → index-CIBiwzhK.d.ts} +1 -1
- package/dist/index.d.mts +15 -9
- package/dist/index.d.ts +15 -9
- package/dist/index.js +9 -9
- package/dist/index.mjs +6 -6
- package/dist/install-integrity.json +112 -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 +2 -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-2ZY57Q7W.js +0 -41
- package/dist/cli/describe-IB7SUIUT.js +0 -48
- package/dist/cli/git-A2QIIMW5.js +0 -25
- package/dist/cli/search-HN6SVDQU.js +0 -32
- package/dist/cli/skills-QZ6HZX2T.js +0 -33
- package/dist/cli/tools-KFIH7UWB.js +0 -55
- package/dist/cli/update-VZLEJLWJ.js +0 -33
|
@@ -29,6 +29,18 @@ export type DetachedDaytonaStartupDiagnostic = {
|
|
|
29
29
|
errorCode?: string;
|
|
30
30
|
transportAttemptId?: string;
|
|
31
31
|
} | null;
|
|
32
|
+
/** Fixed summaries only: raw runner output can contain customer secrets. */
|
|
33
|
+
launcherFailure?: {
|
|
34
|
+
reason:
|
|
35
|
+
| 'node_not_found'
|
|
36
|
+
| 'node_loader_failed'
|
|
37
|
+
| 'permission_denied'
|
|
38
|
+
| 'argument_list_too_long'
|
|
39
|
+
| 'exec_format_error'
|
|
40
|
+
| 'unknown_exit_127'
|
|
41
|
+
| 'output_unavailable';
|
|
42
|
+
nodeCheckExitCode: number;
|
|
43
|
+
};
|
|
32
44
|
errors: string[];
|
|
33
45
|
};
|
|
34
46
|
|
|
@@ -124,6 +136,7 @@ function summarizeProcesses(value: unknown): {
|
|
|
124
136
|
* Process names are reduced to a Node count before logging; runner logs,
|
|
125
137
|
* output, and arbitrary process names are deliberately excluded because no
|
|
126
138
|
* heuristic redactor can guarantee removal of arbitrary customer secrets.
|
|
139
|
+
* Exit-127 shell failures are classified inside the sandbox into fixed enums.
|
|
127
140
|
*/
|
|
128
141
|
export async function inspectDetachedDaytonaStartup(input: {
|
|
129
142
|
sandbox: DaytonaSandbox;
|
|
@@ -131,6 +144,7 @@ export async function inspectDetachedDaytonaStartup(input: {
|
|
|
131
144
|
cmdId: string;
|
|
132
145
|
exitCodePath: string;
|
|
133
146
|
startupDiagnosticPath?: string;
|
|
147
|
+
outputPath?: string;
|
|
134
148
|
}): Promise<DetachedDaytonaStartupDiagnostic> {
|
|
135
149
|
const errors: string[] = [];
|
|
136
150
|
const diagnostic: DetachedDaytonaStartupDiagnostic = {
|
|
@@ -189,6 +203,70 @@ export async function inspectDetachedDaytonaStartup(input: {
|
|
|
189
203
|
.catch(() => errors.push('process_snapshot_failed')),
|
|
190
204
|
]);
|
|
191
205
|
|
|
206
|
+
if (
|
|
207
|
+
input.outputPath &&
|
|
208
|
+
diagnostic.exitCodeFile === 127 &&
|
|
209
|
+
!diagnostic.runnerStartup
|
|
210
|
+
) {
|
|
211
|
+
// The failed shell is retired immediately after this inspection. Classify
|
|
212
|
+
// its bounded output inside the sandbox, returning only known enum values
|
|
213
|
+
// and an exit code. Never ship arbitrary output through a heuristic redactor.
|
|
214
|
+
const quotedPath = "'" + input.outputPath.replace(/'/g, "'\\''") + "'";
|
|
215
|
+
try {
|
|
216
|
+
const result = await input.sandbox.process.executeCommand(
|
|
217
|
+
`
|
|
218
|
+
if [ -r ${quotedPath} ]; then
|
|
219
|
+
startup_tail="$(tail -c 4096 -- ${quotedPath} 2>/dev/null)"
|
|
220
|
+
case "$startup_tail" in
|
|
221
|
+
*"error while loading shared libraries"*) startup_reason=node_loader_failed ;;
|
|
222
|
+
*"not found: node"*|*node*"not found"*|*node*"No such file"*) startup_reason=node_not_found ;;
|
|
223
|
+
*"Permission denied"*) startup_reason=permission_denied ;;
|
|
224
|
+
*"Argument list too long"*) startup_reason=argument_list_too_long ;;
|
|
225
|
+
*"Exec format error"*) startup_reason=exec_format_error ;;
|
|
226
|
+
*) startup_reason=unknown_exit_127 ;;
|
|
227
|
+
esac
|
|
228
|
+
else
|
|
229
|
+
startup_reason=output_unavailable
|
|
230
|
+
fi
|
|
231
|
+
node --version >/dev/null 2>&1
|
|
232
|
+
startup_node_code=$?
|
|
233
|
+
printf '{"reason":"%s","nodeCheckExitCode":%s}\\n' "$startup_reason" "$startup_node_code"
|
|
234
|
+
`,
|
|
235
|
+
undefined,
|
|
236
|
+
{},
|
|
237
|
+
5,
|
|
238
|
+
);
|
|
239
|
+
const value = JSON.parse(result.result) as Record<string, unknown>;
|
|
240
|
+
const reasons = [
|
|
241
|
+
'node_not_found',
|
|
242
|
+
'node_loader_failed',
|
|
243
|
+
'permission_denied',
|
|
244
|
+
'argument_list_too_long',
|
|
245
|
+
'exec_format_error',
|
|
246
|
+
'unknown_exit_127',
|
|
247
|
+
'output_unavailable',
|
|
248
|
+
];
|
|
249
|
+
if (
|
|
250
|
+
typeof value.reason !== 'string' ||
|
|
251
|
+
!reasons.includes(value.reason) ||
|
|
252
|
+
typeof value.nodeCheckExitCode !== 'number' ||
|
|
253
|
+
!Number.isInteger(value.nodeCheckExitCode) ||
|
|
254
|
+
value.nodeCheckExitCode < 0 ||
|
|
255
|
+
value.nodeCheckExitCode > 255
|
|
256
|
+
) {
|
|
257
|
+
errors.push('launcher_diagnostic_invalid');
|
|
258
|
+
} else {
|
|
259
|
+
diagnostic.launcherFailure = {
|
|
260
|
+
reason: value.reason as NonNullable<
|
|
261
|
+
DetachedDaytonaStartupDiagnostic['launcherFailure']
|
|
262
|
+
>['reason'],
|
|
263
|
+
nodeCheckExitCode: value.nodeCheckExitCode,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
errors.push('launcher_diagnostic_unavailable');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
192
270
|
return diagnostic;
|
|
193
271
|
}
|
|
194
272
|
|
|
@@ -225,6 +303,7 @@ export type DetachedDaytonaRunnerSupervisor = {
|
|
|
225
303
|
command: string;
|
|
226
304
|
exitCodePath: string;
|
|
227
305
|
startupDiagnosticPath?: string;
|
|
306
|
+
outputPath?: string;
|
|
228
307
|
}): Promise<Extract<DetachedDaytonaRunnerState, { phase: 'ready' }>>;
|
|
229
308
|
};
|
|
230
309
|
|
|
@@ -394,7 +473,7 @@ export function prepareDetachedDaytonaRunner(input: {
|
|
|
394
473
|
let started = false;
|
|
395
474
|
|
|
396
475
|
return {
|
|
397
|
-
async start({ command, exitCodePath, startupDiagnosticPath }) {
|
|
476
|
+
async start({ command, exitCodePath, startupDiagnosticPath, outputPath }) {
|
|
398
477
|
if (started) {
|
|
399
478
|
throw new Error(
|
|
400
479
|
`Detached Daytona runner session ${input.sessionId} was already started.`,
|
|
@@ -435,6 +514,7 @@ export function prepareDetachedDaytonaRunner(input: {
|
|
|
435
514
|
cmdId: state.cmdId,
|
|
436
515
|
exitCodePath,
|
|
437
516
|
startupDiagnosticPath,
|
|
517
|
+
outputPath,
|
|
438
518
|
readiness: input.readiness,
|
|
439
519
|
baselineHeartbeatAt: state.baselineHeartbeatAt,
|
|
440
520
|
runControl: input.runControl,
|
|
@@ -461,6 +541,7 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
|
|
|
461
541
|
cmdId: string;
|
|
462
542
|
exitCodePath: string;
|
|
463
543
|
startupDiagnosticPath?: string;
|
|
544
|
+
outputPath?: string;
|
|
464
545
|
readiness: DetachedRunnerReadinessPort;
|
|
465
546
|
baselineHeartbeatAt: string | null;
|
|
466
547
|
runControl?: <T>(
|
|
@@ -517,6 +598,7 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
|
|
|
517
598
|
cmdId: input.cmdId,
|
|
518
599
|
exitCodePath: input.exitCodePath,
|
|
519
600
|
startupDiagnosticPath: input.startupDiagnosticPath,
|
|
601
|
+
outputPath: input.outputPath,
|
|
520
602
|
});
|
|
521
603
|
const diagnostic = await (input.runControl
|
|
522
604
|
? input.runControl(inspect)
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
recoveryForRunnerStartupNotLive,
|
|
68
68
|
} from '../../play-run-recovery-policy';
|
|
69
69
|
import {
|
|
70
|
+
DaytonaNodeUnavailableError,
|
|
70
71
|
stageDaytonaRunnerPayload,
|
|
71
72
|
withRunnerSandboxProvider,
|
|
72
73
|
} from './daytona-payload-transport';
|
|
@@ -1506,6 +1507,7 @@ const DAYTONA_UNANSWERED_REQUEST_CODES: ReadonlySet<string> = new Set([
|
|
|
1506
1507
|
* loud too, because its input may be too large for any sandbox.
|
|
1507
1508
|
*/
|
|
1508
1509
|
function isFailedDaytonaRunnerStart(error: unknown): boolean {
|
|
1510
|
+
if (error instanceof DaytonaNodeUnavailableError) return true;
|
|
1509
1511
|
if (error instanceof DaytonaControlTimeoutError) return true;
|
|
1510
1512
|
if (!isDaytonaProviderError(error)) return false;
|
|
1511
1513
|
const status = daytonaErrorStatusForLog(error);
|
|
@@ -1536,6 +1538,9 @@ function startFailureForLog(error: unknown) {
|
|
|
1536
1538
|
errorName: error instanceof Error ? error.name : null,
|
|
1537
1539
|
providerStatus: daytonaErrorStatusForLog(error),
|
|
1538
1540
|
providerCode: daytonaErrorCodeForLog(error),
|
|
1541
|
+
...(error instanceof DaytonaNodeUnavailableError
|
|
1542
|
+
? { nodeReadiness: error.diagnostic }
|
|
1543
|
+
: {}),
|
|
1539
1544
|
...(diagnostic
|
|
1540
1545
|
? {
|
|
1541
1546
|
// Retain the already-sanitized evidence before sandbox retirement.
|
|
@@ -1563,6 +1568,7 @@ function startFailureForLog(error: unknown) {
|
|
|
1563
1568
|
transportAttemptId: startup.transportAttemptId,
|
|
1564
1569
|
}
|
|
1565
1570
|
: null,
|
|
1571
|
+
launcherFailure: diagnostic.launcherFailure,
|
|
1566
1572
|
inspectionErrorCount: diagnostic.errors.length,
|
|
1567
1573
|
},
|
|
1568
1574
|
}
|
|
@@ -2092,6 +2098,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
2092
2098
|
command: stagedPayload.command,
|
|
2093
2099
|
exitCodePath: stagedPayload.exitCodePath,
|
|
2094
2100
|
startupDiagnosticPath: stagedPayload.startupDiagnosticPath,
|
|
2101
|
+
outputPath: stagedPayload.outputPath,
|
|
2095
2102
|
});
|
|
2096
2103
|
} catch (error) {
|
|
2097
2104
|
if (
|
|
@@ -88,6 +88,9 @@ export function resolveRuntimeSandboxComputeItems(input: {
|
|
|
88
88
|
sandboxId,
|
|
89
89
|
startedAt: resource.startedAt,
|
|
90
90
|
endedAt,
|
|
91
|
+
maxBillingDurationSeconds: resource.maxBillingDurationSeconds,
|
|
92
|
+
observedWallTimeSeconds:
|
|
93
|
+
Math.max(0, observedEndedAt - resource.startedAt) / 1_000,
|
|
91
94
|
}),
|
|
92
95
|
);
|
|
93
96
|
}
|
|
@@ -201,6 +204,9 @@ export function resolveDaytonaRuntimeComputeItems(input: {
|
|
|
201
204
|
sandboxId,
|
|
202
205
|
startedAt: resource.startedAt,
|
|
203
206
|
endedAt,
|
|
207
|
+
maxBillingDurationSeconds: resource.maxBillingDurationSeconds,
|
|
208
|
+
observedWallTimeSeconds:
|
|
209
|
+
Math.max(0, observedEndedAt - resource.startedAt) / 1_000,
|
|
204
210
|
}),
|
|
205
211
|
);
|
|
206
212
|
}
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
CHANGED
|
@@ -38,6 +38,26 @@ const RATE_SCOPE_HOLD_TABLE = 'rate_scope_holds';
|
|
|
38
38
|
const PHYSICAL_DISPATCH_RULE_ID = 'runtime:physical-dispatch';
|
|
39
39
|
|
|
40
40
|
const MIN_CONCURRENCY_WAIT_MS = 10;
|
|
41
|
+
/**
|
|
42
|
+
* Longest re-ask when every concurrency slot is taken. A full scope re-asks
|
|
43
|
+
* about when its next slot should free: the average age of its live leases
|
|
44
|
+
* divided by their count. A fast provider stays at the 10 ms floor; one whose
|
|
45
|
+
* calls hold a slot for minutes no longer re-locks its scope row every 10 ms.
|
|
46
|
+
* On 2026-11-06 five such runs re-asked about 160 times a second for hours.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_CONCURRENCY_WAIT_MS = 1_000;
|
|
49
|
+
/**
|
|
50
|
+
* A healthy scope lock is held for milliseconds. An acquire that cannot get it
|
|
51
|
+
* within this budget stops waiting on the server instead of leaving a backend
|
|
52
|
+
* queued behind a stuck holder after its caller gave up. On 2026-11-06 one
|
|
53
|
+
* stuck holder queued such abandoned waits for 16 minutes until they exhausted
|
|
54
|
+
* the shared scheduler pool.
|
|
55
|
+
*/
|
|
56
|
+
const RATE_STATE_LOCK_TIMEOUT_MS = 3_000;
|
|
57
|
+
/** Ends a bounded acquire statement before the gateway's 30 s caller budget. */
|
|
58
|
+
const RATE_STATE_STATEMENT_TIMEOUT_MS = 15_000;
|
|
59
|
+
/** Re-ask after a lock timeout, as for any other busy scope. */
|
|
60
|
+
const RATE_STATE_LOCK_BUSY_WAIT_MS = 1_000;
|
|
41
61
|
/**
|
|
42
62
|
* Longest a requester held back by the fair-share cap sleeps before it re-asks.
|
|
43
63
|
* The natural wait is the rest of the current fair-share window (a sibling's
|
|
@@ -308,13 +328,13 @@ async function queryRateStateWithProbeBaseline<
|
|
|
308
328
|
parameters: unknown[],
|
|
309
329
|
) {
|
|
310
330
|
try {
|
|
311
|
-
return await
|
|
331
|
+
return await queryBoundedRateState<Row>(client, sql(true), parameters);
|
|
312
332
|
} catch (error) {
|
|
313
333
|
if (!isMissingProbeBaselineColumnError(error)) throw error;
|
|
314
334
|
// PostgreSQL rejects the statement before any grant or reservation writes.
|
|
315
335
|
// Retry only that statement; previously accepted success feedback is not
|
|
316
336
|
// applied twice. No DDL, migration preflight, or global schema floor.
|
|
317
|
-
return await
|
|
337
|
+
return await queryBoundedRateState<Row>(client, sql(false), parameters);
|
|
318
338
|
}
|
|
319
339
|
}
|
|
320
340
|
|
|
@@ -771,6 +791,32 @@ async function seedBucketRows(
|
|
|
771
791
|
},
|
|
772
792
|
seeds: RateBucketSeed[],
|
|
773
793
|
nowMs: number | null,
|
|
794
|
+
): Promise<void> {
|
|
795
|
+
// Cold path only, so its own bounded transaction costs two round trips once
|
|
796
|
+
// per bucket per process. Its insert waits on the same scope row as a grant.
|
|
797
|
+
await client.query('BEGIN');
|
|
798
|
+
try {
|
|
799
|
+
await boundRateStateTransaction(client);
|
|
800
|
+
await seedBucketRowsStatement(client, input, seeds, nowMs);
|
|
801
|
+
await client.query('COMMIT');
|
|
802
|
+
} catch (error) {
|
|
803
|
+
try {
|
|
804
|
+
await client.query('ROLLBACK');
|
|
805
|
+
} catch {
|
|
806
|
+
// Preserve the original seed/storage error.
|
|
807
|
+
}
|
|
808
|
+
throw rateStateLockBusyOr(error);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function seedBucketRowsStatement(
|
|
813
|
+
client: PostgresSchedulerQueryClient,
|
|
814
|
+
input: { bucketId: string } & {
|
|
815
|
+
schema?: string | null;
|
|
816
|
+
schedulerSchema?: string | null;
|
|
817
|
+
},
|
|
818
|
+
seeds: RateBucketSeed[],
|
|
819
|
+
nowMs: number | null,
|
|
774
820
|
): Promise<void> {
|
|
775
821
|
await client.query(
|
|
776
822
|
`
|
|
@@ -1797,6 +1843,7 @@ async function acquireWithConcurrency(
|
|
|
1797
1843
|
while (true) {
|
|
1798
1844
|
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
1799
1845
|
try {
|
|
1846
|
+
await boundRateStateTransaction(client);
|
|
1800
1847
|
await client.query(
|
|
1801
1848
|
`SELECT bucket_id FROM ${scopeHoldTableName(input)}
|
|
1802
1849
|
WHERE bucket_id = $1 FOR UPDATE`,
|
|
@@ -1824,11 +1871,73 @@ async function acquireWithConcurrency(
|
|
|
1824
1871
|
probeBaselineSupported = false;
|
|
1825
1872
|
continue;
|
|
1826
1873
|
}
|
|
1827
|
-
throw error;
|
|
1874
|
+
throw rateStateLockBusyOr(error);
|
|
1828
1875
|
}
|
|
1829
1876
|
}
|
|
1830
1877
|
}
|
|
1831
1878
|
|
|
1879
|
+
/**
|
|
1880
|
+
* Bound the current rate-state transaction. `SET LOCAL` keeps the limits inside
|
|
1881
|
+
* this transaction: the scheduler pool runs behind PgBouncer transaction
|
|
1882
|
+
* pooling, where a session setting would leak to another caller.
|
|
1883
|
+
*/
|
|
1884
|
+
async function boundRateStateTransaction(
|
|
1885
|
+
client: PostgresSchedulerQueryClient,
|
|
1886
|
+
): Promise<void> {
|
|
1887
|
+
await client.query(
|
|
1888
|
+
`SELECT set_config('lock_timeout', $1, true),
|
|
1889
|
+
set_config('statement_timeout', $2, true)`,
|
|
1890
|
+
[`${RATE_STATE_LOCK_TIMEOUT_MS}ms`, `${RATE_STATE_STATEMENT_TIMEOUT_MS}ms`],
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** Bound one formerly-autocommit statement without leaking session settings. */
|
|
1895
|
+
async function queryBoundedRateState<
|
|
1896
|
+
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
1897
|
+
>(client: PostgresSchedulerQueryClient, sql: string, parameters?: unknown[]) {
|
|
1898
|
+
await client.query('BEGIN');
|
|
1899
|
+
try {
|
|
1900
|
+
await boundRateStateTransaction(client);
|
|
1901
|
+
const result = await client.query<Row>(sql, parameters);
|
|
1902
|
+
await client.query('COMMIT');
|
|
1903
|
+
return result;
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
try {
|
|
1906
|
+
await client.query('ROLLBACK');
|
|
1907
|
+
} catch {
|
|
1908
|
+
// Preserve the original statement error.
|
|
1909
|
+
}
|
|
1910
|
+
throw rateStateLockBusyOr(error);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/** A scope row another transaction still holds after the lock budget. */
|
|
1915
|
+
class RateStateLockBusyError extends Error {
|
|
1916
|
+
constructor(cause: unknown) {
|
|
1917
|
+
super('Rate-state scope lock was not available within its budget.', {
|
|
1918
|
+
cause,
|
|
1919
|
+
});
|
|
1920
|
+
this.name = 'RateStateLockBusyError';
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* A bounded rate-state transaction that ran out of its lock or statement
|
|
1926
|
+
* budget (57014 is statement_timeout).
|
|
1927
|
+
*/
|
|
1928
|
+
function isRateStateBusy(error: unknown): boolean {
|
|
1929
|
+
return (
|
|
1930
|
+
error instanceof RateStateLockBusyError ||
|
|
1931
|
+
(error as { code?: unknown } | null)?.code === '57014'
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function rateStateLockBusyOr(error: unknown): unknown {
|
|
1936
|
+
return (error as { code?: unknown } | null)?.code === '55P03'
|
|
1937
|
+
? new RateStateLockBusyError(error)
|
|
1938
|
+
: error;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1832
1941
|
async function grantWithConcurrency(
|
|
1833
1942
|
client: PostgresSchedulerQueryClient,
|
|
1834
1943
|
input: RateStateAcquireInput,
|
|
@@ -2315,7 +2424,29 @@ async function grantWithConcurrency(
|
|
|
2315
2424
|
(SELECT cool_until_ms FROM cooling),
|
|
2316
2425
|
(SELECT retry_at_ms FROM cooling)
|
|
2317
2426
|
) - (SELECT now_ms FROM refreshed LIMIT 1)${fair?.waitBranch ?? ''}
|
|
2318
|
-
WHEN (SELECT free FROM free_slots) <= 0 THEN
|
|
2427
|
+
WHEN (SELECT free FROM free_slots) <= 0 THEN (
|
|
2428
|
+
-- Re-ask about when the next slot should free. Leases are never
|
|
2429
|
+
-- renewed, so a lease's age is its TTL minus its remaining life.
|
|
2430
|
+
SELECT LEAST(
|
|
2431
|
+
${MAX_CONCURRENCY_WAIT_MS}::bigint,
|
|
2432
|
+
GREATEST(
|
|
2433
|
+
$7::bigint,
|
|
2434
|
+
CEIL(
|
|
2435
|
+
COALESCE(
|
|
2436
|
+
AVG(
|
|
2437
|
+
(SELECT now_ms FROM now_value)
|
|
2438
|
+
- (cl.expires_at_ms - $8::bigint)
|
|
2439
|
+
),
|
|
2440
|
+
0
|
|
2441
|
+
) / GREATEST((SELECT active_count FROM concurrency_context), 1)
|
|
2442
|
+
)::bigint
|
|
2443
|
+
)
|
|
2444
|
+
)
|
|
2445
|
+
FROM ${concurrencyLeaseTableName(input)} cl
|
|
2446
|
+
WHERE cl.bucket_id = $1
|
|
2447
|
+
AND cl.rule_id = ANY($5::text[])
|
|
2448
|
+
AND cl.expires_at_ms > (SELECT now_ms FROM now_value)
|
|
2449
|
+
)
|
|
2319
2450
|
ELSE LEAST((SELECT pending_recovery_wait_ms FROM provider_hold), (
|
|
2320
2451
|
SELECT CEIL(
|
|
2321
2452
|
MAX(
|
|
@@ -2417,9 +2548,28 @@ export async function acquirePostgresRateState(
|
|
|
2417
2548
|
client: PostgresSchedulerQueryClient,
|
|
2418
2549
|
input: RateStateAcquireInput,
|
|
2419
2550
|
): Promise<PlayRunnerRateStateAcquireResult> {
|
|
2420
|
-
return await withRateStateAvailabilityGuard(input, () =>
|
|
2421
|
-
|
|
2422
|
-
|
|
2551
|
+
return await withRateStateAvailabilityGuard(input, async () => {
|
|
2552
|
+
try {
|
|
2553
|
+
const result = await acquireRateStateStatements(client, input);
|
|
2554
|
+
return {
|
|
2555
|
+
...result,
|
|
2556
|
+
acknowledgedObservationBatch: concurrencyObservationBatchFrom(
|
|
2557
|
+
input.concurrencyObservationBatch,
|
|
2558
|
+
),
|
|
2559
|
+
};
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
// A busy scope is normal backpressure. A rolled-back feedback fold must
|
|
2562
|
+
// keep its exact batch on the runner; a committed fold is safe to replay
|
|
2563
|
+
// after an uncertain grant because the observer watermark deduplicates it.
|
|
2564
|
+
if (!(error instanceof RateStateLockBusyError)) throw error;
|
|
2565
|
+
return {
|
|
2566
|
+
granted: 0,
|
|
2567
|
+
waitMs: RATE_STATE_LOCK_BUSY_WAIT_MS,
|
|
2568
|
+
leaseIds: [],
|
|
2569
|
+
acknowledgedObservationBatch: null,
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2423
2573
|
}
|
|
2424
2574
|
|
|
2425
2575
|
/**
|
|
@@ -2434,10 +2584,11 @@ export async function validatePostgresRateStatePermit(
|
|
|
2434
2584
|
},
|
|
2435
2585
|
): Promise<PlayRunnerRateStateValidateResult> {
|
|
2436
2586
|
return await withRateStateAvailabilityGuard(input, async () => {
|
|
2437
|
-
const result = await
|
|
2587
|
+
const result = await queryBoundedRateState<{
|
|
2438
2588
|
server_now_ms: string | number;
|
|
2439
2589
|
retry_at_ms: string | number;
|
|
2440
2590
|
}>(
|
|
2591
|
+
client,
|
|
2441
2592
|
`
|
|
2442
2593
|
WITH now_value AS (
|
|
2443
2594
|
SELECT COALESCE(
|
|
@@ -2495,13 +2646,14 @@ export async function claimPostgresProviderDispatch(
|
|
|
2495
2646
|
}
|
|
2496
2647
|
> {
|
|
2497
2648
|
return withRateStateAvailabilityGuard(input, async () => {
|
|
2498
|
-
const result = await
|
|
2649
|
+
const result = await queryBoundedRateState<{
|
|
2499
2650
|
server_now_ms: string | number;
|
|
2500
2651
|
retry_at_ms: string | number;
|
|
2501
2652
|
allowed: boolean;
|
|
2502
2653
|
cadence_wait: boolean;
|
|
2503
2654
|
stale_permit: boolean;
|
|
2504
2655
|
}>(
|
|
2656
|
+
client,
|
|
2505
2657
|
`
|
|
2506
2658
|
WITH now_value AS (
|
|
2507
2659
|
SELECT COALESCE($2::bigint,
|
|
@@ -2758,11 +2910,12 @@ async function concurrencyThrottleFrom(
|
|
|
2758
2910
|
if (concurrencyRuleIds.length === 0) {
|
|
2759
2911
|
return { attribution: 'not_observed', observations: [], batch: null };
|
|
2760
2912
|
}
|
|
2761
|
-
const attributedLease = await
|
|
2913
|
+
const attributedLease = await queryBoundedRateState<{
|
|
2762
2914
|
in_flight_at_start: number | null;
|
|
2763
2915
|
target_requests_per_second: number | null;
|
|
2764
2916
|
minimum_rtt_ms: number | null;
|
|
2765
2917
|
}>(
|
|
2918
|
+
client,
|
|
2766
2919
|
`
|
|
2767
2920
|
SELECT
|
|
2768
2921
|
in_flight_at_start,
|
|
@@ -2850,13 +3003,17 @@ async function applyConcurrencyObservations(
|
|
|
2850
3003
|
const concurrencyRules = rules.filter(
|
|
2851
3004
|
(rule) => rule.maxConcurrency != null && rule.maxConcurrency > 0,
|
|
2852
3005
|
);
|
|
2853
|
-
const countSuccesses = async () => {
|
|
3006
|
+
const countSuccesses = async (bounded = false) => {
|
|
2854
3007
|
const successes = Math.max(
|
|
2855
3008
|
0,
|
|
2856
3009
|
Math.min(1_000_000, Math.trunc(input.observedSuccesses ?? 0)),
|
|
2857
3010
|
);
|
|
2858
3011
|
if (successes <= 0) return;
|
|
2859
|
-
|
|
3012
|
+
const query = bounded
|
|
3013
|
+
? (sql: string, params: unknown[]) =>
|
|
3014
|
+
queryBoundedRateState(client, sql, params)
|
|
3015
|
+
: (sql: string, params: unknown[]) => client.query(sql, params);
|
|
3016
|
+
await query(
|
|
2860
3017
|
`
|
|
2861
3018
|
UPDATE ${scopeHoldTableName(input)}
|
|
2862
3019
|
SET rate_successes = rate_successes + $2::bigint,
|
|
@@ -2881,7 +3038,7 @@ async function applyConcurrencyObservations(
|
|
|
2881
3038
|
if (batch === null) {
|
|
2882
3039
|
// Legacy writers have no observation identity. Preserve their success
|
|
2883
3040
|
// input; new writers always supply the replay-safe batch below.
|
|
2884
|
-
await countSuccesses();
|
|
3041
|
+
await countSuccesses(true);
|
|
2885
3042
|
return {
|
|
2886
3043
|
disposition: 'unbatched',
|
|
2887
3044
|
ruleCount: 0,
|
|
@@ -2895,6 +3052,7 @@ async function applyConcurrencyObservations(
|
|
|
2895
3052
|
|
|
2896
3053
|
await client.query('BEGIN');
|
|
2897
3054
|
try {
|
|
3055
|
+
await boundRateStateTransaction(client);
|
|
2898
3056
|
const accepted = await client.query<{ last_sequence: number }>(
|
|
2899
3057
|
`
|
|
2900
3058
|
WITH now_value AS (
|
|
@@ -3103,7 +3261,7 @@ async function applyConcurrencyObservations(
|
|
|
3103
3261
|
} catch {
|
|
3104
3262
|
// Preserve the original controller/storage error.
|
|
3105
3263
|
}
|
|
3106
|
-
throw error;
|
|
3264
|
+
throw rateStateLockBusyOr(error);
|
|
3107
3265
|
}
|
|
3108
3266
|
}
|
|
3109
3267
|
|
|
@@ -3130,8 +3288,8 @@ async function acquireRateStateStatements(
|
|
|
3130
3288
|
: acquirePureRate(client, grantInput, rules, seeds, requested);
|
|
3131
3289
|
|
|
3132
3290
|
// Cold path only: seed missing rows / refresh changed configuration. A warm
|
|
3133
|
-
// pure-rate acquire
|
|
3134
|
-
//
|
|
3291
|
+
// pure-rate acquire has one grant statement in a bounded transaction.
|
|
3292
|
+
// Concurrency-bearing acquires lock the scope before the lease snapshot.
|
|
3135
3293
|
let seeded = false;
|
|
3136
3294
|
if (!warmRateBuckets.has(warmKey)) {
|
|
3137
3295
|
await seedBucketRows(client, input, seeds, nowMs);
|
|
@@ -3179,7 +3337,16 @@ async function acquireRateStateStatements(
|
|
|
3179
3337
|
// so a row widened out of band is repaired here instead of waiting for a
|
|
3180
3338
|
// process restart. Repairing after the grant keeps the acquire itself to a
|
|
3181
3339
|
// single granting statement, so no permit is ever issued twice.
|
|
3182
|
-
|
|
3340
|
+
try {
|
|
3341
|
+
await seedBucketRows(client, input, seeds, nowMs);
|
|
3342
|
+
} catch (error) {
|
|
3343
|
+
// The grant above already committed. A busy scope must not turn its
|
|
3344
|
+
// leases into a zero answer the runner never releases. Leave the bucket
|
|
3345
|
+
// cold so the next acquire repairs before it grants.
|
|
3346
|
+
if (!isRateStateBusy(error)) throw error;
|
|
3347
|
+
warmRateBuckets.delete(warmKey);
|
|
3348
|
+
return acquireResultOf(outcome);
|
|
3349
|
+
}
|
|
3183
3350
|
}
|
|
3184
3351
|
markRateBucketWarm(warmKey);
|
|
3185
3352
|
return acquireResultOf(outcome);
|
|
@@ -3199,6 +3366,27 @@ async function releaseRateStateStatements(
|
|
|
3199
3366
|
input: PlayRunnerRateStateReleaseInput & { nowMs?: number },
|
|
3200
3367
|
): Promise<void> {
|
|
3201
3368
|
const allRules = normalizeRules(input.rules as AcquirePacingRule[]);
|
|
3369
|
+
if (
|
|
3370
|
+
input.observedSuccesses !== undefined ||
|
|
3371
|
+
input.concurrencyObservations !== undefined ||
|
|
3372
|
+
input.concurrencyObservationBatch !== undefined
|
|
3373
|
+
) {
|
|
3374
|
+
const batch = concurrencyObservationBatchFrom(
|
|
3375
|
+
input.concurrencyObservationBatch,
|
|
3376
|
+
);
|
|
3377
|
+
if (!batch) {
|
|
3378
|
+
throw new Error(
|
|
3379
|
+
'Rate-state success feedback requires a replay-safe observation batch.',
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
await applyConcurrencyObservations(
|
|
3383
|
+
client,
|
|
3384
|
+
input,
|
|
3385
|
+
allRules,
|
|
3386
|
+
concurrencyObservationsFrom(input.concurrencyObservations),
|
|
3387
|
+
batch,
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3202
3390
|
if (allRules.length === 0) return;
|
|
3203
3391
|
const refundReservationIds = [
|
|
3204
3392
|
...new Set(
|
|
@@ -3216,7 +3404,8 @@ async function releaseRateStateStatements(
|
|
|
3216
3404
|
// no scope owner. Create its uninitialized hold before taking the normal
|
|
3217
3405
|
// hold -> reservation -> rule lock path; seedBucketRows will initialize its
|
|
3218
3406
|
// learner on the next admission and preserve this inherited deadline.
|
|
3219
|
-
await
|
|
3407
|
+
await queryBoundedRateState(
|
|
3408
|
+
client,
|
|
3220
3409
|
`
|
|
3221
3410
|
INSERT INTO ${scopeHoldTableName(input)} (bucket_id, retry_at_ms)
|
|
3222
3411
|
SELECT bucket_id, MAX(claimed_retry_at_ms)
|
|
@@ -3232,7 +3421,8 @@ async function releaseRateStateStatements(
|
|
|
3232
3421
|
// a bucket, and an HTTP retry sees no reservation left to refund. The shared
|
|
3233
3422
|
// dispatch cursor deliberately stays monotonic, so cancellation can never
|
|
3234
3423
|
// compress provider starts into a burst.
|
|
3235
|
-
await
|
|
3424
|
+
await queryBoundedRateState(
|
|
3425
|
+
client,
|
|
3236
3426
|
`
|
|
3237
3427
|
WITH now_value AS (
|
|
3238
3428
|
SELECT COALESCE(
|
|
@@ -3318,7 +3508,8 @@ async function releaseRateStateStatements(
|
|
|
3318
3508
|
];
|
|
3319
3509
|
if (leaseIds.length === 0) return;
|
|
3320
3510
|
// DELETE by holder token is naturally idempotent, including HTTP retries.
|
|
3321
|
-
await
|
|
3511
|
+
await queryBoundedRateState(
|
|
3512
|
+
client,
|
|
3322
3513
|
`
|
|
3323
3514
|
DELETE FROM ${concurrencyLeaseTableName(input)}
|
|
3324
3515
|
WHERE bucket_id = $1
|
|
@@ -3376,11 +3567,12 @@ async function penalizeRateStateStatements(
|
|
|
3376
3567
|
// min_rps.
|
|
3377
3568
|
// - seen_backpressure flips the ramp from slow-start (x2) to additive.
|
|
3378
3569
|
// - tokens drained: no more grants until refill re-accrues past the cooldown.
|
|
3379
|
-
await
|
|
3570
|
+
await queryBoundedRateState(
|
|
3571
|
+
client,
|
|
3380
3572
|
`INSERT INTO ${scopeHoldTableName(input)} (bucket_id) VALUES ($1) ON CONFLICT DO NOTHING`,
|
|
3381
3573
|
[input.bucketId],
|
|
3382
3574
|
);
|
|
3383
|
-
const rateStateResult = await
|
|
3575
|
+
const rateStateResult = await queryBoundedRateState<{
|
|
3384
3576
|
retry_at_ms: string | number | null;
|
|
3385
3577
|
cool_until_ms: string | number | null;
|
|
3386
3578
|
rate_rule_count: string | number;
|
|
@@ -3392,6 +3584,7 @@ async function penalizeRateStateStatements(
|
|
|
3392
3584
|
observation_cycle_max_ms: string | number | null;
|
|
3393
3585
|
confirmed_successes_max: string | number | null;
|
|
3394
3586
|
}>(
|
|
3587
|
+
client,
|
|
3395
3588
|
`
|
|
3396
3589
|
WITH now_value AS (
|
|
3397
3590
|
SELECT COALESCE(
|
|
@@ -98,6 +98,32 @@ function sandboxSizeMetadata(cpu: number, memoryGiB: number) {
|
|
|
98
98
|
return entry ? { sandboxSize: entry[0] } : {};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Lets settlement tell a run that used its declared runtime from a duration no
|
|
103
|
+
* runtime limit explains. Billed time stops at the ceiling, so the unclamped
|
|
104
|
+
* duration travels beside it: without it an overrun looks like a run that
|
|
105
|
+
* stayed inside its limit. Items without a ceiling keep their historical shape.
|
|
106
|
+
*/
|
|
107
|
+
function runtimeLimitMetadata(
|
|
108
|
+
maxBillingDurationSeconds: number | null | undefined,
|
|
109
|
+
observedWallTimeSeconds: number | undefined,
|
|
110
|
+
) {
|
|
111
|
+
if (
|
|
112
|
+
typeof maxBillingDurationSeconds !== 'number' ||
|
|
113
|
+
!Number.isFinite(maxBillingDurationSeconds) ||
|
|
114
|
+
maxBillingDurationSeconds <= 0
|
|
115
|
+
) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
maxBillingDurationSeconds,
|
|
120
|
+
...(typeof observedWallTimeSeconds === 'number' &&
|
|
121
|
+
Number.isFinite(observedWallTimeSeconds)
|
|
122
|
+
? { observedWallTimeSeconds: Math.max(0, observedWallTimeSeconds) }
|
|
123
|
+
: {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
export function resolveDaytonaSandboxComputeItem(input: {
|
|
102
128
|
itemId: string;
|
|
103
129
|
source?: string;
|
|
@@ -108,6 +134,10 @@ export function resolveDaytonaSandboxComputeItem(input: {
|
|
|
108
134
|
sandboxId?: string | null;
|
|
109
135
|
startedAt?: number;
|
|
110
136
|
endedAt?: number;
|
|
137
|
+
/** The resource's own billing ceiling: its timeout plus terminal grace. */
|
|
138
|
+
maxBillingDurationSeconds?: number | null;
|
|
139
|
+
/** Wall time before the ceiling clamped it; recorded only with a ceiling. */
|
|
140
|
+
observedWallTimeSeconds?: number;
|
|
111
141
|
}): ComputeBillingItem {
|
|
112
142
|
const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
|
|
113
143
|
const cpu = Math.max(0, input.cpu);
|
|
@@ -138,6 +168,10 @@ export function resolveDaytonaSandboxComputeItem(input: {
|
|
|
138
168
|
billableDiskGiB,
|
|
139
169
|
startedAt: input.startedAt,
|
|
140
170
|
endedAt: input.endedAt,
|
|
171
|
+
...runtimeLimitMetadata(
|
|
172
|
+
input.maxBillingDurationSeconds,
|
|
173
|
+
input.observedWallTimeSeconds,
|
|
174
|
+
),
|
|
141
175
|
},
|
|
142
176
|
};
|
|
143
177
|
}
|
|
@@ -152,6 +186,10 @@ export function resolveModalSandboxComputeItem(input: {
|
|
|
152
186
|
sandboxId?: string | null;
|
|
153
187
|
startedAt?: number;
|
|
154
188
|
endedAt?: number;
|
|
189
|
+
/** The resource's own billing ceiling: its timeout plus terminal grace. */
|
|
190
|
+
maxBillingDurationSeconds?: number | null;
|
|
191
|
+
/** Wall time before the ceiling clamped it; recorded only with a ceiling. */
|
|
192
|
+
observedWallTimeSeconds?: number;
|
|
155
193
|
}): ComputeBillingItem {
|
|
156
194
|
const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
|
|
157
195
|
const physicalCpuCores = Math.max(0, input.physicalCpuCores);
|
|
@@ -186,6 +224,10 @@ export function resolveModalSandboxComputeItem(input: {
|
|
|
186
224
|
}),
|
|
187
225
|
startedAt: input.startedAt,
|
|
188
226
|
endedAt: input.endedAt,
|
|
227
|
+
...runtimeLimitMetadata(
|
|
228
|
+
input.maxBillingDurationSeconds,
|
|
229
|
+
input.observedWallTimeSeconds,
|
|
230
|
+
),
|
|
189
231
|
},
|
|
190
232
|
};
|
|
191
233
|
}
|