@parcha/pi-durable-disk 0.1.0-beta.18 → 0.1.0-beta.20
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/CHANGELOG.md +31 -0
- package/README.md +176 -892
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +27 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.js +3 -0
- package/dist/status.js +19 -8
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -56,12 +56,14 @@ export interface InstanceLife {
|
|
|
56
56
|
* A drain stops new submissions and waits up to `life.drainMs` for running work; with `resume` it then writes the run
|
|
57
57
|
* sleeping with its wake (the deadline its work waits for, null when idle and idle parking is on, otherwise now), so
|
|
58
58
|
* the supervisor starts it again and a deploy never strands it; a wake hook that refuses falls back to a due wake.
|
|
59
|
-
* `pause` seals it paused.
|
|
59
|
+
* `pause` seals it paused. An app that released the run itself (`run.release()`, in `onOpen` or after it) has nothing left to
|
|
60
|
+
* serve: the instance exits 0 when `onOpen` returns, and a stop after that exits 0 without a drain, since the Harness is
|
|
61
|
+
* closed and the run's status final. A rejection of `onOpen` is the app failing (exit 1), except once a drain or a park's release
|
|
60
62
|
* began: closing the Harness under the app makes its work reject ("Session is closed"), and ending the process there
|
|
61
63
|
* would cut the release before its unmount (a container's FUSE daemon dies with the process, leaving the delegation
|
|
62
64
|
* orphaned).
|
|
63
65
|
*/
|
|
64
|
-
export declare function serveUntilDone(run: Pick<DurableRun, "setStatus" | "release" | "record"
|
|
66
|
+
export declare function serveUntilDone(run: Pick<DurableRun, "setStatus" | "release" | "record"> & Partial<Pick<DurableRun, "releasing">>, onOpen: ((run: DurableRun) => void | Promise<void>) | undefined, onSigterm: string | undefined, log: (event: string, extra?: Record<string, unknown>) => void, signals?: Pick<NodeJS.EventEmitter, "once">, life?: InstanceLife): Promise<number>;
|
|
65
67
|
/**
|
|
66
68
|
* The flags every instance this supervisor starts gets after `run`: `--app MODULE` (made absolute, since the unit's working
|
|
67
69
|
* directory is not the supervisor's) and any `--run-arg`, in that order.
|
package/dist/cli.js
CHANGED
|
@@ -285,7 +285,9 @@ async function runInstance(values) {
|
|
|
285
285
|
* A drain stops new submissions and waits up to `life.drainMs` for running work; with `resume` it then writes the run
|
|
286
286
|
* sleeping with its wake (the deadline its work waits for, null when idle and idle parking is on, otherwise now), so
|
|
287
287
|
* the supervisor starts it again and a deploy never strands it; a wake hook that refuses falls back to a due wake.
|
|
288
|
-
* `pause` seals it paused.
|
|
288
|
+
* `pause` seals it paused. An app that released the run itself (`run.release()`, in `onOpen` or after it) has nothing left to
|
|
289
|
+
* serve: the instance exits 0 when `onOpen` returns, and a stop after that exits 0 without a drain, since the Harness is
|
|
290
|
+
* closed and the run's status final. A rejection of `onOpen` is the app failing (exit 1), except once a drain or a park's release
|
|
289
291
|
* began: closing the Harness under the app makes its work reject ("Session is closed"), and ending the process there
|
|
290
292
|
* would cut the release before its unmount (a container's FUSE daemon dies with the process, leaving the delegation
|
|
291
293
|
* orphaned).
|
|
@@ -305,11 +307,25 @@ export function serveUntilDone(run, onOpen, onSigterm, log, signals = process, l
|
|
|
305
307
|
let releasing = false;
|
|
306
308
|
let parking;
|
|
307
309
|
const sleeping = (at) => run.setStatus("sleeping", { reason: "drained" }, { wakeAt: at === null ? null : new Date(at).toISOString() });
|
|
310
|
+
// The app released the run: wait for its release to end (a second `release()` joins it) and leave.
|
|
311
|
+
const leaveReleased = async (event) => {
|
|
312
|
+
try {
|
|
313
|
+
await run.release();
|
|
314
|
+
log("released", { ...event, record: run.record });
|
|
315
|
+
done(0);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
log("release failed", { code: err.code, message: err.message });
|
|
319
|
+
done(exitCodeFor(err));
|
|
320
|
+
}
|
|
321
|
+
};
|
|
308
322
|
const shutdown = async (signal) => {
|
|
309
323
|
if (releasing)
|
|
310
324
|
return;
|
|
311
325
|
releasing = true;
|
|
312
326
|
log("draining", { signal, onSigterm, drainMs });
|
|
327
|
+
if (run.releasing === true)
|
|
328
|
+
return leaveReleased({ drained: "released" });
|
|
313
329
|
try {
|
|
314
330
|
if (await parking?.stop())
|
|
315
331
|
return done(0);
|
|
@@ -358,6 +374,12 @@ export function serveUntilDone(run, onOpen, onSigterm, log, signals = process, l
|
|
|
358
374
|
// onOpen may return at once (work submitted) or run for the instance's life.
|
|
359
375
|
Promise.resolve()
|
|
360
376
|
.then(() => onOpen?.(durable))
|
|
377
|
+
.then(() => {
|
|
378
|
+
if (releasing || run.releasing !== true)
|
|
379
|
+
return;
|
|
380
|
+
releasing = true;
|
|
381
|
+
void leaveReleased({ drained: "app" });
|
|
382
|
+
})
|
|
361
383
|
.catch((err) => {
|
|
362
384
|
if (releasing)
|
|
363
385
|
return;
|
|
@@ -642,6 +664,10 @@ async function forkRun(values) {
|
|
|
642
664
|
const COMMON = { disk: { type: "string" }, region: { type: "string" }, id: { type: "string" }, "mount-root": { type: "string" }, "api-key-env": { type: "string" }, "host-name": { type: "string" } };
|
|
643
665
|
export async function main(argv) {
|
|
644
666
|
const [command, ...rest] = argv;
|
|
667
|
+
if (command === "--help" || command === "-h" || command === "help" || (command !== undefined && rest[0] === "--help")) {
|
|
668
|
+
process.stdout.write(`${USAGE}\n`);
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
645
671
|
try {
|
|
646
672
|
switch (command) {
|
|
647
673
|
case "run":
|
package/dist/run.d.ts
CHANGED
|
@@ -277,6 +277,8 @@ export interface DurableRun {
|
|
|
277
277
|
/** `run.json` as last written. */
|
|
278
278
|
readonly record: RunRecord;
|
|
279
279
|
readonly fenced: boolean;
|
|
280
|
+
/** True once `release()` began: the Harness is closed, the status is final, and the instance has nothing left to serve. */
|
|
281
|
+
readonly releasing: boolean;
|
|
280
282
|
/** Write a status transition. `done` and `failed` run the barrier first, so a terminal status never names workspace files that are not durable. */
|
|
281
283
|
setStatus(status: RunStatus, detail?: JsonValue | null, options?: {
|
|
282
284
|
readonly wakeAt?: string | null;
|
package/dist/run.js
CHANGED
|
@@ -754,6 +754,9 @@ class Run {
|
|
|
754
754
|
await this.#store.storage.close(this.#context);
|
|
755
755
|
});
|
|
756
756
|
}
|
|
757
|
+
get releasing() {
|
|
758
|
+
return this.#releasing !== undefined || this.#released;
|
|
759
|
+
}
|
|
757
760
|
async setStatus(status, detail = null, options = {}) {
|
|
758
761
|
if (!this.#lease.fenced && (this.#released || this.#releasing))
|
|
759
762
|
throw new RunError("RUN_RELEASED", `run ${this.ref.id} is released`);
|
package/dist/status.js
CHANGED
|
@@ -184,8 +184,9 @@ export function parseStartMark(text) {
|
|
|
184
184
|
// watchdog's is (src/watchdog.ts says why). One refresh at a time, all synchronous: a stop (the message) is handled only
|
|
185
185
|
// between two refreshes, so none is still writing once the thread has exited. It writes only while `dir` is still the
|
|
186
186
|
// directory it first saw (its device and inode, read on this thread, so a stalled mount never holds the caller's): a path
|
|
187
|
-
// that names another (a descriptor number reused) or none is never written, and the keeper stops.
|
|
188
|
-
//
|
|
187
|
+
// that names another (a descriptor number reused) or none is never written, and the keeper stops. A rewrite that fails is
|
|
188
|
+
// checked the same way, so a directory that moved between the check and the write is reported as moved, not as a failed
|
|
189
|
+
// write. Its first failure goes to the main thread, once.
|
|
189
190
|
const KEEPER_SOURCE = `"use strict";
|
|
190
191
|
const fs = require("node:fs");
|
|
191
192
|
const path = require("node:path");
|
|
@@ -200,19 +201,23 @@ const report = (message) => {
|
|
|
200
201
|
reported = true;
|
|
201
202
|
parentPort.postMessage(message);
|
|
202
203
|
};
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// Why dir is no longer the directory the keeper first saw, or null while it is (the first call records it).
|
|
205
|
+
function moved() {
|
|
205
206
|
let now;
|
|
206
207
|
try {
|
|
207
208
|
now = fs.statSync(dir, { bigint: true });
|
|
208
209
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
return report("its directory cannot be reached (" + (error.code || error.message) + "), so it is no longer the directory the keeper started on");
|
|
210
|
+
return "its directory cannot be reached (" + (error.code || error.message) + "), so it is no longer the directory the keeper started on";
|
|
211
211
|
}
|
|
212
212
|
first ??= { dev: now.dev, ino: now.ino };
|
|
213
|
-
|
|
213
|
+
return now.dev === first.dev && now.ino === first.ino ? null : "its directory is no longer the directory the keeper started on";
|
|
214
|
+
}
|
|
215
|
+
function refresh() {
|
|
216
|
+
if (stopped) return;
|
|
217
|
+
const away = moved();
|
|
218
|
+
if (away) {
|
|
214
219
|
stopped = true;
|
|
215
|
-
return report(
|
|
220
|
+
return report(away);
|
|
216
221
|
}
|
|
217
222
|
let text;
|
|
218
223
|
try {
|
|
@@ -245,6 +250,12 @@ function refresh() {
|
|
|
245
250
|
fs.closeSync(handle);
|
|
246
251
|
}
|
|
247
252
|
} catch (error) {
|
|
253
|
+
// The directory may have moved after the check above: then that is the reason, and the keeper stops.
|
|
254
|
+
const away = moved();
|
|
255
|
+
if (away) {
|
|
256
|
+
stopped = true;
|
|
257
|
+
return report(away);
|
|
258
|
+
}
|
|
248
259
|
report("rewriting it failed (" + (error.code || error.message) + ")");
|
|
249
260
|
}
|
|
250
261
|
}
|
package/package.json
CHANGED