rightsize 0.1.2 → 0.2.0
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 +28 -4
- package/dist/backend-docker/backend.d.ts +53 -1
- package/dist/backend-docker/backend.d.ts.map +1 -1
- package/dist/backend-docker/backend.js +106 -13
- package/dist/backend-docker/backend.js.map +1 -1
- package/dist/backend-docker/labels.d.ts +27 -5
- package/dist/backend-docker/labels.d.ts.map +1 -1
- package/dist/backend-docker/labels.js +33 -5
- package/dist/backend-docker/labels.js.map +1 -1
- package/dist/backend-msb/backend.d.ts +35 -6
- package/dist/backend-msb/backend.d.ts.map +1 -1
- package/dist/backend-msb/backend.js +69 -14
- package/dist/backend-msb/backend.js.map +1 -1
- package/dist/backend-msb/provider.d.ts.map +1 -1
- package/dist/backend-msb/provider.js +7 -11
- package/dist/backend-msb/provider.js.map +1 -1
- package/dist/backend-msb/provisioner.d.ts.map +1 -1
- package/dist/backend-msb/provisioner.js +1 -20
- package/dist/backend-msb/provisioner.js.map +1 -1
- package/dist/core/backend.d.ts +73 -0
- package/dist/core/backend.d.ts.map +1 -1
- package/dist/core/backends.d.ts +16 -2
- package/dist/core/backends.d.ts.map +1 -1
- package/dist/core/backends.js +39 -5
- package/dist/core/backends.js.map +1 -1
- package/dist/core/cache-dir.d.ts +17 -0
- package/dist/core/cache-dir.d.ts.map +1 -0
- package/dist/core/cache-dir.js +29 -0
- package/dist/core/cache-dir.js.map +1 -0
- package/dist/core/cleanup.d.ts +19 -4
- package/dist/core/cleanup.d.ts.map +1 -1
- package/dist/core/cleanup.js +14 -27
- package/dist/core/cleanup.js.map +1 -1
- package/dist/core/diagnostics.d.ts +33 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +76 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/errors.d.ts +41 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +56 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/generic-container.d.ts +107 -1
- package/dist/core/generic-container.d.ts.map +1 -1
- package/dist/core/generic-container.js +417 -3
- package/dist/core/generic-container.js.map +1 -1
- package/dist/core/model.d.ts +22 -0
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/network.d.ts.map +1 -1
- package/dist/core/network.js +2 -0
- package/dist/core/network.js.map +1 -1
- package/dist/core/reaper/env.d.ts +14 -0
- package/dist/core/reaper/env.d.ts.map +1 -0
- package/dist/core/reaper/env.js +22 -0
- package/dist/core/reaper/env.js.map +1 -0
- package/dist/core/reaper/init.d.ts +39 -0
- package/dist/core/reaper/init.d.ts.map +1 -0
- package/dist/core/reaper/init.js +140 -0
- package/dist/core/reaper/init.js.map +1 -0
- package/dist/core/reaper/ledger.d.ts +32 -0
- package/dist/core/reaper/ledger.d.ts.map +1 -0
- package/dist/core/reaper/ledger.js +160 -0
- package/dist/core/reaper/ledger.js.map +1 -0
- package/dist/core/reaper/process-liveness.d.ts +39 -0
- package/dist/core/reaper/process-liveness.d.ts.map +1 -0
- package/dist/core/reaper/process-liveness.js +117 -0
- package/dist/core/reaper/process-liveness.js.map +1 -0
- package/dist/core/reaper/run-record.d.ts +48 -0
- package/dist/core/reaper/run-record.d.ts.map +1 -0
- package/dist/core/reaper/run-record.js +58 -0
- package/dist/core/reaper/run-record.js.map +1 -0
- package/dist/core/reaper/sweep.d.ts +23 -0
- package/dist/core/reaper/sweep.d.ts.map +1 -0
- package/dist/core/reaper/sweep.js +76 -0
- package/dist/core/reaper/sweep.js.map +1 -0
- package/dist/core/reaper/watchdog.d.ts +100 -0
- package/dist/core/reaper/watchdog.d.ts.map +1 -0
- package/dist/core/reaper/watchdog.js +412 -0
- package/dist/core/reaper/watchdog.js.map +1 -0
- package/dist/core/reuse/env.d.ts +12 -0
- package/dist/core/reuse/env.d.ts.map +1 -0
- package/dist/core/reuse/env.js +15 -0
- package/dist/core/reuse/env.js.map +1 -0
- package/dist/core/reuse/hash.d.ts +34 -0
- package/dist/core/reuse/hash.d.ts.map +1 -0
- package/dist/core/reuse/hash.js +52 -0
- package/dist/core/reuse/hash.js.map +1 -0
- package/dist/core/reuse/registry.d.ts +50 -0
- package/dist/core/reuse/registry.d.ts.map +1 -0
- package/dist/core/reuse/registry.js +75 -0
- package/dist/core/reuse/registry.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/backend-msb/reaper.d.ts +0 -10
- package/dist/backend-msb/reaper.d.ts.map +0 -1
- package/dist/backend-msb/reaper.js +0 -73
- package/dist/backend-msb/reaper.js.map +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two backend families the reaping ledger can name — deliberately not
|
|
3
|
+
* `SandboxBackend["name"]` (`"microsandbox"` / `"docker"`) verbatim: this is
|
|
4
|
+
* the cross-language wire format every rightsize implementation reads and
|
|
5
|
+
* writes (a Kotlin or Rust process sweeping a dead Node run's ledger parses
|
|
6
|
+
* this same field), so it uses the short, language-neutral spelling the
|
|
7
|
+
* feature spec defines rather than any one language's own backend-name
|
|
8
|
+
* string.
|
|
9
|
+
*/
|
|
10
|
+
export type LedgerBackendKind = "msb" | "docker";
|
|
11
|
+
/**
|
|
12
|
+
* One `runs/<run-id>.json` record: written atomically before this process's
|
|
13
|
+
* first sandbox is created, and read by every OTHER process's sweep to
|
|
14
|
+
* decide whether this run is still alive. Every field here is part of the
|
|
15
|
+
* cross-language contract — a Kotlin or Rust sweep must be able to parse
|
|
16
|
+
* exactly this shape.
|
|
17
|
+
*/
|
|
18
|
+
export interface RunRecord {
|
|
19
|
+
/** This process's OS pid. */
|
|
20
|
+
readonly pid: number;
|
|
21
|
+
/** ISO-8601 instant the *process* started (not when this record was written) — defeats PID reuse. */
|
|
22
|
+
readonly startedIso: string;
|
|
23
|
+
/** Which backend family this run's sandboxes belong to. */
|
|
24
|
+
readonly backend: LedgerBackendKind;
|
|
25
|
+
/** Absolute path to the provisioned `msb` binary this run used. Present only for `backend: "msb"`. */
|
|
26
|
+
readonly msbPath?: string;
|
|
27
|
+
}
|
|
28
|
+
/** `<cacheDir>/runs` — the directory every run's ledger files live under. */
|
|
29
|
+
export declare function runsDir(cacheDir: string): string;
|
|
30
|
+
/** `runs/<run-id>.json` — the run record. */
|
|
31
|
+
export declare function recordPath(cacheDir: string, runId: string): string;
|
|
32
|
+
/** `runs/<run-id>.sandboxes` — one sandbox name per line, append-before-create/remove-after-stop. */
|
|
33
|
+
export declare function sandboxesPath(cacheDir: string, runId: string): string;
|
|
34
|
+
/** `runs/<run-id>.networks` — same protocol as `.sandboxes`, for created networks. */
|
|
35
|
+
export declare function networksPath(cacheDir: string, runId: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Parses a `runs/<run-id>.json` body. Returns `undefined` for anything that
|
|
38
|
+
* isn't a well-shaped record — malformed JSON, a missing/mistyped required
|
|
39
|
+
* field, or a `backend` value this contract doesn't know — rather than
|
|
40
|
+
* throwing, so a sweep can treat "unparseable" as its own liveness case (see
|
|
41
|
+
* the sweep spec: fresh unparseable JSON is skipped, stale unparseable JSON
|
|
42
|
+
* is cleaned) instead of crashing on a record from a future rightsize
|
|
43
|
+
* version.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseRunRecord(text: string): RunRecord | undefined;
|
|
46
|
+
/** Serializes a `RunRecord` for `runs/<run-id>.json`. */
|
|
47
|
+
export declare function serializeRunRecord(record: RunRecord): string;
|
|
48
|
+
//# sourceMappingURL=run-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-record.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/run-record.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,6BAA6B;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,sGAAsG;IACtG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,qGAAqG;AACrG,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAsBlE;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAE5D"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
/** `<cacheDir>/runs` — the directory every run's ledger files live under. */
|
|
3
|
+
export function runsDir(cacheDir) {
|
|
4
|
+
return path.join(cacheDir, "runs");
|
|
5
|
+
}
|
|
6
|
+
/** `runs/<run-id>.json` — the run record. */
|
|
7
|
+
export function recordPath(cacheDir, runId) {
|
|
8
|
+
return path.join(runsDir(cacheDir), `${runId}.json`);
|
|
9
|
+
}
|
|
10
|
+
/** `runs/<run-id>.sandboxes` — one sandbox name per line, append-before-create/remove-after-stop. */
|
|
11
|
+
export function sandboxesPath(cacheDir, runId) {
|
|
12
|
+
return path.join(runsDir(cacheDir), `${runId}.sandboxes`);
|
|
13
|
+
}
|
|
14
|
+
/** `runs/<run-id>.networks` — same protocol as `.sandboxes`, for created networks. */
|
|
15
|
+
export function networksPath(cacheDir, runId) {
|
|
16
|
+
return path.join(runsDir(cacheDir), `${runId}.networks`);
|
|
17
|
+
}
|
|
18
|
+
function isLedgerBackendKind(value) {
|
|
19
|
+
return value === "msb" || value === "docker";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parses a `runs/<run-id>.json` body. Returns `undefined` for anything that
|
|
23
|
+
* isn't a well-shaped record — malformed JSON, a missing/mistyped required
|
|
24
|
+
* field, or a `backend` value this contract doesn't know — rather than
|
|
25
|
+
* throwing, so a sweep can treat "unparseable" as its own liveness case (see
|
|
26
|
+
* the sweep spec: fresh unparseable JSON is skipped, stale unparseable JSON
|
|
27
|
+
* is cleaned) instead of crashing on a record from a future rightsize
|
|
28
|
+
* version.
|
|
29
|
+
*/
|
|
30
|
+
export function parseRunRecord(text) {
|
|
31
|
+
let obj;
|
|
32
|
+
try {
|
|
33
|
+
obj = JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (typeof obj !== "object" || obj === null) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const rec = obj;
|
|
42
|
+
const pid = rec["pid"];
|
|
43
|
+
const startedIso = rec["startedIso"];
|
|
44
|
+
const backend = rec["backend"];
|
|
45
|
+
if (typeof pid !== "number" || typeof startedIso !== "string" || !isLedgerBackendKind(backend)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const msbPath = rec["msbPath"];
|
|
49
|
+
if (msbPath !== undefined && typeof msbPath !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return msbPath === undefined ? { pid, startedIso, backend } : { pid, startedIso, backend, msbPath };
|
|
53
|
+
}
|
|
54
|
+
/** Serializes a `RunRecord` for `runs/<run-id>.json`. */
|
|
55
|
+
export function serializeRunRecord(record) {
|
|
56
|
+
return JSON.stringify(record);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=run-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-record.js","sourceRoot":"","sources":["../../../src/core/reaper/run-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AA+BlC,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CAAC,QAAgB;IACtC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACrC,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,KAAa;IACxD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;AACvD,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,KAAa;IAC3D,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,YAAY,CAAC,CAAC;AAC5D,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,KAAa;IAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,WAAW,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAc;IACzC,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,QAAQ,CAAC;AAC/C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/F,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACtG,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,MAAiB;IAClD,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type LedgerBackendKind } from "./run-record.js";
|
|
2
|
+
import { type ProcessTimeSource } from "./process-liveness.js";
|
|
3
|
+
/** What one sweep pass needs: where the ledger lives, which run/backend to exempt, and how to actually remove a sandbox/network by name. */
|
|
4
|
+
export interface SweepDeps {
|
|
5
|
+
readonly cacheDir: string;
|
|
6
|
+
/** This process's own run id — always skipped, alive or not. */
|
|
7
|
+
readonly thisRunId: string;
|
|
8
|
+
/** This process's active backend — a dead run is only reaped if its recorded backend matches; cross-backend leftovers are left for a process on that backend. */
|
|
9
|
+
readonly backendKind: LedgerBackendKind;
|
|
10
|
+
readonly removeByName: (name: string) => Promise<void>;
|
|
11
|
+
readonly removeNetwork: (networkId: string) => Promise<void>;
|
|
12
|
+
readonly timeSource: ProcessTimeSource;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One full sweep pass: every `runs/*.json` other than this process's own is
|
|
16
|
+
* inspected once. Dead runs whose recorded backend matches this process's
|
|
17
|
+
* active backend are reaped (their sandboxes/networks removed, then their
|
|
18
|
+
* ledger files deleted); alive runs, this run, and cross-backend runs are
|
|
19
|
+
* left untouched. Never throws — a failure reaping one run must not stop
|
|
20
|
+
* the sweep from considering the rest.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sweepOnce(deps: SweepDeps): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/sweep.ts"],"names":[],"mappings":"AAOA,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAK9E,4IAA4I;AAC5I,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iKAAiK;IACjK,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,QAAQ,CAAC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;CACxC;AAoBD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAa9D"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { deleteRunRecordFiles, listRunIds, readNetworkIds, readRunRecordRaw, readSandboxNames, } from "./ledger.js";
|
|
2
|
+
import { parseRunRecord } from "./run-record.js";
|
|
3
|
+
import { isRecordAlive } from "./process-liveness.js";
|
|
4
|
+
/** An unparseable record younger than this is presumed mid-write by its own process, not genuinely corrupt — skipped rather than reaped. */
|
|
5
|
+
const UNPARSEABLE_FRESH_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
6
|
+
async function reapRun(deps, runId) {
|
|
7
|
+
const names = await readSandboxNames(deps.cacheDir, runId);
|
|
8
|
+
for (const name of names) {
|
|
9
|
+
await deps.removeByName(name).catch(() => {
|
|
10
|
+
// Best-effort: "not found" is expected (another sweep may have won the
|
|
11
|
+
// race), and any other failure must not stop the rest of the reap.
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const networkIds = await readNetworkIds(deps.cacheDir, runId);
|
|
15
|
+
for (const networkId of networkIds) {
|
|
16
|
+
await deps.removeNetwork(networkId).catch(() => {
|
|
17
|
+
// Best-effort: an in-use network (another still-alive run sharing it,
|
|
18
|
+
// in principle) is left alone rather than failing the whole sweep.
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
await deleteRunRecordFiles(deps.cacheDir, runId);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One full sweep pass: every `runs/*.json` other than this process's own is
|
|
25
|
+
* inspected once. Dead runs whose recorded backend matches this process's
|
|
26
|
+
* active backend are reaped (their sandboxes/networks removed, then their
|
|
27
|
+
* ledger files deleted); alive runs, this run, and cross-backend runs are
|
|
28
|
+
* left untouched. Never throws — a failure reaping one run must not stop
|
|
29
|
+
* the sweep from considering the rest.
|
|
30
|
+
*/
|
|
31
|
+
export async function sweepOnce(deps) {
|
|
32
|
+
const runIds = await listRunIds(deps.cacheDir);
|
|
33
|
+
for (const runId of runIds) {
|
|
34
|
+
if (runId === deps.thisRunId) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await sweepOneRun(deps, runId);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A single run's sweep failing (a stat race, a malformed removeByName
|
|
42
|
+
// implementation, ...) must not abort the sweep of every other run.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function sweepOneRun(deps, runId) {
|
|
47
|
+
const raw = await readRunRecordRaw(deps.cacheDir, runId);
|
|
48
|
+
if (raw === undefined) {
|
|
49
|
+
// Vanished mid-scan: either this run cleanly shut down or another
|
|
50
|
+
// sweep already reaped it. Nothing left to do.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const record = parseRunRecord(raw.text);
|
|
54
|
+
if (record === undefined) {
|
|
55
|
+
const ageMs = Date.now() - raw.mtimeMs;
|
|
56
|
+
if (ageMs <= UNPARSEABLE_FRESH_AGE_MS) {
|
|
57
|
+
// Could be a torn read of a record still being written by its owning
|
|
58
|
+
// process (writeRunRecord's rename is atomic, but a stat+read pair
|
|
59
|
+
// here is not synchronized with it) — too young to trust as corrupt.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await reapRun(deps, runId);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (record.backend !== deps.backendKind) {
|
|
66
|
+
// A docker process cannot remove msb sandboxes and vice versa — leave
|
|
67
|
+
// this run for a process on its own backend to sweep.
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const alive = await isRecordAlive(deps.timeSource, record.pid, record.startedIso);
|
|
71
|
+
if (alive) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await reapRun(deps, runId);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep.js","sourceRoot":"","sources":["../../../src/core/reaper/sweep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* The argv PREFIXES (a sandbox/network name is appended by the watchdog
|
|
4
|
+
* script itself) a backend's own CLI uses to reap something by name.
|
|
5
|
+
* `stop`/`removeNetwork` may be empty arrays where a backend has no separate
|
|
6
|
+
* stop step (docker's `rm -f` does both) or no native network object to
|
|
7
|
+
* remove (msb emulates networks in-guest; there is nothing to tear down).
|
|
8
|
+
*/
|
|
9
|
+
export interface ReaperKillCommand {
|
|
10
|
+
readonly stop: ReadonlyArray<string>;
|
|
11
|
+
readonly remove: ReadonlyArray<string>;
|
|
12
|
+
readonly removeNetwork: ReadonlyArray<string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function watchdogDir(cacheDir: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* `watchdog-<12 hex of SHA-256(content)>.<ext>` — the filename derives from the
|
|
17
|
+
* script's own bytes. The `reaper/` directory is shared with the sibling rightsize
|
|
18
|
+
* libraries (Kotlin, Rust) and with other versions of this package, each shipping
|
|
19
|
+
* its own script with its own argv contract; content-derived names make it
|
|
20
|
+
* impossible to execute a script whose contract this code doesn't match, and
|
|
21
|
+
* impossible for a write to clobber a script another process's `sh` is executing.
|
|
22
|
+
* Exported for tests.
|
|
23
|
+
*/
|
|
24
|
+
export declare function watchdogScriptFilename(content: string): string;
|
|
25
|
+
/** Writes the platform-appropriate watchdog script under `<cacheDir>/reaper/` (skipped when its content-named file already exists) and returns its path. */
|
|
26
|
+
export declare function ensureWatchdogScript(cacheDir: string): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Paths the debug affordance below writes to when `RIGHTSIZE_REAPER_DEBUG=1`
|
|
29
|
+
* — the watchdog's own stdout/stderr (normally `"ignore"`d) plus a meta log
|
|
30
|
+
* of spawn/error/exit events, all under the same `reaper/` directory the
|
|
31
|
+
* watchdog script itself lives in. Exported so a test can locate them
|
|
32
|
+
* without duplicating the naming scheme.
|
|
33
|
+
*/
|
|
34
|
+
export declare function watchdogDebugPaths(cacheDir: string, runId: string): {
|
|
35
|
+
out: string;
|
|
36
|
+
err: string;
|
|
37
|
+
meta: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Where the Windows script's own in-script trace (see windowsScriptContent's
|
|
41
|
+
* doc) lands: same directory as the run record, named by the watchdog
|
|
42
|
+
* process's own pid — the one piece of the script's identity a caller can
|
|
43
|
+
* know ahead of the script writing anything itself. POSIX has no equivalent
|
|
44
|
+
* (its script has no such tracing; its debug story is the stdout/stderr
|
|
45
|
+
* capture above, which is enough there since `sh -x` isn't needed to tell
|
|
46
|
+
* the difference between "never started" and "died partway").
|
|
47
|
+
*/
|
|
48
|
+
export declare function watchdogTracePath(recordPathValue: string, pid: number): string;
|
|
49
|
+
export interface SpawnWatchdogOptions {
|
|
50
|
+
readonly cacheDir: string;
|
|
51
|
+
readonly sandboxesPath: string;
|
|
52
|
+
readonly networksPath: string;
|
|
53
|
+
readonly recordPath: string;
|
|
54
|
+
readonly killCommand: ReaperKillCommand;
|
|
55
|
+
/**
|
|
56
|
+
* This process's own pid. Used only by the Windows script (see
|
|
57
|
+
* windowsScriptContent's doc for why it polls instead of blocking on
|
|
58
|
+
* stdin EOF); harmless to pass on POSIX, which ignores it.
|
|
59
|
+
*/
|
|
60
|
+
readonly ownerPid: number;
|
|
61
|
+
}
|
|
62
|
+
export interface WatchdogHandle {
|
|
63
|
+
readonly process: ChildProcess;
|
|
64
|
+
/** Closes the held pipe early — test-only, POSIX-only (the Windows script no longer reads stdin at all; production code lets the POSIX process exit close its pipe naturally). */
|
|
65
|
+
closeForTests(): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Spawns the watchdog detached, unref()'d so it never keeps this process's
|
|
69
|
+
* event loop alive.
|
|
70
|
+
*
|
|
71
|
+
* On POSIX, its stdin is a pipe whose write end ONLY this process holds —
|
|
72
|
+
* that write end is `handle.process.stdin`, returned so the caller can
|
|
73
|
+
* keep a live reference for the rest of the process's life (letting it get
|
|
74
|
+
* garbage-collected or explicitly closing it would arm the watchdog
|
|
75
|
+
* early). `detached: true` there just means the standard POSIX
|
|
76
|
+
* survive-your-parent behavior (setsid); this has always worked and this
|
|
77
|
+
* investigation never found a POSIX failure.
|
|
78
|
+
*
|
|
79
|
+
* On Windows, `detached: true` is load-bearing for a completely different
|
|
80
|
+
* reason, proven with a minimal control experiment (a plain `node -e`
|
|
81
|
+
* heartbeat writer — see git history): a NON-detached Windows child is
|
|
82
|
+
* killed by the OS itself the instant this process is torn down, via
|
|
83
|
+
* Windows Job Object semantics (Node/libuv ties a non-detached child to a
|
|
84
|
+
* job object that closes, killing every member, when the owner's own job
|
|
85
|
+
* handle closes) — regardless of what the child is doing. `detached: true`
|
|
86
|
+
* escapes that job and reliably survives (confirmed: 60s+ of continued
|
|
87
|
+
* heartbeats past the owner's death). PowerShell was the original design
|
|
88
|
+
* for the Windows script, but BOTH available hosts (legacy `powershell`
|
|
89
|
+
* and `pwsh`) die within ~200ms of being spawned with `detached: true`,
|
|
90
|
+
* before running a single line — so the Windows watchdog is `node` running
|
|
91
|
+
* a small generated `.js` file instead (see windowsScriptContent), since
|
|
92
|
+
* plain `node` IS proven to survive `detached: true` here. Its stdin is
|
|
93
|
+
* `"ignore"` — it polls for this process's death rather than reading
|
|
94
|
+
* anything, so there is no pipe to leak into any later child this process
|
|
95
|
+
* spawns either. All other stdio is `"ignore"` (or, under
|
|
96
|
+
* `RIGHTSIZE_REAPER_DEBUG`, captured to files) so CI runners never hang on
|
|
97
|
+
* an inherited handle.
|
|
98
|
+
*/
|
|
99
|
+
export declare function spawnWatchdog(opts: SpawnWatchdogOptions): Promise<WatchdogHandle>;
|
|
100
|
+
//# sourceMappingURL=watchdog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/watchdog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,oBAAoB,CAAC;AAMrE;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG9D;AAkND,4JAA4J;AAC5J,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4B5E;AAqBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAO9G;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9E;AAWD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,kLAAkL;IAClL,aAAa,IAAI,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0EvF"}
|