@molecule/api-code-sandbox-flyio 1.2.8 → 1.2.9
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/README.md +1120 -1029
- package/dist/api.d.ts +6 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -0
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -6
- package/dist/provider.d.ts +17 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +177 -12
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -125,7 +125,12 @@ export declare class FlyApiClient {
|
|
|
125
125
|
private readonly baseUrl;
|
|
126
126
|
private readonly timeoutMs;
|
|
127
127
|
private readonly fetchImpl;
|
|
128
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Injectable delay, shared with the provider: a retry that a CALLER drives
|
|
130
|
+
* (the provider's exec-precondition backoff) waits on the same injected clock
|
|
131
|
+
* the client's own retries use, so tests collapse every wait in one place.
|
|
132
|
+
*/
|
|
133
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
129
134
|
/**
|
|
130
135
|
* Creates a Fly Machines API client.
|
|
131
136
|
* @param options - Token resolver, base URL, and injectable fetch/sleep.
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAgDD,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;gBAED,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAgBhC;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQhF;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAoBjE;AAED,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,KAAK,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC/B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAgDD,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAE7C;;;OAGG;gBACS,OAAO,EAAE,mBAAmB;IASxC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAiFnF"}
|
package/dist/api.js
CHANGED
|
@@ -176,6 +176,11 @@ export class FlyApiClient {
|
|
|
176
176
|
baseUrl;
|
|
177
177
|
timeoutMs;
|
|
178
178
|
fetchImpl;
|
|
179
|
+
/**
|
|
180
|
+
* Injectable delay, shared with the provider: a retry that a CALLER drives
|
|
181
|
+
* (the provider's exec-precondition backoff) waits on the same injected clock
|
|
182
|
+
* the client's own retries use, so tests collapse every wait in one place.
|
|
183
|
+
*/
|
|
179
184
|
sleep;
|
|
180
185
|
/**
|
|
181
186
|
* Creates a Fly Machines API client.
|
package/dist/index.d.ts
CHANGED
|
@@ -315,12 +315,26 @@
|
|
|
315
315
|
* short-term burst limit up to 3 req/s, per action", scoped "per-action,
|
|
316
316
|
* per-machine … That might be Machine ID or App ID, depending on the type of
|
|
317
317
|
* request", with Get Machine at 5 req/s and app deletions capped at 100 per
|
|
318
|
-
* minute. `429` and `5xx` are retried with `Retry-After`-aware backoff
|
|
319
|
-
* `
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
318
|
+
* minute. `429` and `5xx` are retried with `Retry-After`-aware backoff, plus the
|
|
319
|
+
* `404` an app-propagation lag answers the Machine create with; every other
|
|
320
|
+
* `4xx` is a real answer and is never retried. The consequence to plan for is
|
|
321
|
+
* that EVERY file operation is an exec API call, so one sandbox's reads and
|
|
322
|
+
* writes serialize at roughly one per second and a multi-megabyte `writeFile`
|
|
323
|
+
* (chunked at ~45 KB decoded) takes tens of seconds. An agent that edits files
|
|
324
|
+
* in a tight loop will feel this; batch where you can.
|
|
325
|
+
*
|
|
326
|
+
* **A refused exec that dropped the connection is retried; a refused exec that
|
|
327
|
+
* names a Machine state is not.** Fly answers `412 failed_precondition: exec
|
|
328
|
+
* request failed: EOF` when the request reached a `started` Machine's exec agent
|
|
329
|
+
* and the connection died before the command ran — a Machine seconds past boot,
|
|
330
|
+
* or Fly's own exec burst. `exec` retries that one at the transport (up to four
|
|
331
|
+
* attempts, ~11 s of backoff, each preceded by a Machine-state read and a wait
|
|
332
|
+
* for `started` when it has not got there), because every caller's alternative
|
|
333
|
+
* is to throw away a whole boot or deploy and start over. The OTHER `412`,
|
|
334
|
+
* `failed_precondition: machine not running`, is a real state and is not
|
|
335
|
+
* repeated: `start()`/`wake()` wait it out before the first exec. A 412 that is
|
|
336
|
+
* not retried comes back with the Machine's current state appended to its
|
|
337
|
+
* message, and a 412 the retries could not clear carries the state it ended in.
|
|
324
338
|
*
|
|
325
339
|
* **`create()` launches the Machine, and `start()`/`wake()` WAIT for it.** Unlike
|
|
326
340
|
* Docker's create-then-start, the Machines API starts a Machine as part of
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmWG;AAEH,cAAc,UAAU,CAAA;AACxB,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,UAAU,CAAA;AACxB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAE9B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,eAUrB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -315,12 +315,26 @@
|
|
|
315
315
|
* short-term burst limit up to 3 req/s, per action", scoped "per-action,
|
|
316
316
|
* per-machine … That might be Machine ID or App ID, depending on the type of
|
|
317
317
|
* request", with Get Machine at 5 req/s and app deletions capped at 100 per
|
|
318
|
-
* minute. `429` and `5xx` are retried with `Retry-After`-aware backoff
|
|
319
|
-
* `
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
318
|
+
* minute. `429` and `5xx` are retried with `Retry-After`-aware backoff, plus the
|
|
319
|
+
* `404` an app-propagation lag answers the Machine create with; every other
|
|
320
|
+
* `4xx` is a real answer and is never retried. The consequence to plan for is
|
|
321
|
+
* that EVERY file operation is an exec API call, so one sandbox's reads and
|
|
322
|
+
* writes serialize at roughly one per second and a multi-megabyte `writeFile`
|
|
323
|
+
* (chunked at ~45 KB decoded) takes tens of seconds. An agent that edits files
|
|
324
|
+
* in a tight loop will feel this; batch where you can.
|
|
325
|
+
*
|
|
326
|
+
* **A refused exec that dropped the connection is retried; a refused exec that
|
|
327
|
+
* names a Machine state is not.** Fly answers `412 failed_precondition: exec
|
|
328
|
+
* request failed: EOF` when the request reached a `started` Machine's exec agent
|
|
329
|
+
* and the connection died before the command ran — a Machine seconds past boot,
|
|
330
|
+
* or Fly's own exec burst. `exec` retries that one at the transport (up to four
|
|
331
|
+
* attempts, ~11 s of backoff, each preceded by a Machine-state read and a wait
|
|
332
|
+
* for `started` when it has not got there), because every caller's alternative
|
|
333
|
+
* is to throw away a whole boot or deploy and start over. The OTHER `412`,
|
|
334
|
+
* `failed_precondition: machine not running`, is a real state and is not
|
|
335
|
+
* repeated: `start()`/`wake()` wait it out before the first exec. A 412 that is
|
|
336
|
+
* not retried comes back with the Machine's current state appended to its
|
|
337
|
+
* message, and a 412 the retries could not clear carries the state it ended in.
|
|
324
338
|
*
|
|
325
339
|
* **`create()` launches the Machine, and `start()`/`wake()` WAIT for it.** Unlike
|
|
326
340
|
* Docker's create-then-start, the Machines API starts a Machine as part of
|
package/dist/provider.d.ts
CHANGED
|
@@ -10,6 +10,23 @@ import type { SandboxProvider } from '@molecule/api-code-sandbox';
|
|
|
10
10
|
import { FlyApiClient } from './api.js';
|
|
11
11
|
import { type ObjectStore } from './storage.js';
|
|
12
12
|
import type { FlyioConfig } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Whether a failed exec is Fly's TRANSIENT precondition — the request reached
|
|
15
|
+
* the Machine's exec agent and the connection was dropped before the command
|
|
16
|
+
* ran (`412 failed_precondition: exec request failed: EOF`) — rather than the
|
|
17
|
+
* Machine being in a state an exec cannot run in.
|
|
18
|
+
*
|
|
19
|
+
* The distinction decides what retrying can achieve. A dropped connection is
|
|
20
|
+
* worth repeating: the Machine is `started` and its agent recovers in seconds.
|
|
21
|
+
* `failed_precondition: machine not running` is NOT — the Machine really is not
|
|
22
|
+
* running, and that is what {@link FlyioSandboxProvider.startMachine} waits out
|
|
23
|
+
* before the first exec. Repeating the exec there would only burn the caller's
|
|
24
|
+
* budget against a condition the wait endpoint already answers.
|
|
25
|
+
*
|
|
26
|
+
* @param error - The failure an exec attempt raised.
|
|
27
|
+
* @returns `true` when repeating the exec is the right recovery.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isTransientExecPrecondition(error: unknown): boolean;
|
|
13
30
|
/**
|
|
14
31
|
* Creates a Fly.io Machines sandbox provider.
|
|
15
32
|
* @param config - Optional Fly configuration: API token/URL, org slug, app
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAWV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAwBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAWV,eAAe,EAGhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAwBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAIV,WAAW,EAUZ,MAAM,YAAY,CAAA;AAkBnB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGnE;AA8oFD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,YAAY,EACrB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GACzB,eAAe,CAEjB"}
|
package/dist/provider.js
CHANGED
|
@@ -31,6 +31,27 @@ function describeDestroyFailure(step, id, error) {
|
|
|
31
31
|
const reason = error instanceof Error ? error.message : String(error);
|
|
32
32
|
return `Fly sandbox destroy failed at the ${step} for ${id} (${status}): ${reason}`;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a failed exec is Fly's TRANSIENT precondition — the request reached
|
|
36
|
+
* the Machine's exec agent and the connection was dropped before the command
|
|
37
|
+
* ran (`412 failed_precondition: exec request failed: EOF`) — rather than the
|
|
38
|
+
* Machine being in a state an exec cannot run in.
|
|
39
|
+
*
|
|
40
|
+
* The distinction decides what retrying can achieve. A dropped connection is
|
|
41
|
+
* worth repeating: the Machine is `started` and its agent recovers in seconds.
|
|
42
|
+
* `failed_precondition: machine not running` is NOT — the Machine really is not
|
|
43
|
+
* running, and that is what {@link FlyioSandboxProvider.startMachine} waits out
|
|
44
|
+
* before the first exec. Repeating the exec there would only burn the caller's
|
|
45
|
+
* budget against a condition the wait endpoint already answers.
|
|
46
|
+
*
|
|
47
|
+
* @param error - The failure an exec attempt raised.
|
|
48
|
+
* @returns `true` when repeating the exec is the right recovery.
|
|
49
|
+
*/
|
|
50
|
+
export function isTransientExecPrecondition(error) {
|
|
51
|
+
if (!(error instanceof FlyApiError) || error.status !== 412)
|
|
52
|
+
return false;
|
|
53
|
+
return EXEC_BODY_TRANSIENT.test(error.body) && !EXEC_BODY_MACHINE_NOT_RUNNING.test(error.body);
|
|
54
|
+
}
|
|
34
55
|
/**
|
|
35
56
|
* Default sandbox image. Fly pulls images itself, so this MUST be a reference
|
|
36
57
|
* Fly can resolve — a tag in the org's `registry.fly.io` repository or a public
|
|
@@ -158,6 +179,50 @@ const MACHINE_CREATE_ATTEMPTS = 6;
|
|
|
158
179
|
* default retryable set; neither retry can double-create.
|
|
159
180
|
*/
|
|
160
181
|
const MACHINE_CREATE_RETRY_STATUSES = [404, 412];
|
|
182
|
+
/**
|
|
183
|
+
* What Fly answers when it reached the exec endpoint but the connection to the
|
|
184
|
+
* Machine's exec agent died before the command ran: `412 {"error":
|
|
185
|
+
* "failed_precondition: exec request failed: EOF"}`. The Machine is `started`;
|
|
186
|
+
* the agent inside it dropped the request (a Machine seconds past boot, or
|
|
187
|
+
* Fly's own exec burst). The SAME status carries the precondition this provider
|
|
188
|
+
* already handles — `failed_precondition: machine not running` — which is a
|
|
189
|
+
* real state, not a dropped connection, and is reconciled by `startMachine`
|
|
190
|
+
* BEFORE the first exec instead of by repeating it (see {@link
|
|
191
|
+
* isTransientExecPrecondition}).
|
|
192
|
+
*/
|
|
193
|
+
const EXEC_BODY_TRANSIENT = /exec request failed/i;
|
|
194
|
+
/** The real-precondition 412, which retrying an exec cannot fix. */
|
|
195
|
+
const EXEC_BODY_MACHINE_NOT_RUNNING = /machine not running/i;
|
|
196
|
+
/**
|
|
197
|
+
* Attempts for one exec when Fly answers the transient 412 of
|
|
198
|
+
* {@link EXEC_BODY_TRANSIENT}. Four attempts over the three backoffs below is
|
|
199
|
+
* under 12 s of waiting, against the ~75 s each fresh production deploy costs
|
|
200
|
+
* when this is left to a caller: X1 run flash-m85 (2026-10-16) lost three
|
|
201
|
+
* consecutive deploys, and the run, to this one condition.
|
|
202
|
+
*/
|
|
203
|
+
const EXEC_PRECONDITION_ATTEMPTS = 4;
|
|
204
|
+
/**
|
|
205
|
+
* Backoff before each exec retry (ms), indexed by the failed attempt number.
|
|
206
|
+
* Short on purpose: the Machine is up, and the agent recovers in seconds —
|
|
207
|
+
* nothing here is waiting on a boot.
|
|
208
|
+
*/
|
|
209
|
+
const EXEC_PRECONDITION_BACKOFF_MS = [1_000, 3_000, 7_000];
|
|
210
|
+
/**
|
|
211
|
+
* Wall-clock ceiling on {@link EXEC_PRECONDITION_ATTEMPTS}, so a Machine whose
|
|
212
|
+
* exec agent keeps dropping requests cannot hold a caller for the whole budget
|
|
213
|
+
* while the rest of the backoff ladder runs. Measured from the first attempt.
|
|
214
|
+
*/
|
|
215
|
+
const EXEC_PRECONDITION_BUDGET_MS = 60_000;
|
|
216
|
+
/**
|
|
217
|
+
* The backoff before an exec retry, clamped to the last rung so an attempt
|
|
218
|
+
* count raised past the ladder's length cannot read off the end of it.
|
|
219
|
+
* @param attempt - The 1-based number of the attempt that just failed.
|
|
220
|
+
* @returns The delay in milliseconds.
|
|
221
|
+
*/
|
|
222
|
+
function execPreconditionBackoffMs(attempt) {
|
|
223
|
+
const last = EXEC_PRECONDITION_BACKOFF_MS.length - 1;
|
|
224
|
+
return EXEC_PRECONDITION_BACKOFF_MS[Math.min(Math.max(attempt - 1, 0), last)];
|
|
225
|
+
}
|
|
161
226
|
/**
|
|
162
227
|
* Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Bounded by
|
|
163
228
|
* the same Fly exec payload limit as `WRITE_CHUNK_BASE64` (~15 KB, silently
|
|
@@ -1774,26 +1839,126 @@ class FlyioSandboxProvider {
|
|
|
1774
1839
|
// ---------------------------------------------------------------------------
|
|
1775
1840
|
/**
|
|
1776
1841
|
* Issues one Fly exec call against a Machine.
|
|
1842
|
+
*
|
|
1843
|
+
* A transient 412 (see {@link isTransientExecPrecondition}) is retried here, at
|
|
1844
|
+
* the transport, because everywhere it surfaces — a boot, the production
|
|
1845
|
+
* artifact copy, the boot script, a health probe — the caller's alternative is
|
|
1846
|
+
* to throw the whole operation away and start over, which costs tens of seconds
|
|
1847
|
+
* or minutes. Before the retry the Machine's state is re-read (and waited on
|
|
1848
|
+
* when it is not yet `started`), so a Machine that really is still coming up is
|
|
1849
|
+
* not hammered.
|
|
1850
|
+
*
|
|
1851
|
+
* A 412 that is NOT that condition is never retried, and its message carries
|
|
1852
|
+
* the Machine's current state — the caller is told which state the exec was
|
|
1853
|
+
* refused in rather than a bare `failed_precondition`.
|
|
1854
|
+
*
|
|
1777
1855
|
* @param app - The Fly app name.
|
|
1778
1856
|
* @param machineId - The Fly Machine id.
|
|
1779
1857
|
* @returns A transport callback for {@link execCommand}.
|
|
1780
1858
|
*/
|
|
1781
1859
|
rawExec(app, machineId) {
|
|
1782
1860
|
return async (command, timeoutSeconds) => {
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1861
|
+
const deadline = Date.now() + EXEC_PRECONDITION_BUDGET_MS;
|
|
1862
|
+
for (let attempt = 1;; attempt++) {
|
|
1863
|
+
try {
|
|
1864
|
+
const response = await this.client.request(`/apps/${app}/machines/${machineId}/exec`, {
|
|
1865
|
+
method: 'POST',
|
|
1866
|
+
body: { command, timeout: timeoutSeconds },
|
|
1867
|
+
// The API's own timeout bounds the command; allow the HTTP request a
|
|
1868
|
+
// little longer so the transport never fires first.
|
|
1869
|
+
timeoutMs: (timeoutSeconds + 15) * 1000,
|
|
1870
|
+
// A 404 on a Machine we created and are actively driving (e.g. the
|
|
1871
|
+
// exec burst importFiles issues) is Fly API inconsistency under load,
|
|
1872
|
+
// not a real absence — retry it alongside the default 429/5xx.
|
|
1873
|
+
retryStatuses: [404],
|
|
1874
|
+
});
|
|
1875
|
+
return response ?? {};
|
|
1876
|
+
}
|
|
1877
|
+
catch (error) {
|
|
1878
|
+
if (!isTransientExecPrecondition(error)) {
|
|
1879
|
+
throw await this.withMachineState(error, app, machineId);
|
|
1880
|
+
}
|
|
1881
|
+
if (attempt >= EXEC_PRECONDITION_ATTEMPTS || Date.now() >= deadline) {
|
|
1882
|
+
logger.warn('Fly kept dropping the exec connection; giving up on this exec', {
|
|
1883
|
+
app,
|
|
1884
|
+
machineId,
|
|
1885
|
+
attempt,
|
|
1886
|
+
});
|
|
1887
|
+
throw await this.withMachineState(error, app, machineId);
|
|
1888
|
+
}
|
|
1889
|
+
logger.warn('Fly dropped the exec connection (412 exec request failed); retrying', {
|
|
1890
|
+
app,
|
|
1891
|
+
machineId,
|
|
1892
|
+
attempt,
|
|
1893
|
+
attempts: EXEC_PRECONDITION_ATTEMPTS,
|
|
1894
|
+
});
|
|
1895
|
+
// The Machine is up (that is what makes this 412 the transient one), but
|
|
1896
|
+
// re-read it anyway: `stop`/`suspend` can land between attempts, and
|
|
1897
|
+
// retrying an exec against a Machine that is not running would repeat a
|
|
1898
|
+
// 412 that waiting — not repeating — is what answers.
|
|
1899
|
+
await this.waitForExecReady(app, machineId);
|
|
1900
|
+
await this.client.sleep(execPreconditionBackoffMs(attempt));
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1795
1903
|
};
|
|
1796
1904
|
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Confirms the Machine is in a state an exec can run in, waiting for `started`
|
|
1907
|
+
* when it is still on its way up.
|
|
1908
|
+
*
|
|
1909
|
+
* A wait that does not reach `started` is logged and the exec is retried
|
|
1910
|
+
* anyway: the retry is the recovery this path exists for, and turning a
|
|
1911
|
+
* recoverable dropped connection into a start-timeout error would hide the
|
|
1912
|
+
* condition the caller's error message is supposed to name.
|
|
1913
|
+
*
|
|
1914
|
+
* @param app - The Fly app name.
|
|
1915
|
+
* @param machineId - The Fly Machine id.
|
|
1916
|
+
*/
|
|
1917
|
+
async waitForExecReady(app, machineId) {
|
|
1918
|
+
const machine = await this.client
|
|
1919
|
+
.request(`/apps/${app}/machines/${machineId}`, { nullOn: [404] })
|
|
1920
|
+
.catch(() => null);
|
|
1921
|
+
if (machine?.state === 'started')
|
|
1922
|
+
return;
|
|
1923
|
+
try {
|
|
1924
|
+
await this.waitForStarted(app, machineId);
|
|
1925
|
+
}
|
|
1926
|
+
catch (error) {
|
|
1927
|
+
logger.warn('The Machine did not report started after a refused exec; retrying the exec', {
|
|
1928
|
+
app,
|
|
1929
|
+
machineId,
|
|
1930
|
+
state: machine?.state ?? 'unknown',
|
|
1931
|
+
error,
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Names the Machine's state on an exec 412, so the caller is told WHICH state
|
|
1937
|
+
* the exec was refused in. Fly's own body says only `failed_precondition`.
|
|
1938
|
+
*
|
|
1939
|
+
* A state that cannot be read is reported as such rather than guessed, and any
|
|
1940
|
+
* non-412 error (or an error that already carries the state) is returned
|
|
1941
|
+
* untouched — this runs on the failure path, where a diagnostic must never
|
|
1942
|
+
* replace the failure it is describing.
|
|
1943
|
+
*
|
|
1944
|
+
* @param error - The failure to annotate.
|
|
1945
|
+
* @param app - The Fly app name.
|
|
1946
|
+
* @param machineId - The Fly Machine id.
|
|
1947
|
+
* @returns The original error, or a 412 carrying the Machine's state.
|
|
1948
|
+
*/
|
|
1949
|
+
async withMachineState(error, app, machineId) {
|
|
1950
|
+
if (!(error instanceof Error))
|
|
1951
|
+
return new Error(String(error));
|
|
1952
|
+
if (!(error instanceof FlyApiError) || error.status !== 412)
|
|
1953
|
+
return error;
|
|
1954
|
+
if (/machine state:/i.test(error.message))
|
|
1955
|
+
return error;
|
|
1956
|
+
const machine = await this.client
|
|
1957
|
+
.request(`/apps/${app}/machines/${machineId}`, { nullOn: [404] })
|
|
1958
|
+
.catch(() => null);
|
|
1959
|
+
const state = machine?.state ?? 'unreadable';
|
|
1960
|
+
return new FlyApiError(error.method, error.path, error.status, `${error.body} (machine ${machineId} state: ${state})`, { cause: error });
|
|
1961
|
+
}
|
|
1797
1962
|
/**
|
|
1798
1963
|
* Starts (or resumes) a Machine, tolerating the case where it is already up.
|
|
1799
1964
|
*
|