@molecule/api-code-sandbox-flyio 1.2.8 → 1.2.10
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 +1146 -1025
- 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 +44 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +250 -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,50 @@ 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 — one a repeat (after a
|
|
15
|
+
* short wait) can clear, rather than a Machine state the exec has to wait out.
|
|
16
|
+
*
|
|
17
|
+
* Fly answers the exec endpoint with 412 in two different situations, and only
|
|
18
|
+
* the BODY and the Machine's own reported state separate them:
|
|
19
|
+
*
|
|
20
|
+
* 1. `failed_precondition: exec request failed: EOF` — the request reached the
|
|
21
|
+
* Machine's exec agent and the connection dropped before the command ran.
|
|
22
|
+
* Always transient: the Machine is `started` and its agent recovers in
|
|
23
|
+
* seconds.
|
|
24
|
+
* 2. `failed_precondition: machine not running` — transient ONLY while Fly's
|
|
25
|
+
* own Machines API reports the Machine `started`, `starting` or `replacing`
|
|
26
|
+
* (`machineState`). The exec endpoint lags the state Fly reports: a Machine
|
|
27
|
+
* whose config or image was just updated, or that is seconds past boot,
|
|
28
|
+
* reads `started` in `GET /machines/{id}` while its exec endpoint still
|
|
29
|
+
* refuses requests. Against a Machine Fly reports `stopped`/`suspended`/
|
|
30
|
+
* `destroyed` the same body is the REAL precondition — the Machine is not
|
|
31
|
+
* running, and {@link FlyioSandboxProvider.startMachine} is what answers it;
|
|
32
|
+
* repeating the exec would only burn the caller's budget.
|
|
33
|
+
*
|
|
34
|
+
* @param error - The failure an exec attempt raised.
|
|
35
|
+
* @param machineState - The state `GET /machines/{id}` reports for the Machine,
|
|
36
|
+
* or `null`/`undefined` when it could not be read (which classifies case 2 as
|
|
37
|
+
* NOT transient — an unknown state is never assumed recoverable).
|
|
38
|
+
* @returns `true` when repeating the exec is the right recovery.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isTransientExecPrecondition(error: unknown, machineState?: string | null): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a state Fly reports for a Machine means the Machine is up, or on its
|
|
43
|
+
* way up — the states in which an exec that was refused for `machine not
|
|
44
|
+
* running` is the exec endpoint lagging the state, not the truth.
|
|
45
|
+
*
|
|
46
|
+
* A deliberate allow-list rather than "anything but stopped/suspended": the
|
|
47
|
+
* three states here are the ones a refused exec was observed to recover from,
|
|
48
|
+
* and every other state — `stopped`, `suspended`, `destroyed`, `failed`,
|
|
49
|
+
* `created`, unreadable — keeps today's immediate throw. Widening it without
|
|
50
|
+
* that evidence would turn a real precondition into a retry budget spent
|
|
51
|
+
* against a Machine that is not coming up.
|
|
52
|
+
*
|
|
53
|
+
* @param state - The Machine state from `GET /machines/{id}`, if readable.
|
|
54
|
+
* @returns `true` when waiting and repeating the exec can clear the refusal.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isMachineWarmingUp(state: string | null | undefined): boolean;
|
|
13
57
|
/**
|
|
14
58
|
* Creates a Fly.io Machines sandbox provider.
|
|
15
59
|
* @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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAIjG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAksFD;;;;;;;;;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,58 @@ 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 — one a repeat (after a
|
|
36
|
+
* short wait) can clear, rather than a Machine state the exec has to wait out.
|
|
37
|
+
*
|
|
38
|
+
* Fly answers the exec endpoint with 412 in two different situations, and only
|
|
39
|
+
* the BODY and the Machine's own reported state separate them:
|
|
40
|
+
*
|
|
41
|
+
* 1. `failed_precondition: exec request failed: EOF` — the request reached the
|
|
42
|
+
* Machine's exec agent and the connection dropped before the command ran.
|
|
43
|
+
* Always transient: the Machine is `started` and its agent recovers in
|
|
44
|
+
* seconds.
|
|
45
|
+
* 2. `failed_precondition: machine not running` — transient ONLY while Fly's
|
|
46
|
+
* own Machines API reports the Machine `started`, `starting` or `replacing`
|
|
47
|
+
* (`machineState`). The exec endpoint lags the state Fly reports: a Machine
|
|
48
|
+
* whose config or image was just updated, or that is seconds past boot,
|
|
49
|
+
* reads `started` in `GET /machines/{id}` while its exec endpoint still
|
|
50
|
+
* refuses requests. Against a Machine Fly reports `stopped`/`suspended`/
|
|
51
|
+
* `destroyed` the same body is the REAL precondition — the Machine is not
|
|
52
|
+
* running, and {@link FlyioSandboxProvider.startMachine} is what answers it;
|
|
53
|
+
* repeating the exec would only burn the caller's budget.
|
|
54
|
+
*
|
|
55
|
+
* @param error - The failure an exec attempt raised.
|
|
56
|
+
* @param machineState - The state `GET /machines/{id}` reports for the Machine,
|
|
57
|
+
* or `null`/`undefined` when it could not be read (which classifies case 2 as
|
|
58
|
+
* NOT transient — an unknown state is never assumed recoverable).
|
|
59
|
+
* @returns `true` when repeating the exec is the right recovery.
|
|
60
|
+
*/
|
|
61
|
+
export function isTransientExecPrecondition(error, machineState) {
|
|
62
|
+
if (!(error instanceof FlyApiError) || error.status !== 412)
|
|
63
|
+
return false;
|
|
64
|
+
if (EXEC_BODY_DROPPED_CONNECTION.test(error.body))
|
|
65
|
+
return true;
|
|
66
|
+
return EXEC_BODY_MACHINE_NOT_RUNNING.test(error.body) && isMachineWarmingUp(machineState);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a state Fly reports for a Machine means the Machine is up, or on its
|
|
70
|
+
* way up — the states in which an exec that was refused for `machine not
|
|
71
|
+
* running` is the exec endpoint lagging the state, not the truth.
|
|
72
|
+
*
|
|
73
|
+
* A deliberate allow-list rather than "anything but stopped/suspended": the
|
|
74
|
+
* three states here are the ones a refused exec was observed to recover from,
|
|
75
|
+
* and every other state — `stopped`, `suspended`, `destroyed`, `failed`,
|
|
76
|
+
* `created`, unreadable — keeps today's immediate throw. Widening it without
|
|
77
|
+
* that evidence would turn a real precondition into a retry budget spent
|
|
78
|
+
* against a Machine that is not coming up.
|
|
79
|
+
*
|
|
80
|
+
* @param state - The Machine state from `GET /machines/{id}`, if readable.
|
|
81
|
+
* @returns `true` when waiting and repeating the exec can clear the refusal.
|
|
82
|
+
*/
|
|
83
|
+
export function isMachineWarmingUp(state) {
|
|
84
|
+
return state === 'started' || state === 'starting' || state === 'replacing';
|
|
85
|
+
}
|
|
34
86
|
/**
|
|
35
87
|
* Default sandbox image. Fly pulls images itself, so this MUST be a reference
|
|
36
88
|
* Fly can resolve — a tag in the org's `registry.fly.io` repository or a public
|
|
@@ -158,6 +210,54 @@ const MACHINE_CREATE_ATTEMPTS = 6;
|
|
|
158
210
|
* default retryable set; neither retry can double-create.
|
|
159
211
|
*/
|
|
160
212
|
const MACHINE_CREATE_RETRY_STATUSES = [404, 412];
|
|
213
|
+
/**
|
|
214
|
+
* What Fly answers when it reached the exec endpoint but the connection to the
|
|
215
|
+
* Machine's exec agent died before the command ran: `412 {"error":
|
|
216
|
+
* "failed_precondition: exec request failed: EOF"}`. The Machine is `started`;
|
|
217
|
+
* the agent inside it dropped the request (a Machine seconds past boot, or
|
|
218
|
+
* Fly's own exec burst).
|
|
219
|
+
*/
|
|
220
|
+
const EXEC_BODY_DROPPED_CONNECTION = /exec request failed/i;
|
|
221
|
+
/**
|
|
222
|
+
* The OTHER 412 the exec endpoint answers — `{"error":"failed_precondition:
|
|
223
|
+
* machine not running"}`. Whether that is the real precondition or the exec
|
|
224
|
+
* endpoint lagging a Machine that is up cannot be read off the body, so it is
|
|
225
|
+
* settled by the state Fly reports for the Machine at that moment
|
|
226
|
+
* ({@link isMachineWarmingUp}).
|
|
227
|
+
*/
|
|
228
|
+
const EXEC_BODY_MACHINE_NOT_RUNNING = /machine not running/i;
|
|
229
|
+
/**
|
|
230
|
+
* Attempts for one exec when Fly answers a transient 412 — either body of
|
|
231
|
+
* {@link isTransientExecPrecondition}. Four attempts over the three backoffs
|
|
232
|
+
* below is under 12 s of waiting, against the ~75 s each fresh production
|
|
233
|
+
* deploy costs when this is left to a caller: X1 run flash-m85 (2026-10-16)
|
|
234
|
+
* lost three consecutive deploys, and the run, to the dropped connection;
|
|
235
|
+
* flash-m94 (2026-10-16) lost 65 s and a whole redeploy to the same budget not
|
|
236
|
+
* covering `machine not running` on a Machine Fly reported `started`.
|
|
237
|
+
*/
|
|
238
|
+
const EXEC_PRECONDITION_ATTEMPTS = 4;
|
|
239
|
+
/**
|
|
240
|
+
* Backoff before each exec retry (ms), indexed by the failed attempt number.
|
|
241
|
+
* Short on purpose: the Machine is up, and the agent recovers in seconds —
|
|
242
|
+
* nothing here is waiting on a boot.
|
|
243
|
+
*/
|
|
244
|
+
const EXEC_PRECONDITION_BACKOFF_MS = [1_000, 3_000, 7_000];
|
|
245
|
+
/**
|
|
246
|
+
* Wall-clock ceiling on {@link EXEC_PRECONDITION_ATTEMPTS}, so a Machine whose
|
|
247
|
+
* exec agent keeps dropping requests cannot hold a caller for the whole budget
|
|
248
|
+
* while the rest of the backoff ladder runs. Measured from the first attempt.
|
|
249
|
+
*/
|
|
250
|
+
const EXEC_PRECONDITION_BUDGET_MS = 60_000;
|
|
251
|
+
/**
|
|
252
|
+
* The backoff before an exec retry, clamped to the last rung so an attempt
|
|
253
|
+
* count raised past the ladder's length cannot read off the end of it.
|
|
254
|
+
* @param attempt - The 1-based number of the attempt that just failed.
|
|
255
|
+
* @returns The delay in milliseconds.
|
|
256
|
+
*/
|
|
257
|
+
function execPreconditionBackoffMs(attempt) {
|
|
258
|
+
const last = EXEC_PRECONDITION_BACKOFF_MS.length - 1;
|
|
259
|
+
return EXEC_PRECONDITION_BACKOFF_MS[Math.min(Math.max(attempt - 1, 0), last)];
|
|
260
|
+
}
|
|
161
261
|
/**
|
|
162
262
|
* Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Bounded by
|
|
163
263
|
* the same Fly exec payload limit as `WRITE_CHUNK_BASE64` (~15 KB, silently
|
|
@@ -1772,28 +1872,166 @@ class FlyioSandboxProvider {
|
|
|
1772
1872
|
// ---------------------------------------------------------------------------
|
|
1773
1873
|
// Sandbox
|
|
1774
1874
|
// ---------------------------------------------------------------------------
|
|
1875
|
+
/**
|
|
1876
|
+
* Reads the state Fly reports for a Machine, or `null` when it cannot be read.
|
|
1877
|
+
*
|
|
1878
|
+
* Best-effort on purpose: this runs both on the failure path and just before a
|
|
1879
|
+
* retry, and a failed diagnostic read must not replace the failure it is
|
|
1880
|
+
* describing. `null` is meaningful — it never classifies a refusal as
|
|
1881
|
+
* recoverable (see {@link isTransientExecPrecondition}).
|
|
1882
|
+
*
|
|
1883
|
+
* @param app - The Fly app name.
|
|
1884
|
+
* @param machineId - The Fly Machine id.
|
|
1885
|
+
* @returns The Machine's state, or `null` when the read failed or 404'd.
|
|
1886
|
+
*/
|
|
1887
|
+
async readMachineState(app, machineId) {
|
|
1888
|
+
try {
|
|
1889
|
+
const machine = await this.client.request(`/apps/${app}/machines/${machineId}`, {
|
|
1890
|
+
nullOn: [404],
|
|
1891
|
+
});
|
|
1892
|
+
return machine?.state ?? null;
|
|
1893
|
+
}
|
|
1894
|
+
catch (error) {
|
|
1895
|
+
logger.debug('Could not read the Machine state while an exec was refused', {
|
|
1896
|
+
app,
|
|
1897
|
+
machineId,
|
|
1898
|
+
error,
|
|
1899
|
+
});
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1775
1903
|
/**
|
|
1776
1904
|
* Issues one Fly exec call against a Machine.
|
|
1905
|
+
*
|
|
1906
|
+
* A transient 412 (see {@link isTransientExecPrecondition}) is retried here, at
|
|
1907
|
+
* the transport, because everywhere it surfaces — a boot, the production
|
|
1908
|
+
* artifact copy, the boot script, a health probe — the caller's alternative is
|
|
1909
|
+
* to throw the whole operation away and start over, which costs tens of seconds
|
|
1910
|
+
* or minutes. Before the retry the Machine's state is re-read (and waited on
|
|
1911
|
+
* when it is not yet `started`), so a Machine that really is still coming up is
|
|
1912
|
+
* not hammered.
|
|
1913
|
+
*
|
|
1914
|
+
* A 412 that is NOT that condition is never retried, and its message carries
|
|
1915
|
+
* the Machine's current state — the caller is told which state the exec was
|
|
1916
|
+
* refused in rather than a bare `failed_precondition`.
|
|
1917
|
+
*
|
|
1777
1918
|
* @param app - The Fly app name.
|
|
1778
1919
|
* @param machineId - The Fly Machine id.
|
|
1779
1920
|
* @returns A transport callback for {@link execCommand}.
|
|
1780
1921
|
*/
|
|
1781
1922
|
rawExec(app, machineId) {
|
|
1782
1923
|
return async (command, timeoutSeconds) => {
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1924
|
+
const deadline = Date.now() + EXEC_PRECONDITION_BUDGET_MS;
|
|
1925
|
+
for (let attempt = 1;; attempt++) {
|
|
1926
|
+
try {
|
|
1927
|
+
const response = await this.client.request(`/apps/${app}/machines/${machineId}/exec`, {
|
|
1928
|
+
method: 'POST',
|
|
1929
|
+
body: { command, timeout: timeoutSeconds },
|
|
1930
|
+
// The API's own timeout bounds the command; allow the HTTP request a
|
|
1931
|
+
// little longer so the transport never fires first.
|
|
1932
|
+
timeoutMs: (timeoutSeconds + 15) * 1000,
|
|
1933
|
+
// A 404 on a Machine we created and are actively driving (e.g. the
|
|
1934
|
+
// exec burst importFiles issues) is Fly API inconsistency under load,
|
|
1935
|
+
// not a real absence — retry it alongside the default 429/5xx.
|
|
1936
|
+
retryStatuses: [404],
|
|
1937
|
+
});
|
|
1938
|
+
return response ?? {};
|
|
1939
|
+
}
|
|
1940
|
+
catch (error) {
|
|
1941
|
+
// Which 412 this is cannot be read off the body alone: `machine not
|
|
1942
|
+
// running` is transient against a Machine Fly reports `started` and the
|
|
1943
|
+
// real precondition against one it reports `stopped`. So the state is
|
|
1944
|
+
// read here, once, and the same read decides the classification, the
|
|
1945
|
+
// wait below, and the state the error finally names.
|
|
1946
|
+
const state = error instanceof FlyApiError && error.status === 412
|
|
1947
|
+
? await this.readMachineState(app, machineId)
|
|
1948
|
+
: null;
|
|
1949
|
+
if (!isTransientExecPrecondition(error, state)) {
|
|
1950
|
+
throw await this.withMachineState(error, app, machineId, state);
|
|
1951
|
+
}
|
|
1952
|
+
if (attempt >= EXEC_PRECONDITION_ATTEMPTS || Date.now() >= deadline) {
|
|
1953
|
+
logger.warn('Fly kept refusing the exec with a transient precondition', {
|
|
1954
|
+
app,
|
|
1955
|
+
machineId,
|
|
1956
|
+
attempt,
|
|
1957
|
+
state: state ?? 'unreadable',
|
|
1958
|
+
});
|
|
1959
|
+
throw await this.withMachineState(error, app, machineId, state);
|
|
1960
|
+
}
|
|
1961
|
+
logger.warn('Fly refused the exec with a transient precondition; retrying', {
|
|
1962
|
+
app,
|
|
1963
|
+
machineId,
|
|
1964
|
+
attempt,
|
|
1965
|
+
attempts: EXEC_PRECONDITION_ATTEMPTS,
|
|
1966
|
+
state: state ?? 'unreadable',
|
|
1967
|
+
});
|
|
1968
|
+
// The Machine is up (that is what makes this 412 the transient one), but
|
|
1969
|
+
// re-read it anyway: `stop`/`suspend` can land between attempts, and
|
|
1970
|
+
// retrying an exec against a Machine that is not running would repeat a
|
|
1971
|
+
// 412 that waiting — not repeating — is what answers.
|
|
1972
|
+
await this.waitForExecReady(app, machineId, state);
|
|
1973
|
+
await this.client.sleep(execPreconditionBackoffMs(attempt));
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1795
1976
|
};
|
|
1796
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Confirms the Machine is in a state an exec can run in, waiting for `started`
|
|
1980
|
+
* when it is still on its way up.
|
|
1981
|
+
*
|
|
1982
|
+
* A wait that does not reach `started` is logged and the exec is retried
|
|
1983
|
+
* anyway: the retry is the recovery this path exists for, and turning a
|
|
1984
|
+
* recoverable refusal into a start-timeout error would hide the condition the
|
|
1985
|
+
* caller's error message is supposed to name.
|
|
1986
|
+
*
|
|
1987
|
+
* @param app - The Fly app name.
|
|
1988
|
+
* @param machineId - The Fly Machine id.
|
|
1989
|
+
* @param state - The state already read for this Machine, when the caller read
|
|
1990
|
+
* one (the exec retry does); read again when omitted.
|
|
1991
|
+
*/
|
|
1992
|
+
async waitForExecReady(app, machineId, state) {
|
|
1993
|
+
const machineState = state === undefined ? await this.readMachineState(app, machineId) : state;
|
|
1994
|
+
if (machineState === 'started')
|
|
1995
|
+
return;
|
|
1996
|
+
try {
|
|
1997
|
+
await this.waitForStarted(app, machineId);
|
|
1998
|
+
}
|
|
1999
|
+
catch (error) {
|
|
2000
|
+
logger.warn('The Machine did not report started after a refused exec; retrying the exec', {
|
|
2001
|
+
app,
|
|
2002
|
+
machineId,
|
|
2003
|
+
state: machineState ?? 'unknown',
|
|
2004
|
+
error,
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Names the Machine's state on an exec 412, so the caller is told WHICH state
|
|
2010
|
+
* the exec was refused in. Fly's own body says only `failed_precondition`.
|
|
2011
|
+
*
|
|
2012
|
+
* A state that cannot be read is reported as such rather than guessed, and any
|
|
2013
|
+
* non-412 error (or an error that already carries the state) is returned
|
|
2014
|
+
* untouched — this runs on the failure path, where a diagnostic must never
|
|
2015
|
+
* replace the failure it is describing.
|
|
2016
|
+
*
|
|
2017
|
+
* @param error - The failure to annotate.
|
|
2018
|
+
* @param app - The Fly app name.
|
|
2019
|
+
* @param machineId - The Fly Machine id.
|
|
2020
|
+
* @param knownState - The state already read for this Machine, when the caller
|
|
2021
|
+
* read one (the exec retry does); read again when omitted.
|
|
2022
|
+
* @returns The original error, or a 412 carrying the Machine's state.
|
|
2023
|
+
*/
|
|
2024
|
+
async withMachineState(error, app, machineId, knownState) {
|
|
2025
|
+
if (!(error instanceof Error))
|
|
2026
|
+
return new Error(String(error));
|
|
2027
|
+
if (!(error instanceof FlyApiError) || error.status !== 412)
|
|
2028
|
+
return error;
|
|
2029
|
+
if (/machine state:/i.test(error.message))
|
|
2030
|
+
return error;
|
|
2031
|
+
const read = knownState === undefined ? await this.readMachineState(app, machineId) : knownState;
|
|
2032
|
+
const state = read ?? 'unreadable';
|
|
2033
|
+
return new FlyApiError(error.method, error.path, error.status, `${error.body} (machine ${machineId} state: ${state})`, { cause: error });
|
|
2034
|
+
}
|
|
1797
2035
|
/**
|
|
1798
2036
|
* Starts (or resumes) a Machine, tolerating the case where it is already up.
|
|
1799
2037
|
*
|