@blaxel/core 0.3.1-preview.210 → 0.3.1-preview.212
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/settings.js +2 -2
- package/dist/cjs/common/transient-retry.js +3 -1
- package/dist/cjs/sandbox/sandbox.js +72 -6
- package/dist/cjs/types/common/transient-retry.d.ts +1 -0
- package/dist/cjs/types/sandbox/sandbox.d.ts +1 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/cjs-browser/common/transient-retry.js +3 -1
- package/dist/cjs-browser/sandbox/sandbox.js +72 -6
- package/dist/cjs-browser/types/common/transient-retry.d.ts +1 -0
- package/dist/cjs-browser/types/sandbox/sandbox.d.ts +1 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm/common/transient-retry.js +3 -2
- package/dist/esm/sandbox/sandbox.js +72 -6
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/settings.js +2 -2
- package/dist/esm-browser/common/transient-retry.js +3 -2
- package/dist/esm-browser/sandbox/sandbox.js +72 -6
- package/package.json +1 -1
|
@@ -28,8 +28,8 @@ function missingCredentialsMessage() {
|
|
|
28
28
|
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
29
29
|
}
|
|
30
30
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
31
|
-
const BUILD_VERSION = "0.3.1-preview.
|
|
32
|
-
const BUILD_COMMIT = "
|
|
31
|
+
const BUILD_VERSION = "0.3.1-preview.212";
|
|
32
|
+
const BUILD_COMMIT = "06b4bb451ef8797cbc2cd3e3a957d62c45f2cc12";
|
|
33
33
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
34
34
|
const BLAXEL_API_VERSION = "2026-04-28";
|
|
35
35
|
// Cache for config.yaml tracking value
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isTransientResetError = isTransientResetError;
|
|
4
|
+
exports.backoffDelayMs = backoffDelayMs;
|
|
4
5
|
exports.retryOnTransientReset = retryOnTransientReset;
|
|
5
6
|
const settings_js_1 = require("./settings.js");
|
|
6
7
|
// Markers that, when present anywhere in the error chain, are unambiguous
|
|
@@ -107,7 +108,8 @@ const DEFAULT_MAX_DELAY_MS = 2000;
|
|
|
107
108
|
// single wait never blocks unreasonably long. Exponential (rather than linear)
|
|
108
109
|
// gives a later attempt room to span a multi-second sandbox cold-start/standby
|
|
109
110
|
// wake, which is the window a first-call reset falls into, while early attempts
|
|
110
|
-
// stay fast for the common quick-reset case.
|
|
111
|
+
// stay fast for the common quick-reset case. Exported so other polling paths
|
|
112
|
+
// (e.g. the create-504 wait in sandbox.ts) share the same delay curve.
|
|
111
113
|
function backoffDelayMs(attempt, baseDelayMs, maxDelayMs) {
|
|
112
114
|
const exponential = baseDelayMs * Math.pow(2, attempt - 1);
|
|
113
115
|
const capped = Math.min(exponential, maxDelayMs);
|
|
@@ -37,6 +37,7 @@ exports.SandboxInstance = void 0;
|
|
|
37
37
|
const uuid_1 = require("uuid");
|
|
38
38
|
const index_js_1 = require("../client/index.js");
|
|
39
39
|
const logger_js_1 = require("../common/logger.js");
|
|
40
|
+
const transient_retry_js_1 = require("../common/transient-retry.js");
|
|
40
41
|
const pagination_js_1 = require("../common/pagination.js");
|
|
41
42
|
const settings_js_1 = require("../common/settings.js");
|
|
42
43
|
const index_js_2 = require("./codegen/index.js");
|
|
@@ -67,6 +68,15 @@ const TRANSIENT_SANDBOX_STATUSES = new Set([
|
|
|
67
68
|
]);
|
|
68
69
|
const TRANSIENT_STATUS_MAX_WAIT_MS = 30_000;
|
|
69
70
|
const TRANSIENT_STATUS_POLL_MS = 500;
|
|
71
|
+
// A create that outlives the edge's 60s origin-read timeout gets a 504 from
|
|
72
|
+
// CloudFront while the control plane keeps deploying the sandbox for up to
|
|
73
|
+
// 300s (ENG-3662 timeout ladder inversion). The 504 body is edge HTML with no
|
|
74
|
+
// usable payload, so the record is polled instead until it settles.
|
|
75
|
+
// Polling backs off exponentially (1s, 2s, 4s, then 5s + jitter) so a fleet of
|
|
76
|
+
// clients stuck in this window does not hammer the control plane every second.
|
|
77
|
+
const CREATE_GATEWAY_TIMEOUT_MAX_WAIT_MS = 120_000;
|
|
78
|
+
const CREATE_GATEWAY_TIMEOUT_BASE_POLL_MS = 1_000;
|
|
79
|
+
const CREATE_GATEWAY_TIMEOUT_MAX_POLL_MS = 5_000;
|
|
70
80
|
const isSandboxNotFound = (e) => {
|
|
71
81
|
if (typeof e !== "object" || e === null)
|
|
72
82
|
return false;
|
|
@@ -241,14 +251,25 @@ class SandboxInstance {
|
|
|
241
251
|
if (edgeDomain && !settings_js_1.settings.disableH2) {
|
|
242
252
|
Promise.resolve().then(() => __importStar(require("../common/h2pool.js"))).then(({ h2Pool }) => h2Pool.warm(edgeDomain)).catch(() => { });
|
|
243
253
|
}
|
|
244
|
-
const [
|
|
254
|
+
const [createResult, h2Session] = await Promise.all([
|
|
245
255
|
(0, index_js_1.createSandbox)({
|
|
246
256
|
body: sandbox,
|
|
247
257
|
query: createIfNotExist ? { createIfNotExist } : undefined,
|
|
248
|
-
throwOnError: true,
|
|
249
258
|
}),
|
|
250
259
|
edgeDomain && !settings_js_1.settings.disableH2 ? Promise.resolve().then(() => __importStar(require("../common/h2pool.js"))).then(({ h2Pool }) => h2Pool.get(edgeDomain)).catch(() => null) : Promise.resolve(null),
|
|
251
260
|
]);
|
|
261
|
+
let data = createResult.data;
|
|
262
|
+
if (createResult.error !== undefined) {
|
|
263
|
+
const name = sandbox.metadata.name;
|
|
264
|
+
if (createResult.response.status === 504 && name) {
|
|
265
|
+
// The edge gave up on the connection but the creation is still running
|
|
266
|
+
// server-side; wait for the record instead of failing (ENG-3662).
|
|
267
|
+
data = await SandboxInstance.waitAfterCreateGatewayTimeout(name, createResult.error);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
throw createResult.error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
252
273
|
// Inject the H2 session into the config so subsystems can use it
|
|
253
274
|
const config = { ...data, h2Session, h2Domain: settings_js_1.settings.disableH2 ? null : edgeDomain };
|
|
254
275
|
const instance = new SandboxInstance(config);
|
|
@@ -388,6 +409,12 @@ class SandboxInstance {
|
|
|
388
409
|
static async createIfNotExists(sandbox) {
|
|
389
410
|
const ATTEMPTS = 3;
|
|
390
411
|
let lastStatus = "unknown";
|
|
412
|
+
// The 'vanished' window (create 409s while get 404s) is driven by the same
|
|
413
|
+
// transient causes as 'dying': an in-flight delete finishing, or a
|
|
414
|
+
// concurrent create whose row is not readable yet. Give it the same time
|
|
415
|
+
// budget as waitWhileSandboxDying instead of burning the attempt budget
|
|
416
|
+
// 500ms apart (~1s total), which threw on deletes taking >1s (ENG-3667).
|
|
417
|
+
const vanishedDeadline = Date.now() + TRANSIENT_STATUS_MAX_WAIT_MS;
|
|
391
418
|
for (let i = 0; i < ATTEMPTS; ++i) {
|
|
392
419
|
const finalAttempt = i === ATTEMPTS - 1;
|
|
393
420
|
try {
|
|
@@ -399,6 +426,12 @@ class SandboxInstance {
|
|
|
399
426
|
if (!name) {
|
|
400
427
|
throw new Error("Sandbox name is required");
|
|
401
428
|
}
|
|
429
|
+
// The controlplane tags the creation-lock 409 with
|
|
430
|
+
// reason=CREATION_IN_PROGRESS when the conflict comes from a
|
|
431
|
+
// concurrent create still in flight (ENG-3776). The field is absent
|
|
432
|
+
// on older controlplanes and on real row conflicts, so it only
|
|
433
|
+
// sharpens the give-up error; the polling behavior is the same.
|
|
434
|
+
const creationInProgress = e.reason === "CREATION_IN_PROGRESS";
|
|
402
435
|
// Get the existing sandbox to check its status
|
|
403
436
|
let sandboxInstance;
|
|
404
437
|
try {
|
|
@@ -406,10 +439,14 @@ class SandboxInstance {
|
|
|
406
439
|
}
|
|
407
440
|
catch (getError) {
|
|
408
441
|
if (isSandboxNotFound(getError)) {
|
|
409
|
-
// The record vanished between the create conflict and this status check
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
442
|
+
// The record vanished between the create conflict and this status check.
|
|
443
|
+
lastStatus = creationInProgress ? "creation in progress" : "vanished";
|
|
444
|
+
if (Date.now() < vanishedDeadline) {
|
|
445
|
+
// Inside the transient window: poll without consuming attempts.
|
|
446
|
+
await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
|
|
447
|
+
--i;
|
|
448
|
+
}
|
|
449
|
+
else if (!finalAttempt) {
|
|
413
450
|
await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
|
|
414
451
|
}
|
|
415
452
|
continue;
|
|
@@ -435,6 +472,35 @@ class SandboxInstance {
|
|
|
435
472
|
}
|
|
436
473
|
throw new Error(`Unable to create sandbox after ${ATTEMPTS} attempts. Last conflicting status: ${lastStatus}.`);
|
|
437
474
|
}
|
|
475
|
+
// Poll the record after a create was cut by the edge with a 504 while the
|
|
476
|
+
// control plane is still deploying it. Resolves with the sandbox once it
|
|
477
|
+
// reaches DEPLOYED; throws on FAILED or once the wait budget is spent.
|
|
478
|
+
static async waitAfterCreateGatewayTimeout(name, createError) {
|
|
479
|
+
logger_js_1.logger.debug(`Sandbox ${name} creation timed out at the edge (504); polling the record while it finishes deploying`);
|
|
480
|
+
const deadline = Date.now() + CREATE_GATEWAY_TIMEOUT_MAX_WAIT_MS;
|
|
481
|
+
let attempt = 0;
|
|
482
|
+
while (Date.now() < deadline) {
|
|
483
|
+
attempt++;
|
|
484
|
+
await new Promise((resolve) => setTimeout(resolve, (0, transient_retry_js_1.backoffDelayMs)(attempt, CREATE_GATEWAY_TIMEOUT_BASE_POLL_MS, CREATE_GATEWAY_TIMEOUT_MAX_POLL_MS)));
|
|
485
|
+
let current;
|
|
486
|
+
try {
|
|
487
|
+
const { data } = await (0, index_js_1.getSandbox)({ path: { sandboxName: name }, throwOnError: true });
|
|
488
|
+
current = data;
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
// The record can lag behind the accepted create; keep waiting on 404.
|
|
492
|
+
if (isSandboxNotFound(e))
|
|
493
|
+
continue;
|
|
494
|
+
throw e;
|
|
495
|
+
}
|
|
496
|
+
if (current.status === "DEPLOYED")
|
|
497
|
+
return current;
|
|
498
|
+
if (current.status === "FAILED") {
|
|
499
|
+
throw new Error(`Sandbox ${name} failed to deploy after the create timed out at the edge (504).`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw createError;
|
|
503
|
+
}
|
|
438
504
|
// Poll the record until an in-flight delete/deactivation settles (or the record
|
|
439
505
|
// disappears), bounded by TRANSIENT_STATUS_MAX_WAIT_MS. Errors from get (e.g. 404
|
|
440
506
|
// once the record is gone) end the wait: the caller's create retry decides next.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* identically.
|
|
12
12
|
*/
|
|
13
13
|
export declare function isTransientResetError(error: unknown): boolean;
|
|
14
|
+
export declare function backoffDelayMs(attempt: number, baseDelayMs: number, maxDelayMs: number): number;
|
|
14
15
|
export type RetryOptions = {
|
|
15
16
|
retries?: number;
|
|
16
17
|
baseDelayMs?: number;
|
|
@@ -97,6 +97,7 @@ export declare class SandboxInstance {
|
|
|
97
97
|
static updateLifecycle(sandboxName: string, lifecycle: SandboxLifecycle | null): Promise<SandboxInstance>;
|
|
98
98
|
static updateNetwork(sandboxName: string, network: SandboxUpdateNetwork): Promise<SandboxInstance>;
|
|
99
99
|
static createIfNotExists(sandbox: SandboxModel | SandboxCreateConfiguration): Promise<SandboxInstance>;
|
|
100
|
+
private static waitAfterCreateGatewayTimeout;
|
|
100
101
|
private static waitWhileSandboxDying;
|
|
101
102
|
static fromSession(session: SessionWithToken): Promise<SandboxInstance>;
|
|
102
103
|
}
|