rollbridge 0.1.38 → 0.1.40
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 +22 -4
- package/changelog.d/20260830-guardian-retired-replacement-process-key.md +12 -1
- package/changelog.d/20260830-release-generation-activation-lifecycle.md +16 -2
- package/changelog.d/20260830055159-guardian-daemon-restart.md +2 -0
- package/docs/cli.md +33 -9
- package/docs/config.md +37 -13
- package/docs/logging.md +4 -3
- package/docs/troubleshooting.md +7 -5
- package/package.json +1 -1
- package/src/cli.js +100 -26
- package/src/config.js +14 -6
- package/src/daemon.js +741 -153
- package/src/guardian-client.js +68 -16
- package/src/managed-process.js +55 -8
- package/src/process-guardian.js +731 -43
- package/src/release-group.js +77 -6
- package/test/config-validation.test.js +4 -0
- package/test/fixtures/guardian-recovery-owner.js +86 -0
- package/test/fixtures/pre-split3-process-guardian.js +14 -0
- package/test/guardian-client.test.js +1339 -62
- package/test/managed-process.test.js +136 -7
- package/test/owner-recovery.test.js +814 -52
- package/test/owner-replacement.test.js +526 -23
- package/test/release-group.test.js +19 -2
- package/test/release-runtime-retention.test.js +121 -4
- package/test/rollbridge.test.js +21 -0
- package/test/support/process.js +41 -0
|
@@ -66,6 +66,26 @@ test("keeps every output line when fewer than the retention limit are produced",
|
|
|
66
66
|
assert.deepEqual(logs.map((entry) => entry.line), ["one", "two"])
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
+
test("reassembles output lines split across stream chunks", () => {
|
|
70
|
+
const managed = buildProcess(50)
|
|
71
|
+
|
|
72
|
+
managed.appendLog("stdout", "8191:x")
|
|
73
|
+
assert.deepEqual(managed.status().logs, [])
|
|
74
|
+
|
|
75
|
+
managed.appendLog("stdout", "xx\n")
|
|
76
|
+
assert.deepEqual(managed.status().logs.map((entry) => entry.line), ["8191:xxx"])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("bounds an output fragment that never terminates", () => {
|
|
80
|
+
const managed = buildProcess(50)
|
|
81
|
+
const fragment = "x".repeat(1024 * 1024)
|
|
82
|
+
|
|
83
|
+
managed.appendLog("stdout", fragment)
|
|
84
|
+
|
|
85
|
+
assert.equal(managed.status().logs.length, 15)
|
|
86
|
+
assert.equal(managed.outputBuffers.stdout.length, 64 * 1024)
|
|
87
|
+
})
|
|
88
|
+
|
|
69
89
|
test("emits each output line after retaining it", () => {
|
|
70
90
|
const managed = buildProcess(50)
|
|
71
91
|
let observed
|
|
@@ -198,6 +218,27 @@ test("records the manual start reason", async () => {
|
|
|
198
218
|
}
|
|
199
219
|
})
|
|
200
220
|
|
|
221
|
+
test("a later stop cancels a start queued behind an in-flight stop", async () => {
|
|
222
|
+
const managed = buildProcess(50)
|
|
223
|
+
let finishStop = () => {}
|
|
224
|
+
|
|
225
|
+
managed.command = `${JSON.stringify(process.execPath)} -e ${JSON.stringify("setInterval(() => {}, 1000)")}`
|
|
226
|
+
managed.stopPromise = new Promise((resolve) => {
|
|
227
|
+
finishStop = resolve
|
|
228
|
+
})
|
|
229
|
+
const queuedStart = managed.start()
|
|
230
|
+
const finalStop = managed.stop()
|
|
231
|
+
|
|
232
|
+
finishStop()
|
|
233
|
+
await Promise.all([queuedStart, finalStop])
|
|
234
|
+
try {
|
|
235
|
+
assert.equal(managed.status().pid, undefined)
|
|
236
|
+
assert.equal(managed.status().state, "stopped")
|
|
237
|
+
} finally {
|
|
238
|
+
await managed.stop()
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
201
242
|
test("does not record a start reason when the spawn fails", async () => {
|
|
202
243
|
const managed = new ManagedProcess({
|
|
203
244
|
command: "true",
|
|
@@ -404,18 +445,106 @@ test("activateStrict runs the configured activation command once per call and re
|
|
|
404
445
|
const commands = /** @type {{command: string, label: string, pid: number | undefined, timeoutMs: number}[]} */ ([])
|
|
405
446
|
const managed = buildLongLived(() => false)
|
|
406
447
|
|
|
448
|
+
try {
|
|
449
|
+
await managed.start()
|
|
450
|
+
const pid = managed.pid
|
|
451
|
+
|
|
452
|
+
assert.ok(pid)
|
|
453
|
+
managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
|
|
454
|
+
managed.runHook = async (command, timeoutMs, label, hookPid) => {
|
|
455
|
+
commands.push({command, label, pid: hookPid, timeoutMs})
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await managed.activateStrict()
|
|
460
|
+
assert.deepEqual(commands, [{command: "jobs activate", label: "activate command", pid, timeoutMs: 30000}])
|
|
461
|
+
|
|
462
|
+
managed.runHook = async () => new Error("activation rejected")
|
|
463
|
+
await assert.rejects(() => managed.activateStrict(), /activation rejected/)
|
|
464
|
+
} finally {
|
|
465
|
+
await managed.stop()
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test("activateStrict rejects when the activated process is replaced while its hook runs", async () => {
|
|
470
|
+
const managed = buildLongLived(() => false)
|
|
471
|
+
|
|
472
|
+
await managed.start()
|
|
473
|
+
const child = managed.child
|
|
474
|
+
const pid = managed.pid
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
|
|
478
|
+
managed.lifecycleRole = "candidate"
|
|
479
|
+
managed.runHook = async () => {
|
|
480
|
+
managed.child = undefined
|
|
481
|
+
managed.pid = undefined
|
|
482
|
+
return undefined
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
await assert.rejects(() => managed.activateStrict(), /exited before activation completed/)
|
|
486
|
+
assert.equal(managed.lifecycleRole, "candidate")
|
|
487
|
+
} finally {
|
|
488
|
+
managed.child = child
|
|
489
|
+
managed.pid = pid
|
|
490
|
+
await managed.stop()
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test("activateStrict rejects an activation request when its process is not running", async () => {
|
|
495
|
+
const managed = buildLongLived(() => false)
|
|
496
|
+
let hookRan = false
|
|
497
|
+
|
|
407
498
|
managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0}
|
|
408
|
-
managed.
|
|
409
|
-
|
|
410
|
-
commands.push({command, label, pid, timeoutMs})
|
|
499
|
+
managed.runHook = async () => {
|
|
500
|
+
hookRan = true
|
|
411
501
|
return undefined
|
|
412
502
|
}
|
|
413
503
|
|
|
414
|
-
await managed.activateStrict()
|
|
415
|
-
assert.
|
|
504
|
+
await assert.rejects(() => managed.activateStrict(), /is not running for activation/)
|
|
505
|
+
assert.equal(hookRan, false)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test("quiesce waits for active-role restoration before retiring a restarted process", async () => {
|
|
509
|
+
const managed = buildLongLived(() => false)
|
|
510
|
+
const hooks = /** @type {string[]} */ ([])
|
|
511
|
+
/** @type {() => void} */
|
|
512
|
+
let allowActivation = () => {}
|
|
513
|
+
/** @type {() => void} */
|
|
514
|
+
let markActivationStarted = () => {}
|
|
515
|
+
const activationAllowed = new Promise((resolve) => { allowActivation = () => resolve(undefined) })
|
|
516
|
+
const activationStarted = new Promise((resolve) => { markActivationStarted = () => resolve(undefined) })
|
|
517
|
+
|
|
518
|
+
managed.lifecycle = {activateCommand: "jobs activate", drainTimeoutMs: 0, quietCommand: "jobs retire"}
|
|
519
|
+
managed.runHook = async (_command, _timeoutMs, label) => {
|
|
520
|
+
if (label === "activate command") {
|
|
521
|
+
hooks.push("activate:start")
|
|
522
|
+
markActivationStarted()
|
|
523
|
+
await activationAllowed
|
|
524
|
+
hooks.push("activate:end")
|
|
525
|
+
} else {
|
|
526
|
+
hooks.push("quiet")
|
|
527
|
+
}
|
|
528
|
+
return undefined
|
|
529
|
+
}
|
|
530
|
+
const start = assert.rejects(() => managed.start("crash", "active"), /quiesced before lifecycle role active was restored/)
|
|
416
531
|
|
|
417
|
-
|
|
418
|
-
|
|
532
|
+
try {
|
|
533
|
+
await activationStarted
|
|
534
|
+
const quiesce = managed.quiesceStrict()
|
|
535
|
+
|
|
536
|
+
await Promise.resolve()
|
|
537
|
+
assert.deepEqual(hooks, ["activate:start"], "retirement must not race ahead of role restoration")
|
|
538
|
+
allowActivation()
|
|
539
|
+
await Promise.all([start, quiesce])
|
|
540
|
+
assert.deepEqual(hooks, ["activate:start", "activate:end", "quiet"])
|
|
541
|
+
assert.equal(managed.status().state, "quiesced")
|
|
542
|
+
assert.equal(managed.lifecycleRole, "retired")
|
|
543
|
+
} finally {
|
|
544
|
+
allowActivation()
|
|
545
|
+
await start.catch(() => {})
|
|
546
|
+
await managed.stop()
|
|
547
|
+
}
|
|
419
548
|
})
|
|
420
549
|
|
|
421
550
|
test("sends the configured stopSignal as the graceful stop signal", async () => {
|