@blaxel/core 0.2.87-preview.174 → 0.2.87
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/h2fetch.js +54 -17
- package/dist/cjs/common/settings.js +29 -3
- package/dist/cjs/common/transient-retry.js +145 -0
- package/dist/cjs/sandbox/drive/drive.js +11 -6
- package/dist/cjs/sandbox/filesystem/filesystem.js +126 -158
- package/dist/cjs/sandbox/process/process.js +29 -16
- package/dist/cjs/types/common/h2fetch.d.ts +7 -0
- package/dist/cjs/types/common/settings.d.ts +22 -2
- package/dist/cjs/types/common/transient-retry.d.ts +30 -0
- package/dist/cjs/types/sandbox/filesystem/filesystem.d.ts +3 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/h2fetch.js +1 -0
- package/dist/cjs-browser/common/settings.js +29 -3
- package/dist/cjs-browser/common/transient-retry.js +145 -0
- package/dist/cjs-browser/sandbox/drive/drive.js +11 -6
- package/dist/cjs-browser/sandbox/filesystem/filesystem.js +126 -158
- package/dist/cjs-browser/sandbox/process/process.js +29 -16
- package/dist/cjs-browser/types/common/h2fetch.d.ts +7 -0
- package/dist/cjs-browser/types/common/settings.d.ts +22 -2
- package/dist/cjs-browser/types/common/transient-retry.d.ts +30 -0
- package/dist/cjs-browser/types/sandbox/filesystem/filesystem.d.ts +3 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/h2fetch.js +53 -17
- package/dist/esm/common/settings.js +29 -3
- package/dist/esm/common/transient-retry.js +141 -0
- package/dist/esm/sandbox/drive/drive.js +11 -6
- package/dist/esm/sandbox/filesystem/filesystem.js +124 -157
- package/dist/esm/sandbox/process/process.js +29 -16
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/h2fetch.js +1 -0
- package/dist/esm-browser/common/settings.js +29 -3
- package/dist/esm-browser/common/transient-retry.js +141 -0
- package/dist/esm-browser/sandbox/drive/drive.js +11 -6
- package/dist/esm-browser/sandbox/filesystem/filesystem.js +124 -157
- package/dist/esm-browser/sandbox/process/process.js +29 -16
- package/package.json +1 -1
|
@@ -1,101 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SandboxFileSystem = void 0;
|
|
3
|
+
exports.SandboxFileSystem = exports.isTransientUploadError = void 0;
|
|
4
|
+
exports.retryOnTransient = retryOnTransient;
|
|
4
5
|
const node_js_1 = require("../../common/node.js");
|
|
5
6
|
const settings_js_1 = require("../../common/settings.js");
|
|
7
|
+
const h2fetch_js_1 = require("../../common/h2fetch.js");
|
|
8
|
+
const transient_retry_js_1 = require("../../common/transient-retry.js");
|
|
6
9
|
const action_js_1 = require("../action.js");
|
|
7
10
|
const index_js_1 = require("../client/index.js");
|
|
8
11
|
// Multipart upload constants
|
|
9
12
|
const MULTIPART_THRESHOLD = 5 * 1024 * 1024; // 5MB
|
|
10
13
|
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per part
|
|
11
14
|
const MAX_PARALLEL_UPLOADS = 3; // Number of parallel part uploads
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
// Deliberately excluded: bare "INTERNAL_ERROR" and "fetch failed". Both are
|
|
23
|
-
// too generic on their own (an application 500 body or any failed fetch would
|
|
24
|
-
// match), so we only treat them as transient when paired with a transport
|
|
25
|
-
// error code on the cause (see isTransientUploadError).
|
|
26
|
-
const TRANSIENT_RESET_MARKERS = [
|
|
27
|
-
"ENHANCE_YOUR_CALM", // H2 flow-control backpressure reset
|
|
28
|
-
"NGHTTP2_INTERNAL_ERROR", // H2 internal stream error (qualified form)
|
|
29
|
-
"ERR_HTTP2", // node http2 error code family
|
|
30
|
-
"GOAWAY", // peer is draining the connection
|
|
31
|
-
"HTTP/2 session closed before response", // thrown by our own h2 transport
|
|
32
|
-
"HTTP/2 session sent GOAWAY before response",
|
|
33
|
-
];
|
|
34
|
-
// Node-level error codes (from `error.code` / `error.cause.code`) that mean
|
|
35
|
-
// the connection itself dropped mid-flight and the request never completed.
|
|
36
|
-
// These are safe to retry for an idempotent part upload.
|
|
37
|
-
const TRANSIENT_ERROR_CODES = new Set([
|
|
38
|
-
"ECONNRESET",
|
|
39
|
-
"ECONNREFUSED",
|
|
40
|
-
"ETIMEDOUT",
|
|
41
|
-
"EPIPE",
|
|
42
|
-
"ERR_HTTP2_STREAM_ERROR",
|
|
43
|
-
"ERR_HTTP2_GOAWAY_SESSION",
|
|
44
|
-
"ERR_HTTP2_SESSION_ERROR",
|
|
45
|
-
]);
|
|
46
|
-
function collectErrorText(error) {
|
|
47
|
-
const messages = [];
|
|
48
|
-
const codes = [];
|
|
49
|
-
// Walk the error -> cause chain (bounded) so a transport error wrapped by a
|
|
50
|
-
// higher-level "fetch failed" is still classified correctly.
|
|
51
|
-
let current = error;
|
|
52
|
-
for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
|
|
53
|
-
const node = current;
|
|
54
|
-
if (typeof node.message === "string")
|
|
55
|
-
messages.push(node.message);
|
|
56
|
-
if (typeof node.code === "string")
|
|
57
|
-
codes.push(node.code);
|
|
58
|
-
current = node.cause;
|
|
59
|
-
}
|
|
60
|
-
return { messages, codes };
|
|
61
|
-
}
|
|
62
|
-
function isTransientUploadError(error) {
|
|
63
|
-
if (!error || typeof error !== "object") {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
const { messages, codes } = collectErrorText(error);
|
|
67
|
-
// 1. An explicit transient transport error code anywhere in the chain.
|
|
68
|
-
if (codes.some((code) => TRANSIENT_ERROR_CODES.has(code))) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
// 2. An unambiguous protocol-level reset marker in any message.
|
|
72
|
-
if (messages.some((text) => TRANSIENT_RESET_MARKERS.some((marker) => text.includes(marker)))) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
function nextRetryDelayMs(attempt) {
|
|
78
|
-
// Linear backoff (200ms, 400ms, ...) plus up to one extra base delay of
|
|
79
|
-
// random jitter so concurrent part retries do not all fire on the same tick.
|
|
80
|
-
const base = RETRY_BASE_DELAY_MS * attempt;
|
|
81
|
-
const jitter = Math.floor(Math.random() * RETRY_BASE_DELAY_MS);
|
|
82
|
-
return base + jitter;
|
|
83
|
-
}
|
|
84
|
-
async function retryOnTransient(fn) {
|
|
85
|
-
const retries = settings_js_1.settings.fsPartRetries; // 0 = off (current default behavior)
|
|
86
|
-
let attempt = 0;
|
|
87
|
-
for (;;) {
|
|
88
|
-
try {
|
|
89
|
-
return await fn();
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
attempt++;
|
|
93
|
-
if (retries <= 0 || attempt > retries || !isTransientUploadError(error)) {
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, nextRetryDelayMs(attempt)));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
15
|
+
// The transient-reset classifier and retry loop live in
|
|
16
|
+
// common/transient-retry.ts, shared with the idempotent sandbox-op retry
|
|
17
|
+
// (read/list/etc.) so every path judges "transient" identically. These aliases
|
|
18
|
+
// preserve the import surface the ENG-2680 fault tests already depend on.
|
|
19
|
+
exports.isTransientUploadError = transient_retry_js_1.isTransientResetError;
|
|
20
|
+
function retryOnTransient(fn) {
|
|
21
|
+
// Upload path keeps its own (lower) fsPartRetries budget; the shared helper
|
|
22
|
+
// otherwise defaults to the higher idempotent-read budget (sandboxReadRetries).
|
|
23
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(fn, { retries: settings_js_1.settings.fsPartRetries });
|
|
99
24
|
}
|
|
100
25
|
class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
101
26
|
process;
|
|
@@ -177,28 +102,44 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
177
102
|
if (fileBlob.size > MULTIPART_THRESHOLD) {
|
|
178
103
|
return await this.uploadWithMultipart(path, fileBlob, "0644");
|
|
179
104
|
}
|
|
180
|
-
// Use regular upload for small files
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Build URL
|
|
105
|
+
// Use regular upload for small files. Run the PUT under the same upload
|
|
106
|
+
// reliability wrapper as multipart parts: bound concurrency (ENG-2680) and
|
|
107
|
+
// retry transient connection resets (ECONNRESET/GOAWAY/ENHANCE_YOUR_CALM). A
|
|
108
|
+
// PUT of the same bytes to the same path is idempotent, so retry is safe.
|
|
109
|
+
// The FormData is rebuilt per attempt so a retried request has a fresh body.
|
|
186
110
|
let url = `${this.url}/filesystem/${path}`;
|
|
187
111
|
if (this.forcedUrl) {
|
|
188
112
|
url = `${this.forcedUrl.toString()}/filesystem/${path}`;
|
|
189
113
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
114
|
+
const h2Domain = this.sandbox?.h2Domain;
|
|
115
|
+
const putOnce = async () => {
|
|
116
|
+
const formData = new FormData();
|
|
117
|
+
formData.append("file", fileBlob, "test-binary.bin");
|
|
118
|
+
formData.append("permissions", "0644");
|
|
119
|
+
formData.append("path", path);
|
|
120
|
+
// A forceUrl (session-token) sandbox carries its own headers and must not
|
|
121
|
+
// require global credentials; settings.headers now throws without them
|
|
122
|
+
// (ENG-2698). Mirror streamLogs/execWithStreaming, which already pick
|
|
123
|
+
// sandbox.headers for forceUrl sessions.
|
|
124
|
+
const headers = this.sandbox.forceUrl ? this.sandbox.headers : settings_js_1.settings.headers;
|
|
125
|
+
const response = await this.h2Fetch(url, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
headers: {
|
|
128
|
+
...headers,
|
|
129
|
+
},
|
|
130
|
+
body: formData,
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const errorText = await response.text();
|
|
134
|
+
throw new Error(`Failed to write binary: ${response.status} ${errorText}`);
|
|
135
|
+
}
|
|
136
|
+
return await response.json();
|
|
137
|
+
};
|
|
138
|
+
// Acquire the upload slot per-attempt INSIDE the retry so the per-domain cap
|
|
139
|
+
// bounds in-flight streams, not retry sequences: the slot is released during
|
|
140
|
+
// backoff, so a failing PUT never pins a slot while it sleeps (Mendral review).
|
|
141
|
+
const putWithSlot = h2Domain ? () => (0, h2fetch_js_1.withUploadSlot)(h2Domain, putOnce) : putOnce;
|
|
142
|
+
return retryOnTransient(putWithSlot);
|
|
202
143
|
}
|
|
203
144
|
async writeTree(files, destinationPath = null) {
|
|
204
145
|
const options = {
|
|
@@ -223,30 +164,37 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
223
164
|
}
|
|
224
165
|
async read(path) {
|
|
225
166
|
path = this.formatPath(path);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
167
|
+
// Idempotent GET: self-heal a transient connection reset (e.g. first call
|
|
168
|
+
// after sandbox resume). Non-transient errors (404, etc.) are not retried.
|
|
169
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
170
|
+
const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
|
|
171
|
+
path: { path },
|
|
172
|
+
baseUrl: this.url,
|
|
173
|
+
}));
|
|
174
|
+
this.handleResponseError(response, data, error);
|
|
175
|
+
if (data && 'content' in data) {
|
|
176
|
+
return data.content;
|
|
177
|
+
}
|
|
178
|
+
throw new Error("Unsupported file type");
|
|
179
|
+
});
|
|
235
180
|
}
|
|
236
181
|
async readBinary(path) {
|
|
237
182
|
path = this.formatPath(path);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
183
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
184
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
185
|
+
const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
|
|
186
|
+
path: { path },
|
|
187
|
+
baseUrl: this.url,
|
|
188
|
+
headers: {
|
|
189
|
+
'Accept': 'application/octet-stream',
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
this.handleResponseError(response, data, error);
|
|
193
|
+
if (typeof data === 'string') {
|
|
194
|
+
return new Blob([data]);
|
|
195
|
+
}
|
|
196
|
+
return data;
|
|
197
|
+
});
|
|
250
198
|
}
|
|
251
199
|
async download(src, destinationPath, { mode = 0o644 } = {}) {
|
|
252
200
|
if (!node_js_1.fs) {
|
|
@@ -269,15 +217,19 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
269
217
|
}
|
|
270
218
|
async ls(path) {
|
|
271
219
|
path = this.formatPath(path);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
220
|
+
// Idempotent GET: self-heal a transient connection reset (the file-tree
|
|
221
|
+
// refresh that customers hit as intermittent 500s after sandbox resume).
|
|
222
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
223
|
+
const { response, data, error } = await (0, index_js_1.getFilesystemByPath)(this.withClient({
|
|
224
|
+
path: { path },
|
|
225
|
+
baseUrl: this.url,
|
|
226
|
+
}));
|
|
227
|
+
this.handleResponseError(response, data, error);
|
|
228
|
+
if (!data || !('files' in data || 'subdirectories' in data)) {
|
|
229
|
+
throw new Error(JSON.stringify({ error: "Directory not found" }));
|
|
230
|
+
}
|
|
231
|
+
return data;
|
|
232
|
+
});
|
|
281
233
|
}
|
|
282
234
|
async search(query, path = "/", options) {
|
|
283
235
|
const formattedPath = this.formatPath(path);
|
|
@@ -294,13 +246,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
294
246
|
if (options?.excludeHidden !== undefined) {
|
|
295
247
|
queryParams.excludeHidden = options.excludeHidden;
|
|
296
248
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
249
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
250
|
+
const result = await (0, index_js_1.getFilesystemSearchByPath)(this.withClient({
|
|
251
|
+
path: { path: formattedPath },
|
|
252
|
+
query: queryParams,
|
|
253
|
+
baseUrl: this.url,
|
|
254
|
+
}));
|
|
255
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
256
|
+
return result.data;
|
|
257
|
+
});
|
|
304
258
|
}
|
|
305
259
|
async find(path, options) {
|
|
306
260
|
const formattedPath = this.formatPath(path);
|
|
@@ -320,13 +274,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
320
274
|
if (options?.excludeHidden !== undefined) {
|
|
321
275
|
queryParams.excludeHidden = options.excludeHidden;
|
|
322
276
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
277
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
278
|
+
const result = await (0, index_js_1.getFilesystemFindByPath)(this.withClient({
|
|
279
|
+
path: { path: formattedPath },
|
|
280
|
+
query: queryParams,
|
|
281
|
+
baseUrl: this.url,
|
|
282
|
+
}));
|
|
283
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
284
|
+
return result.data;
|
|
285
|
+
});
|
|
330
286
|
}
|
|
331
287
|
async grep(query, path = "/", options) {
|
|
332
288
|
const formattedPath = this.formatPath(path);
|
|
@@ -348,13 +304,15 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
348
304
|
if (options?.excludeDirs && options.excludeDirs.length > 0) {
|
|
349
305
|
queryParams.excludeDirs = options.excludeDirs.join(',');
|
|
350
306
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
307
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
308
|
+
const result = await (0, index_js_1.getFilesystemContentSearchByPath)(this.withClient({
|
|
309
|
+
path: { path: formattedPath },
|
|
310
|
+
query: queryParams,
|
|
311
|
+
baseUrl: this.url,
|
|
312
|
+
}));
|
|
313
|
+
this.handleResponseError(result.response, result.data, result.error);
|
|
314
|
+
return result.data;
|
|
315
|
+
});
|
|
358
316
|
}
|
|
359
317
|
async cp(source, destination, { maxWait = 180000 } = {}) {
|
|
360
318
|
let process = await this.process.exec({
|
|
@@ -506,6 +464,12 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
506
464
|
const size = blob.size;
|
|
507
465
|
const numParts = Math.ceil(size / CHUNK_SIZE);
|
|
508
466
|
const parts = [];
|
|
467
|
+
// Bound concurrent upload-part streams on the shared H2 connection so many
|
|
468
|
+
// parts (within and across files) cannot burst past the server's
|
|
469
|
+
// rapid-reset limit (ENG-2680). Scoped to uploads; default cap is 2. With
|
|
470
|
+
// no h2Domain the parts go over globalThis.fetch on separate connections,
|
|
471
|
+
// so the shared-connection cap does not apply.
|
|
472
|
+
const h2Domain = this.sandbox?.h2Domain;
|
|
509
473
|
// Upload parts in batches for parallel processing
|
|
510
474
|
for (let i = 0; i < numParts; i += MAX_PARALLEL_UPLOADS) {
|
|
511
475
|
const batch = [];
|
|
@@ -514,7 +478,11 @@ class SandboxFileSystem extends action_js_1.SandboxAction {
|
|
|
514
478
|
const start = (partNumber - 1) * CHUNK_SIZE;
|
|
515
479
|
const end = Math.min(start + CHUNK_SIZE, size);
|
|
516
480
|
const chunk = blob.slice(start, end);
|
|
517
|
-
|
|
481
|
+
// Slot acquired per-attempt inside the retry (see writeBinary): bounds
|
|
482
|
+
// concurrent part streams, not retry sequences; freed during backoff.
|
|
483
|
+
const doPart = () => this.uploadPart(uploadId, partNumber, chunk);
|
|
484
|
+
const partWithSlot = h2Domain ? () => (0, h2fetch_js_1.withUploadSlot)(h2Domain, doPart) : doPart;
|
|
485
|
+
batch.push(retryOnTransient(partWithSlot));
|
|
518
486
|
}
|
|
519
487
|
// Wait for batch to complete
|
|
520
488
|
const batchResults = await Promise.all(batch);
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SandboxProcess = void 0;
|
|
4
4
|
const settings_js_1 = require("../../common/settings.js");
|
|
5
5
|
const action_js_1 = require("../action.js");
|
|
6
|
+
const transient_retry_js_1 = require("../../common/transient-retry.js");
|
|
6
7
|
const index_js_1 = require("../client/index.js");
|
|
7
8
|
class SandboxProcess extends action_js_1.SandboxAction {
|
|
8
9
|
constructor(sandbox) {
|
|
@@ -302,19 +303,27 @@ class SandboxProcess extends action_js_1.SandboxAction {
|
|
|
302
303
|
return data;
|
|
303
304
|
}
|
|
304
305
|
async get(identifier) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
// Idempotent GET: self-heal a transient connection reset (also makes the
|
|
307
|
+
// wait() poll loop resilient). exec() stays un-retried — it is a
|
|
308
|
+
// non-idempotent POST and retrying it duplicates the process (ENG-2340).
|
|
309
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
310
|
+
const { response, data, error } = await (0, index_js_1.getProcessByIdentifier)(this.withClient({
|
|
311
|
+
path: { identifier },
|
|
312
|
+
baseUrl: this.url,
|
|
313
|
+
}));
|
|
314
|
+
this.handleResponseError(response, data, error);
|
|
315
|
+
return data;
|
|
316
|
+
});
|
|
311
317
|
}
|
|
312
318
|
async list() {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
320
|
+
return (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
321
|
+
const { response, data, error } = await (0, index_js_1.getProcess)(this.withClient({
|
|
322
|
+
baseUrl: this.url,
|
|
323
|
+
}));
|
|
324
|
+
this.handleResponseError(response, data, error);
|
|
325
|
+
return data;
|
|
326
|
+
});
|
|
318
327
|
}
|
|
319
328
|
async stop(identifier) {
|
|
320
329
|
const { response, data, error } = await (0, index_js_1.deleteProcessByIdentifier)(this.withClient({
|
|
@@ -333,11 +342,15 @@ class SandboxProcess extends action_js_1.SandboxAction {
|
|
|
333
342
|
return data;
|
|
334
343
|
}
|
|
335
344
|
async logs(identifier, type = "all") {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
// Idempotent GET: self-heal a transient connection reset.
|
|
346
|
+
const data = await (0, transient_retry_js_1.retryOnTransientReset)(async () => {
|
|
347
|
+
const { response, data, error } = await (0, index_js_1.getProcessByIdentifierLogs)(this.withClient({
|
|
348
|
+
path: { identifier },
|
|
349
|
+
baseUrl: this.url,
|
|
350
|
+
}));
|
|
351
|
+
this.handleResponseError(response, data, error);
|
|
352
|
+
return data;
|
|
353
|
+
});
|
|
341
354
|
if (type === "all") {
|
|
342
355
|
return data?.logs || "";
|
|
343
356
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import http2 from "http2";
|
|
2
2
|
import type { H2Pool } from "./h2pool.js";
|
|
3
|
+
/**
|
|
4
|
+
* Run `fn` while holding an upload slot for `domain`, releasing it when `fn`
|
|
5
|
+
* settles (the part PUT completes or fails). Bounds concurrent in-flight upload
|
|
6
|
+
* parts per domain across all files sharing the connection. Used by the
|
|
7
|
+
* multipart upload path; a no-op wrapper when the upload cap is unset.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withUploadSlot<T>(domain: string, fn: () => Promise<T>): Promise<T>;
|
|
3
10
|
/**
|
|
4
11
|
* Creates a fetch()-compatible function that sends requests over an existing
|
|
5
12
|
* HTTP/2 session. Falls back to globalThis.fetch() only when the session is
|
|
@@ -18,10 +18,28 @@ export type Config = {
|
|
|
18
18
|
*/
|
|
19
19
|
maxConcurrentH2Requests?: number;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Maximum number of concurrent in-flight multipart upload-part requests per
|
|
22
|
+
* edge domain on the shared H2 connection. Defaults to 2 (the measured value
|
|
23
|
+
* that stops concurrent large uploads tripping ENHANCE_YOUR_CALM). Scoped to
|
|
24
|
+
* the upload-part path only; non-upload traffic is unaffected. `0` disables it.
|
|
25
|
+
*/
|
|
26
|
+
maxConcurrentUploadH2Requests?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Retry attempts for transient connection resets (ECONNRESET, GOAWAY,
|
|
29
|
+
* ENHANCE_YOUR_CALM, etc.) on file uploads. Covers both small single-PUT
|
|
30
|
+
* uploads and multipart parts; both are idempotent writes and safe to retry.
|
|
31
|
+
* Defaults to 3. Set `0` to disable.
|
|
23
32
|
*/
|
|
24
33
|
fsPartRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Retry attempts for transient connection resets on IDEMPOTENT sandbox reads
|
|
36
|
+
* (fs.read/readBinary/ls/search/find/grep, drives.list, process.get/list/logs).
|
|
37
|
+
* Higher than the upload default so a later attempt can span a multi-second
|
|
38
|
+
* sandbox cold-start/standby wake (the window a first-call read reset falls in).
|
|
39
|
+
* Defaults to 5. Set `0` to disable. Never applied to non-idempotent ops
|
|
40
|
+
* (process.exec, drives.mount, etc.).
|
|
41
|
+
*/
|
|
42
|
+
sandboxReadRetries?: number;
|
|
25
43
|
/**
|
|
26
44
|
* Client credentials for OAuth2 client_credentials flow.
|
|
27
45
|
*
|
|
@@ -62,7 +80,9 @@ declare class Settings {
|
|
|
62
80
|
get region(): string | undefined;
|
|
63
81
|
get disableH2(): boolean;
|
|
64
82
|
get maxConcurrentH2Requests(): number;
|
|
83
|
+
get maxConcurrentUploadH2Requests(): number;
|
|
65
84
|
get fsPartRetries(): number;
|
|
85
|
+
get sandboxReadRetries(): number;
|
|
66
86
|
/**
|
|
67
87
|
* Fail fast with a clear, actionable error when credentials are missing or
|
|
68
88
|
* incomplete, instead of sending empty workspace/authorization headers and
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True only for transport-level resets/drops that are safe to retry on an
|
|
3
|
+
* IDEMPOTENT request (transient HTTP/2 stream reset, GOAWAY, or a dropped
|
|
4
|
+
* connection). Application errors (4xx/5xx — even when their body text contains
|
|
5
|
+
* a marker word) and a bare "fetch failed" with no transport code are
|
|
6
|
+
* deliberately NOT transient, so auto-retry never masks a real server error or
|
|
7
|
+
* duplicates a non-idempotent call.
|
|
8
|
+
*
|
|
9
|
+
* This is the single classifier shared by the upload-part retry (ENG-2680) and
|
|
10
|
+
* the idempotent sandbox-op retry (read/list/etc.), so both judge "transient"
|
|
11
|
+
* identically.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isTransientResetError(error: unknown): boolean;
|
|
14
|
+
export type RetryOptions = {
|
|
15
|
+
retries?: number;
|
|
16
|
+
baseDelayMs?: number;
|
|
17
|
+
maxDelayMs?: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Run `fn`, retrying only on transient transport resets (see
|
|
21
|
+
* isTransientResetError) with exponential backoff. Caller owns idempotency:
|
|
22
|
+
* ONLY wrap idempotent operations (GET-shaped reads/lists, or an idempotent
|
|
23
|
+
* PUT of the same bytes) — never a non-idempotent POST such as process.exec,
|
|
24
|
+
* which would duplicate the side effect (ENG-2340).
|
|
25
|
+
*
|
|
26
|
+
* Defaults to `settings.sandboxReadRetries` (the higher idempotent-read budget,
|
|
27
|
+
* sized for a multi-second standby wake). The upload path passes
|
|
28
|
+
* `{ retries: settings.fsPartRetries }` to keep its own (lower) budget.
|
|
29
|
+
*/
|
|
30
|
+
export declare function retryOnTransientReset<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Sandbox } from "../../client/types.gen.js";
|
|
2
|
+
import { isTransientResetError } from "../../common/transient-retry.js";
|
|
2
3
|
import { SandboxAction } from "../action.js";
|
|
3
4
|
import { ContentSearchResponse, Directory, FindResponse, FuzzySearchResponse, SuccessResponse } from "../client/index.js";
|
|
4
5
|
import { SandboxProcess } from "../process/index.js";
|
|
5
6
|
import { CopyResponse, FilesystemFindOptions, FilesystemGrepOptions, FilesystemSearchOptions, SandboxFilesystemFile, WatchEvent } from "./types.js";
|
|
7
|
+
export declare const isTransientUploadError: typeof isTransientResetError;
|
|
8
|
+
export declare function retryOnTransient<T>(fn: () => Promise<T>): Promise<T>;
|
|
6
9
|
export declare class SandboxFileSystem extends SandboxAction {
|
|
7
10
|
private process;
|
|
8
11
|
constructor(sandbox: Sandbox, process: SandboxProcess);
|