heyio 0.24.2 → 0.26.0
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/config.js +1 -0
- package/dist/daemon.js +7 -0
- package/dist/watchdog.js +51 -0
- package/dist/watchdog.test.js +83 -0
- package/package.json +1 -1
- package/web-dist/assets/{index-CF9f3i0T.js → index-BeCJ4SRf.js} +16 -16
- package/web-dist/index.html +1 -1
package/dist/config.js
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ import { backfillReviewVerdicts } from "./copilot/review-backfill.js";
|
|
|
13
13
|
import { startScheduler, stopScheduler } from "./copilot/scheduler.js";
|
|
14
14
|
import { startIoScheduler, stopIoScheduler } from "./copilot/io-scheduler.js";
|
|
15
15
|
import { config } from "./config.js";
|
|
16
|
+
import { startWatchdog } from "./watchdog.js";
|
|
16
17
|
import { ensureWikiStructure } from "./wiki/fs.js";
|
|
17
18
|
import { autoUpdate } from "./update.js";
|
|
18
19
|
import { readdirSync, statSync, rmSync } from "fs";
|
|
@@ -129,6 +130,12 @@ export async function startDaemon() {
|
|
|
129
130
|
// Daily cleanup — prune schedule runs and notifications older than 30 days
|
|
130
131
|
const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
131
132
|
const PRUNE_RETENTION_DAYS = 30;
|
|
133
|
+
// Start event loop watchdog
|
|
134
|
+
let stopWatchdog;
|
|
135
|
+
if (config.watchdogEnabled) {
|
|
136
|
+
stopWatchdog = startWatchdog();
|
|
137
|
+
console.error("[io] Event loop watchdog started");
|
|
138
|
+
}
|
|
132
139
|
const pruneTimer = setInterval(() => {
|
|
133
140
|
try {
|
|
134
141
|
const runsDeleted = pruneOldScheduleRuns(PRUNE_RETENTION_DAYS);
|
package/dist/watchdog.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon liveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* Uses a periodic setInterval to detect event loop stalls. If the interval
|
|
5
|
+
* fires significantly late, the event loop was blocked for that duration.
|
|
6
|
+
*
|
|
7
|
+
* - Warning logged if stall exceeds WARN_THRESHOLD_MS (30s)
|
|
8
|
+
* - Process exits if stall exceeds FATAL_THRESHOLD_MS (60s), relying on
|
|
9
|
+
* the process supervisor to restart
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CHECK_INTERVAL_MS = 30_000;
|
|
12
|
+
const DEFAULT_WARN_THRESHOLD_MS = 30_000;
|
|
13
|
+
const DEFAULT_FATAL_THRESHOLD_MS = 60_000;
|
|
14
|
+
let timer = null;
|
|
15
|
+
let lastTick = 0;
|
|
16
|
+
/**
|
|
17
|
+
* Start the event loop watchdog. Returns a stop function.
|
|
18
|
+
*/
|
|
19
|
+
export function startWatchdog(opts = {}) {
|
|
20
|
+
const checkInterval = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
21
|
+
const warnThreshold = opts.warnThresholdMs ?? DEFAULT_WARN_THRESHOLD_MS;
|
|
22
|
+
const fatalThreshold = opts.fatalThresholdMs ?? DEFAULT_FATAL_THRESHOLD_MS;
|
|
23
|
+
lastTick = Date.now();
|
|
24
|
+
timer = setInterval(() => {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const elapsed = now - lastTick;
|
|
27
|
+
const stallMs = elapsed - checkInterval;
|
|
28
|
+
if (stallMs >= fatalThreshold) {
|
|
29
|
+
console.error(`[watchdog] FATAL: event loop stalled for ${Math.round(stallMs / 1000)}s (threshold: ${Math.round(fatalThreshold / 1000)}s) at ${new Date(now).toISOString()}`);
|
|
30
|
+
if (opts.onFatal) {
|
|
31
|
+
opts.onFatal(stallMs);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (stallMs >= warnThreshold) {
|
|
38
|
+
console.error(`[watchdog] WARNING: event loop stalled for ${Math.round(stallMs / 1000)}s at ${new Date(now).toISOString()}`);
|
|
39
|
+
opts.onStall?.(stallMs);
|
|
40
|
+
}
|
|
41
|
+
lastTick = now;
|
|
42
|
+
}, checkInterval);
|
|
43
|
+
timer.unref();
|
|
44
|
+
return () => {
|
|
45
|
+
if (timer) {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
timer = null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=watchdog.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { startWatchdog } from "./watchdog.js";
|
|
4
|
+
describe("watchdog", () => {
|
|
5
|
+
let stop;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
if (stop) {
|
|
8
|
+
stop();
|
|
9
|
+
stop = undefined;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it("calls onStall when stall exceeds warn threshold", async () => {
|
|
13
|
+
const stalls = [];
|
|
14
|
+
stop = startWatchdog({
|
|
15
|
+
checkIntervalMs: 30,
|
|
16
|
+
warnThresholdMs: 40,
|
|
17
|
+
fatalThresholdMs: 5000,
|
|
18
|
+
onStall: (ms) => stalls.push(ms),
|
|
19
|
+
});
|
|
20
|
+
// Block event loop longer than checkInterval + warnThreshold
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
while (Date.now() - start < 120) {
|
|
23
|
+
// busy-wait
|
|
24
|
+
}
|
|
25
|
+
// Let the interval fire
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
27
|
+
assert.ok(stalls.length > 0, "onStall should have been called");
|
|
28
|
+
assert.ok(stalls[0] >= 40, `stall duration ${stalls[0]} should be >= 40ms`);
|
|
29
|
+
stop();
|
|
30
|
+
stop = undefined;
|
|
31
|
+
});
|
|
32
|
+
it("calls onFatal when stall exceeds fatal threshold", async () => {
|
|
33
|
+
const fatals = [];
|
|
34
|
+
stop = startWatchdog({
|
|
35
|
+
checkIntervalMs: 30,
|
|
36
|
+
warnThresholdMs: 20,
|
|
37
|
+
fatalThresholdMs: 50,
|
|
38
|
+
onStall: () => { },
|
|
39
|
+
onFatal: (ms) => fatals.push(ms),
|
|
40
|
+
});
|
|
41
|
+
// Block event loop longer than checkInterval + fatalThreshold
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
while (Date.now() - start < 150) {
|
|
44
|
+
// busy-wait
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
47
|
+
assert.ok(fatals.length > 0, "onFatal should have been called");
|
|
48
|
+
assert.ok(fatals[0] >= 50, `fatal duration ${fatals[0]} should be >= 50ms`);
|
|
49
|
+
stop();
|
|
50
|
+
stop = undefined;
|
|
51
|
+
});
|
|
52
|
+
it("does not fire when event loop is healthy", async () => {
|
|
53
|
+
const stalls = [];
|
|
54
|
+
const fatals = [];
|
|
55
|
+
stop = startWatchdog({
|
|
56
|
+
checkIntervalMs: 30,
|
|
57
|
+
warnThresholdMs: 500,
|
|
58
|
+
fatalThresholdMs: 1000,
|
|
59
|
+
onStall: (ms) => stalls.push(ms),
|
|
60
|
+
onFatal: (ms) => fatals.push(ms),
|
|
61
|
+
});
|
|
62
|
+
// Wait without blocking — interval fires on time
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
64
|
+
assert.equal(stalls.length, 0, "onStall should not fire for healthy loop");
|
|
65
|
+
assert.equal(fatals.length, 0, "onFatal should not fire for healthy loop");
|
|
66
|
+
stop();
|
|
67
|
+
stop = undefined;
|
|
68
|
+
});
|
|
69
|
+
it("stop function cleans up the interval", () => {
|
|
70
|
+
stop = startWatchdog({
|
|
71
|
+
checkIntervalMs: 30,
|
|
72
|
+
warnThresholdMs: 10,
|
|
73
|
+
fatalThresholdMs: 20,
|
|
74
|
+
onStall: () => { },
|
|
75
|
+
onFatal: () => { },
|
|
76
|
+
});
|
|
77
|
+
// Should not throw on double-stop
|
|
78
|
+
stop();
|
|
79
|
+
stop();
|
|
80
|
+
stop = undefined;
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=watchdog.test.js.map
|