@molecule/api-code-sandbox-flyio 1.0.11 → 1.0.13
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 +26 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +79 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-08-
|
|
6
|
+
Generated: 2026-08-08T23:10:20.743Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-flyio
|
|
@@ -609,6 +609,13 @@ interface FlyRequestOptions {
|
|
|
609
609
|
* Used for idempotent deletes and existence checks (`[404]`).
|
|
610
610
|
*/
|
|
611
611
|
nullOn?: number[]
|
|
612
|
+
/**
|
|
613
|
+
* Extra HTTP statuses to treat as transient (retry) beyond the default
|
|
614
|
+
* `429`/`5xx`. The exec endpoint passes `[404]`: a machine-not-found for a
|
|
615
|
+
* Machine we just created and are actively driving is Fly API inconsistency
|
|
616
|
+
* under exec-burst load, not a real absence, so it is worth repeating.
|
|
617
|
+
*/
|
|
618
|
+
retryStatuses?: number[]
|
|
612
619
|
}
|
|
613
620
|
```
|
|
614
621
|
|
|
@@ -1125,6 +1132,10 @@ function encodePrivateRoutes(routes: Record<string, string>): string
|
|
|
1125
1132
|
Executes a command on a Machine, choosing the direct or detached strategy
|
|
1126
1133
|
based on the caller's time budget.
|
|
1127
1134
|
|
|
1135
|
+
A command too large to pass inline (over {@link FLY_EXEC_MAX_ARG_BYTES}) is
|
|
1136
|
+
spilled to a file via chunked writes and run from there, so there is no size
|
|
1137
|
+
ceiling the caller must respect.
|
|
1138
|
+
|
|
1128
1139
|
```typescript
|
|
1129
1140
|
function execCommand(
|
|
1130
1141
|
rawExec: RawExec,
|
|
@@ -1751,6 +1762,20 @@ Exit status the capture script uses when `tar` failed outright (as opposed to wa
|
|
|
1751
1762
|
const EXIT_TAR_FAILED: 92
|
|
1752
1763
|
```
|
|
1753
1764
|
|
|
1765
|
+
#### `FLY_EXEC_MAX_ARG_BYTES`
|
|
1766
|
+
|
|
1767
|
+
Hard cap on the `sh -c <script>` argument SENT to Fly's exec endpoint. This is
|
|
1768
|
+
NOT the OS `MAX_ARG_STRLEN` (128 KB) — the Fly Machines exec API rejects a
|
|
1769
|
+
command well below that, and does so INVISIBLY: it answers `200` with
|
|
1770
|
+
`exit_code: 0` and an `Unhandled rejection: Rejection([PayloadTooLarge…])`
|
|
1771
|
+
body, so the command never runs yet reads as success. Measured against the
|
|
1772
|
+
live API: ~15.8 KB scripts run, ~23.7 KB are rejected. 15 KB is the safe
|
|
1773
|
+
ceiling; callers that move bulk data (writeFile, importFiles) chunk beneath it.
|
|
1774
|
+
|
|
1775
|
+
```typescript
|
|
1776
|
+
const FLY_EXEC_MAX_ARG_BYTES: 15000
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1754
1779
|
#### `FLY_EXEC_MAX_TIMEOUT_SECONDS`
|
|
1755
1780
|
|
|
1756
1781
|
Fly's hard ceiling on the exec endpoint's `timeout` field, in seconds.
|
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,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAUV,eAAe,EAEhB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EAAE,YAAY,EAAgC,MAAM,UAAU,CAAA;AAsBrE,OAAO,EAAmD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAEhG,OAAO,KAAK,EAEV,WAAW,EAUZ,MAAM,YAAY,CAAA;AA83DnB;;;;;;;;;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
|
@@ -82,6 +82,23 @@ const DEFAULT_PROBE_TIMEOUT_MS = 3000;
|
|
|
82
82
|
* like `npm install`, not for un-tarring.
|
|
83
83
|
*/
|
|
84
84
|
const IMPORT_EXTRACT_TIMEOUT_MS = 55_000;
|
|
85
|
+
/**
|
|
86
|
+
* Attempts for the create's `POST /machines`, retrying the app-propagation 404
|
|
87
|
+
* (see {@link MACHINE_CREATE_RETRY_STATUSES}). Fly propagates a new app to its
|
|
88
|
+
* sub-APIs asynchronously, so `POST /apps` can return before
|
|
89
|
+
* `POST /apps/{app}/machines` sees the app — which 404s "app not found" for a
|
|
90
|
+
* few seconds. Observed live during boot on 2026-08-08 (surfaced to the user as
|
|
91
|
+
* "Failed to start sandbox"). Six attempts span the client's 1+2+4+8+16 s
|
|
92
|
+
* backoff ≈ 31 s, comfortably past the observed window.
|
|
93
|
+
*/
|
|
94
|
+
const MACHINE_CREATE_ATTEMPTS = 6;
|
|
95
|
+
/**
|
|
96
|
+
* Retrying a `POST /machines` on 404 is SAFE precisely because "app not found"
|
|
97
|
+
* means no Machine was created — unlike a 5xx/timeout, where one might have
|
|
98
|
+
* been. So only 404 is added here, on top of the client's default retryable
|
|
99
|
+
* set; the retry cannot double-create.
|
|
100
|
+
*/
|
|
101
|
+
const MACHINE_CREATE_RETRY_STATUSES = [404];
|
|
85
102
|
/**
|
|
86
103
|
* Base64 chunk size for {@link Sandbox.importFiles}'s temp-file write. Bounded by
|
|
87
104
|
* the same Fly exec payload limit as `WRITE_CHUNK_BASE64` (~15 KB, silently
|
|
@@ -90,6 +107,12 @@ const IMPORT_EXTRACT_TIMEOUT_MS = 55_000;
|
|
|
90
107
|
* the gzip pass before transfer is what keeps that count reasonable.
|
|
91
108
|
*/
|
|
92
109
|
const IMPORT_WRITE_CHUNK_BASE64 = 12_000;
|
|
110
|
+
/**
|
|
111
|
+
* Expiry for the presigned URLs {@link Sandbox.importFiles} uses to hand an
|
|
112
|
+
* archive to the object store and back to the Machine. Short — the upload and
|
|
113
|
+
* the single download happen back-to-back.
|
|
114
|
+
*/
|
|
115
|
+
const IMPORT_PRESIGN_EXPIRY_SECONDS = 3600;
|
|
93
116
|
/** Extra seconds allowed for the probe exec beyond its connection budget. */
|
|
94
117
|
const PROBE_EXEC_HEADROOM_SECONDS = 10;
|
|
95
118
|
/**
|
|
@@ -391,7 +414,7 @@ class FlyioSandboxProvider {
|
|
|
391
414
|
*/
|
|
392
415
|
async destroyStaleMachines(app) {
|
|
393
416
|
const managedKey = `${this.metadataPrefix}.managed`;
|
|
394
|
-
let machines
|
|
417
|
+
let machines;
|
|
395
418
|
try {
|
|
396
419
|
machines = await this.client.request(`/apps/${app}/machines`, {
|
|
397
420
|
nullOn: [404],
|
|
@@ -791,6 +814,11 @@ class FlyioSandboxProvider {
|
|
|
791
814
|
const machine = await this.client.request(`/apps/${app}/machines`, {
|
|
792
815
|
method: 'POST',
|
|
793
816
|
body: { region: this.region(), config: machineConfig },
|
|
817
|
+
// A just-created app propagates to the Machines API asynchronously, so this
|
|
818
|
+
// POST can 404 "app not found" for a few seconds. Retry the 404 (safe: no
|
|
819
|
+
// Machine is created when the app is not found) across the client's backoff.
|
|
820
|
+
attempts: MACHINE_CREATE_ATTEMPTS,
|
|
821
|
+
retryStatuses: MACHINE_CREATE_RETRY_STATUSES,
|
|
794
822
|
});
|
|
795
823
|
if (!machine?.id) {
|
|
796
824
|
throw new Error(t('codeSandbox.flyio.error.createFailed', { app }, { defaultValue: `Fly Machine creation in app "${app}" returned no Machine id.` }));
|
|
@@ -1495,25 +1523,63 @@ class FlyioSandboxProvider {
|
|
|
1495
1523
|
const parts = [];
|
|
1496
1524
|
for await (const chunk of archive)
|
|
1497
1525
|
parts.push(Buffer.from(chunk));
|
|
1498
|
-
const
|
|
1499
|
-
// The temp tar lives on the WORKSPACE VOLUME, not /tmp. The chunked
|
|
1500
|
-
// write and the extract are separate exec calls, and a Machine with
|
|
1501
|
-
// autostop=suspend can cold-start between them — which recreates the
|
|
1502
|
-
// rootfs from the image (wiping /tmp) while the ext4 volume at
|
|
1503
|
-
// /workspace persists. A /tmp temp file vanished mid-import; the volume
|
|
1504
|
-
// one survives.
|
|
1505
|
-
const tmp = `${WORKSPACE_PATH}/.mol-import-${machineId}.tgz`;
|
|
1506
|
-
const quotedTmp = shellQuote(tmp);
|
|
1526
|
+
const gz = gzipSync(Buffer.concat(parts));
|
|
1507
1527
|
const quotedPath = shellQuote(path);
|
|
1508
1528
|
const failIfError = (result, step) => {
|
|
1509
1529
|
if (result.exitCode !== 0) {
|
|
1510
1530
|
throw new Error(t('codeSandbox.flyio.error.importFailed', { path, step, error: result.stderr }, {
|
|
1511
|
-
defaultValue: `Failed to import files into ${path} (${step}): ${result.stderr}`,
|
|
1531
|
+
defaultValue: `Failed to import files into ${path} (${step}): ${result.stderr || result.stdout}`,
|
|
1512
1532
|
}));
|
|
1513
1533
|
}
|
|
1514
1534
|
};
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1535
|
+
// PREFERRED: upload the archive to the object store and have the Machine
|
|
1536
|
+
// PULL it with a single curl. Fly's exec endpoint is rate-limited (~5/s)
|
|
1537
|
+
// and caps each command near 15 KB, so streaming a multi-MB tar through
|
|
1538
|
+
// it as base64 chunks triggers a 429 storm that never finishes. A
|
|
1539
|
+
// presigned download is ONE request — the same pull pattern templates use
|
|
1540
|
+
// (see buildRestoreCommand). Falls back to chunked exec when no store is
|
|
1541
|
+
// configured (e.g. unit tests).
|
|
1542
|
+
const store = provider.templateContext().store;
|
|
1543
|
+
if (store) {
|
|
1544
|
+
const key = `${store.prefix}/.imports/${machineId}-${Date.now().toString(36)}${Math.floor(Math.random() * 1e9).toString(36)}.tar.gz`;
|
|
1545
|
+
try {
|
|
1546
|
+
const putUrl = await store.presignPut(key, IMPORT_PRESIGN_EXPIRY_SECONDS);
|
|
1547
|
+
const putRes = await fetch(putUrl, { method: 'PUT', body: gz });
|
|
1548
|
+
if (!putRes.ok) {
|
|
1549
|
+
throw new Error(`presigned PUT to the object store failed: ${putRes.status}`);
|
|
1550
|
+
}
|
|
1551
|
+
const getUrl = await store.presignGet(key, IMPORT_PRESIGN_EXPIRY_SECONDS);
|
|
1552
|
+
const result = await this.exec([
|
|
1553
|
+
'set -e',
|
|
1554
|
+
`mkdir -p ${quotedPath}`,
|
|
1555
|
+
'archive=$(mktemp /tmp/mol-import-XXXXXX.tar.gz)',
|
|
1556
|
+
`curl --fail-with-body -sS --retry 3 --retry-connrefused -o "$archive" ${shellQuote(getUrl)}`,
|
|
1557
|
+
`tar -C ${quotedPath} --no-same-owner --no-same-permissions -xzf "$archive"`,
|
|
1558
|
+
'rm -f "$archive"',
|
|
1559
|
+
].join('\n'), { timeout: IMPORT_EXTRACT_TIMEOUT_MS });
|
|
1560
|
+
// Best-effort: the temp object expires anyway, but delete it promptly.
|
|
1561
|
+
store.remove([key]).catch((error) => {
|
|
1562
|
+
logger.debug('importFiles temp object cleanup failed', { key, error });
|
|
1563
|
+
});
|
|
1564
|
+
failIfError(result, 'download');
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
// The store path is unavailable or failed — fall through to the
|
|
1569
|
+
// chunked-exec transport rather than failing the whole import.
|
|
1570
|
+
logger.warn('importFiles via object store failed; falling back to chunked exec', {
|
|
1571
|
+
error,
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// FALLBACK: chunked base64 over exec. Only used when no object store is
|
|
1576
|
+
// configured. The temp tar lives on the WORKSPACE VOLUME, not /tmp — the
|
|
1577
|
+
// chunked write and the extract are separate exec calls and a Machine
|
|
1578
|
+
// with autostop can cold-start between them, wiping /tmp (rootfs) while
|
|
1579
|
+
// the ext4 volume at /workspace persists.
|
|
1580
|
+
const base64 = gz.toString('base64');
|
|
1581
|
+
const tmp = `${WORKSPACE_PATH}/.mol-import-${machineId}.tgz`;
|
|
1582
|
+
const quotedTmp = shellQuote(tmp);
|
|
1517
1583
|
failIfError(await this.exec(`printf %s ${shellQuote(base64.slice(0, IMPORT_WRITE_CHUNK_BASE64))} | base64 -d > ${quotedTmp}`, { timeout: FILE_OP_TIMEOUT_MS }), 'write');
|
|
1518
1584
|
for (let i = IMPORT_WRITE_CHUNK_BASE64; i < base64.length; i += IMPORT_WRITE_CHUNK_BASE64) {
|
|
1519
1585
|
failIfError(await this.exec(`printf %s ${shellQuote(base64.slice(i, i + IMPORT_WRITE_CHUNK_BASE64))} | base64 -d >> ${quotedTmp}`, { timeout: FILE_OP_TIMEOUT_MS }), 'write');
|