rollbridge 0.1.39 → 0.1.41
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 +23 -9
- package/docs/config.md +37 -13
- package/docs/logging.md +4 -3
- package/docs/troubleshooting.md +7 -5
- package/examples/tensorbuzz.com.js +5 -2
- package/package.json +1 -1
- package/src/cli.js +118 -24
- package/src/config.js +35 -8
- package/src/daemon.js +822 -152
- package/src/guardian-client.js +121 -16
- package/src/managed-process.js +117 -15
- package/src/process-guardian.js +734 -43
- package/src/release-group.js +45 -7
- package/test/completion.test.js +4 -2
- package/test/config-examples.test.js +1 -0
- package/test/config-validation.test.js +22 -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 +1420 -62
- package/test/managed-process.test.js +163 -7
- package/test/owner-recovery.test.js +575 -73
- package/test/owner-replacement.test.js +525 -28
- package/test/release-group.test.js +19 -2
- package/test/release-runtime-retention.test.js +121 -4
- package/test/rollbridge.test.js +263 -13
- package/test/support/process.js +41 -0
package/src/config.js
CHANGED
|
@@ -14,7 +14,7 @@ import {pathToFileURL} from "node:url"
|
|
|
14
14
|
* @typedef {"proxied" | "companion" | "singleton" | "service"} ProcessPolicy
|
|
15
15
|
* @typedef {{backoffFactor: number, maxDelayMs: number, maxRestarts: number | undefined, windowMs: number}} RestartConfig
|
|
16
16
|
* @typedef {{checkIntervalMs: number, limitBytes: number, warnBytes: number}} MemoryConfig
|
|
17
|
-
* @typedef {{activateCommand?: string, drainCommand?: string, drainTimeoutMs: number, quietCommand?: string, stopCommand?: string}} LifecycleConfig
|
|
17
|
+
* @typedef {{activateCommand?: string, drainCommand?: string, drainTimeoutMs: number, quietCommand?: string, reactivateCommand?: string, stopCommand?: string}} LifecycleConfig
|
|
18
18
|
* @typedef {number | "indefinite"} StopTimeoutMs
|
|
19
19
|
* @typedef {"persistent" | "handoff"} ServiceDeployStrategy
|
|
20
20
|
* @typedef {{cwd?: string, deployStrategy: ServiceDeployStrategy, env: Record<string, string>, gracefulStopMs: StopTimeoutMs, health?: HealthConfig, id: string, lifecycle: LifecycleConfig, memory?: MemoryConfig, nonBlockingDrain: boolean, outputLines: number, policy: ProcessPolicy, port?: PortRange, replicas: number, restart: RestartConfig, restartDelayMs: number, stopSignal: string, command: string}} ProcessConfig
|
|
@@ -67,8 +67,12 @@ export async function parseConfigFile(configPath) {
|
|
|
67
67
|
*/
|
|
68
68
|
export async function loadConfig(configPath) {
|
|
69
69
|
const {absolutePath, rawConfig} = await parseConfigFile(configPath)
|
|
70
|
+
const config = normalizeConfig(rawConfig, absolutePath)
|
|
71
|
+
const configDirectory = path.dirname(absolutePath)
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
config.control.path = path.resolve(configDirectory, config.control.path)
|
|
74
|
+
if (config.statePath) config.statePath = path.resolve(configDirectory, config.statePath)
|
|
75
|
+
return config
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
@@ -342,7 +346,7 @@ function normalizeLifecycle(value, key, issues) {
|
|
|
342
346
|
if (value === undefined || value === null) return {drainTimeoutMs: 0}
|
|
343
347
|
|
|
344
348
|
if (!isPlainObject(value)) {
|
|
345
|
-
issues.push({fix: `Set ${key} to a mapping with optional activateCommand, quietCommand, drainCommand, stopCommand, and drainTimeoutMs.`, message: `${key} must be an object`})
|
|
349
|
+
issues.push({fix: `Set ${key} to a mapping with optional activateCommand, quietCommand, reactivateCommand, drainCommand, stopCommand, and drainTimeoutMs.`, message: `${key} must be an object`})
|
|
346
350
|
|
|
347
351
|
return {drainTimeoutMs: 0}
|
|
348
352
|
}
|
|
@@ -351,10 +355,11 @@ function normalizeLifecycle(value, key, issues) {
|
|
|
351
355
|
/** @type {LifecycleConfig} */
|
|
352
356
|
const lifecycle = {drainTimeoutMs: nonNegativeOrDefault(drainTimeoutMs, `${key}.drainTimeoutMs`, issues, 0, false)}
|
|
353
357
|
|
|
354
|
-
if (value.activateCommand !== undefined) lifecycle.activateCommand = normalizeString(value.activateCommand, `${key}.activateCommand`, issues)
|
|
355
|
-
if (value.quietCommand !== undefined) lifecycle.quietCommand = normalizeString(value.quietCommand, `${key}.quietCommand`, issues)
|
|
356
|
-
if (value.
|
|
357
|
-
if (value.
|
|
358
|
+
if (value.activateCommand !== undefined) lifecycle.activateCommand = normalizeString(value.activateCommand, `${key}.activateCommand`, issues, {nonEmpty: true})
|
|
359
|
+
if (value.quietCommand !== undefined) lifecycle.quietCommand = normalizeString(value.quietCommand, `${key}.quietCommand`, issues, {nonEmpty: true})
|
|
360
|
+
if (value.reactivateCommand !== undefined) lifecycle.reactivateCommand = normalizeString(value.reactivateCommand, `${key}.reactivateCommand`, issues, {nonEmpty: true})
|
|
361
|
+
if (value.drainCommand !== undefined) lifecycle.drainCommand = normalizeString(value.drainCommand, `${key}.drainCommand`, issues, {nonEmpty: true})
|
|
362
|
+
if (value.stopCommand !== undefined) lifecycle.stopCommand = normalizeString(value.stopCommand, `${key}.stopCommand`, issues, {nonEmpty: true})
|
|
358
363
|
|
|
359
364
|
if (lifecycle.drainCommand !== undefined && lifecycle.drainTimeoutMs <= 0) {
|
|
360
365
|
issues.push({fix: `Set ${key}.drainTimeoutMs to a positive number to bound ${key}.drainCommand; with 0 the drain step is skipped and the command never runs.`, message: `${key}.drainCommand requires a positive ${key}.drainTimeoutMs`})
|
|
@@ -372,6 +377,7 @@ function normalizeLifecycle(value, key, issues) {
|
|
|
372
377
|
*/
|
|
373
378
|
function validateActivationLifecycle(processes, ownerRecovery, statePath, issues) {
|
|
374
379
|
const activated = processes.filter((processConfig) => processConfig.lifecycle.activateCommand !== undefined)
|
|
380
|
+
const reactivated = processes.filter((processConfig) => processConfig.lifecycle.reactivateCommand !== undefined)
|
|
375
381
|
|
|
376
382
|
if (activated.length > 1) {
|
|
377
383
|
issues.push({fix: "Configure lifecycle.activateCommand on only one release-generation coordinator.", message: "Config may define at most one lifecycle.activateCommand"})
|
|
@@ -386,6 +392,23 @@ function validateActivationLifecycle(processes, ownerRecovery, statePath, issues
|
|
|
386
392
|
}
|
|
387
393
|
}
|
|
388
394
|
|
|
395
|
+
for (const processConfig of reactivated) {
|
|
396
|
+
if (processConfig.policy !== "companion" || !processConfig.nonBlockingDrain) {
|
|
397
|
+
issues.push({fix: `Set lifecycle.reactivateCommand only on a nonBlockingDrain companion; "${processConfig.id}" is ${processConfig.policy} with nonBlockingDrain: ${processConfig.nonBlockingDrain}.`, message: `Process "${processConfig.id}" can only set lifecycle.reactivateCommand on a nonBlockingDrain companion`})
|
|
398
|
+
}
|
|
399
|
+
if (!processConfig.lifecycle.quietCommand) {
|
|
400
|
+
issues.push({fix: `Add lifecycle.quietCommand to "${processConfig.id}" so worker reactivation has a paired retirement command.`, message: `Process "${processConfig.id}" lifecycle.reactivateCommand requires lifecycle.quietCommand`})
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (activated.length > 0) {
|
|
405
|
+
for (const processConfig of processes) {
|
|
406
|
+
if (processConfig.nonBlockingDrain && processConfig.lifecycle.quietCommand && !processConfig.lifecycle.reactivateCommand) {
|
|
407
|
+
issues.push({fix: `Add lifecycle.reactivateCommand to "${processConfig.id}" so failed candidate recovery reverses its external quiet hook.`, message: `Process "${processConfig.id}" lifecycle.quietCommand requires lifecycle.reactivateCommand during generation activation recovery`})
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
389
412
|
if (activated.length > 0 && (!ownerRecovery || !statePath)) {
|
|
390
413
|
issues.push({fix: "Configure statePath and ownerRecovery for durable release-generation transition recovery.", message: "lifecycle.activateCommand requires ownerRecovery and statePath"})
|
|
391
414
|
}
|
|
@@ -849,7 +872,7 @@ function normalizePortRange(value, key, issues) {
|
|
|
849
872
|
* @param {JsonValue} value - Raw value.
|
|
850
873
|
* @param {string} key - Config key.
|
|
851
874
|
* @param {ConfigIssue[]} issues - Issue collector.
|
|
852
|
-
* @param {{default?: string}} [options] - Options.
|
|
875
|
+
* @param {{default?: string, nonEmpty?: boolean}} [options] - Options.
|
|
853
876
|
* @returns {string} Normalized string, or a placeholder when invalid.
|
|
854
877
|
*/
|
|
855
878
|
function normalizeString(value, key, issues, options = {}) {
|
|
@@ -867,6 +890,10 @@ function normalizeString(value, key, issues, options = {}) {
|
|
|
867
890
|
return options.default ?? ""
|
|
868
891
|
}
|
|
869
892
|
|
|
893
|
+
if (options.nonEmpty && !value.trim()) {
|
|
894
|
+
issues.push({fix: `Set ${key} to a non-empty command string.`, message: `${key} must not be empty`})
|
|
895
|
+
}
|
|
896
|
+
|
|
870
897
|
return value
|
|
871
898
|
}
|
|
872
899
|
|