feat(queue): queue as data A2, migration, render and dispatch (#1508)
Filbert approved round 1 (f167b85e). Manifest 782bcb62, 21 files, plus the QUEUE.md markers and the TOOLS.md section. Lead decision 35. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -109,6 +109,36 @@ test("an error after the link releases the lock: unreadable gate, failing temp s
|
||||
assert.deepEqual(readdirSync(d), []);
|
||||
});
|
||||
|
||||
// P1: a release that throws on either gate path becomes a message naming the
|
||||
// lock left behind, and the gate-present refusal reports what release found.
|
||||
test("a release that fails on a gate path is reported, never a stack trace (P1)", (t) => {
|
||||
const d = dir(t);
|
||||
const lock = join(d, LOCK_NAME);
|
||||
const eacces = () => { throw Object.assign(new Error("denied"), { code: "EACCES" }); };
|
||||
const noUnlink = { ...realIo, unlink: (p) => (p === lock ? eacces() : realIo.unlink(p)) };
|
||||
// The gate is present.
|
||||
writeFileSync(join(d, GATE_NAME), record({ verb: "unlock", op: null }));
|
||||
refused(() => acquire({ gitDir: d, io: noUnlink, verb: "move" }),
|
||||
/unlock gate .* is present \(live: .*--check-gate`\nwarning: cannot release the queue lock \(EACCES\); .*mosaic-queue\.lock may be left in place$/);
|
||||
assert.equal(existsSync(lock), true);
|
||||
rmSync(lock);
|
||||
// The gate can't be read.
|
||||
const both = { ...noUnlink, readFile: (p) => (p.endsWith(GATE_NAME) ? eacces() : realIo.readFile(p)) };
|
||||
refused(() => acquire({ gitDir: d, io: both, verb: "move" }),
|
||||
/cannot check the unlock gate .*EACCES; cannot release the queue lock \(EACCES\); .*mosaic-queue\.lock may be left in place$/, 1);
|
||||
assert.equal(existsSync(lock), true);
|
||||
rmSync(lock);
|
||||
// The gate is present and the lock was swapped: release's own message is kept.
|
||||
const swap = (name) => { if (name === "lock-linked") { writeFileSync(`${lock}.copy`, readFileSync(lock)); renameSync(`${lock}.copy`, lock); } };
|
||||
refused(() => acquire({ gitDir: d, io: realIo, verb: "move", hook: swap }),
|
||||
/is present \(live: .*\nwarning: lock .*mosaic-queue\.lock is not the one this process took; left in place$/);
|
||||
assert.equal(existsSync(lock), true);
|
||||
rmSync(lock);
|
||||
// Nothing wrong: the refusal carries no warning.
|
||||
refused(() => acquire({ gitDir: d, io: realIo, verb: "move" }), /--check-gate`$/);
|
||||
assert.deepEqual(readdirSync(d), [GATE_NAME]);
|
||||
});
|
||||
|
||||
test("a paused holder: another writer waits 10 s, then refuses naming it live", async (t) => {
|
||||
const d = dir(t);
|
||||
const child = spawn(process.execPath, [join(HERE, "fixtures", "lock-child.mjs"), d, "hold"], { stdio: ["ignore", "pipe", "ignore"] });
|
||||
@@ -120,7 +150,7 @@ test("a paused holder: another writer waits 10 s, then refuses naming it live",
|
||||
child.kill("SIGKILL");
|
||||
await new Promise((r) => child.on("exit", r));
|
||||
refused(() => acquire({ gitDir: d, io: realIo, verb: "move", waitMs: 0 }), /owner is dead: pid .*; run `scripts\/mosaic queue unlock` once nothing is running/);
|
||||
assert.match(unlock({ gitDir: d, io: realIo }), /^removed queue lock \(dead: pid/);
|
||||
assert.match(unlock({ gitDir: d, io: realIo }).result, /^removed queue lock \(dead: pid/);
|
||||
release(acquire({ gitDir: d, io: realIo, verb: "move", waitMs: 0 }), realIo);
|
||||
});
|
||||
|
||||
@@ -136,7 +166,8 @@ test("two concurrent unlockers: the second refuses on the gate", async (t) => {
|
||||
},
|
||||
});
|
||||
assert.match(inner.message, /unlock gate .* is held \(live: pid \d+, unlock since/);
|
||||
assert.match(outer, /^removed queue lock \(dead/);
|
||||
assert.match(outer.result, /^removed queue lock \(dead/);
|
||||
assert.equal(outer.warning, null);
|
||||
assert.equal(existsSync(join(d, GATE_NAME)), false);
|
||||
});
|
||||
|
||||
@@ -166,7 +197,7 @@ test("a writer publishing during an unlock, gate first: the writer releases and
|
||||
},
|
||||
});
|
||||
assert.match(writerErr.message, /unlock gate .* is present \(live: .*unlock/);
|
||||
assert.equal(out, "no queue lock present; nothing removed");
|
||||
assert.deepEqual(out, { result: "no queue lock present; nothing removed", warning: null });
|
||||
assert.deepEqual(readdirSync(d), []);
|
||||
});
|
||||
|
||||
@@ -176,7 +207,8 @@ test("a gate swapped while held is left in place and reported, on success and on
|
||||
// A copy renamed over the gate: same bytes, a new inode.
|
||||
const swap = () => { writeFileSync(`${gate}.copy`, readFileSync(gate)); renameSync(`${gate}.copy`, gate); };
|
||||
const out = unlock({ gitDir: d, io: realIo, hook: (name) => { if (name === "gate-held") swap(); } });
|
||||
assert.match(out, /^no queue lock present; nothing removed\nwarning: lock .*mosaic-queue\.unlock is not the one this process took; left in place$/);
|
||||
assert.equal(out.result, "no queue lock present; nothing removed");
|
||||
assert.match(out.warning, /^lock .*mosaic-queue\.unlock is not the one this process took; left in place$/);
|
||||
assert.equal(existsSync(gate), true);
|
||||
rmSync(gate);
|
||||
writeFileSync(join(d, LOCK_NAME), record({}));
|
||||
@@ -194,7 +226,7 @@ test("a reused pid within one boot is mismatch; unlock removes the lock and neve
|
||||
writeFileSync(join(d, LOCK_NAME), record({ pid: s.pid, start: wrong }));
|
||||
assert.equal(classify(readFileSync(join(d, LOCK_NAME)), realProc).state, "mismatch");
|
||||
refused(() => acquire({ gitDir: d, io: realIo, verb: "move", waitMs: 0 }), /mismatch: .*was reused/);
|
||||
assert.match(unlock({ gitDir: d, io: realIo }), /removed queue lock \(mismatch/);
|
||||
assert.match(unlock({ gitDir: d, io: realIo }).result, /removed queue lock \(mismatch/);
|
||||
assert.equal(s.exitCode, null);
|
||||
assert.equal(processStart(s.pid), start, "the process still runs, unsignalled");
|
||||
});
|
||||
@@ -258,7 +290,7 @@ test("a delayed release by a dead owner, after unlock and a new owner: the inode
|
||||
const a = acquire({ gitDir: d, io: realIo, op: "owner-a-op", verb: "move" });
|
||||
// Unlock judges A dead (as it would after a crash); B then takes the lock.
|
||||
const judge = { ...realProc, pidAlive: () => false };
|
||||
assert.match(unlock({ gitDir: d, io: realIo, proc: judge }), /removed queue lock \(dead/);
|
||||
assert.match(unlock({ gitDir: d, io: realIo, proc: judge }).result, /removed queue lock \(dead/);
|
||||
const b = acquire({ gitDir: d, io: realIo, op: "owner-b-op", verb: "move" });
|
||||
assert.match(release(a, realIo), /is not the one this process took; left in place/);
|
||||
assert.match(readFileSync(join(d, LOCK_NAME), "utf8"), /owner-b-op/);
|
||||
@@ -279,7 +311,7 @@ test("release checks the inode too: a byte-identical lock file with a new inode
|
||||
|
||||
test("unlock refuses a live, unknown or invalid lock, and does nothing without one", async (t) => {
|
||||
const d = dir(t);
|
||||
assert.equal(unlock({ gitDir: d, io: realIo }), "no queue lock present; nothing removed");
|
||||
assert.deepEqual(unlock({ gitDir: d, io: realIo }), { result: "no queue lock present; nothing removed", warning: null });
|
||||
writeFileSync(join(d, LOCK_NAME), "");
|
||||
refused(() => unlock({ gitDir: d, io: realIo }), /owner is invalid/);
|
||||
writeFileSync(join(d, LOCK_NAME), record());
|
||||
|
||||
Reference in New Issue
Block a user