@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/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. A rejection of `onOpen` is the app failing (exit 1), except once a drain or a park's release
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">, 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>;
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. A rejection of `onOpen` is the app failing (exit 1), except once a drain or a park's release
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. Its first failure goes
188
- // to the main thread, once.
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
- function refresh() {
204
- if (stopped) return;
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
- stopped = true;
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
- if (now.dev !== first.dev || now.ino !== first.ino) {
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("its directory is no longer the directory the keeper started on");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcha/pi-durable-disk",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.20",
4
4
  "description": "A durable pi host on a network disk (Archil): one owner per run, a fence, durable commits, and a supervisor.",
5
5
  "keywords": [
6
6
  "pi",