Fold the sentinel into the scraper and drop the sentinel service (BL-0046)
The stall watchdog now runs inside the scraper (ScraperService.checkStalls, every 30 s from the work loop): same rule as before, one checkpoint-safe restart, quarantine on the second stall, missing checkpoint is no evidence, and paused time no longer counts. Stalls are recorded as scraper incidents (failure.kind 'stall'). The private /api/recovery endpoint is gone. Dropped with the service: reader/scraper-unreachable incidents (supervisor and Docker handle crashes; the health page shows "Scraper: unreachable"), duplicate job-failure incidents and the unused /metrics endpoint. The stack is now reader + scraper (+ optional peer-gateway). The supervisor test moved to runtime/test (npm run test:runtime stays). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88555d4780
commit
b2bb34174c
37 files changed
+163
-311
No files matched your search
@@ -29,12 +29,45 @@ test('status and watchdog read the checkpoint for each source layout', async t =
|
||||
}
|
||||
});
|
||||
|
||||
test('daemon refuses recovery when the checkpoint cannot be read', async () => {
|
||||
const service = Object.create(ScraperService.prototype);
|
||||
service.state = { paused: false, currentIds: { mangapill: 'job' },
|
||||
queue: [{ id: 'job', source: 'mangapill', status: 'downloading' }] };
|
||||
service.jobState = async () => null;
|
||||
const result = await service.recover({ id: 'job', progressKey: '[0,0,"",0,""]' });
|
||||
assert.deepEqual(result, { changed: false });
|
||||
function stallService(job) {
|
||||
const service = new ScraperService({ stallMs: 100 });
|
||||
service.state = { paused: false, sourcePaused: {}, currentIds: { a: 'job' }, incidents: [],
|
||||
queue: [{ id: 'job', source: 'a', title: 'Manga', status: 'downloading' }] };
|
||||
service.jobState = async () => job.value;
|
||||
service.event = async () => {};
|
||||
return service;
|
||||
}
|
||||
|
||||
test('stall check leaves progressing and deliberately paused jobs alone', async () => {
|
||||
const job = { value: { completed: 0, current: { downloadedPages: 1 } } };
|
||||
const service = stallService(job), item = service.state.queue[0];
|
||||
await service.checkStalls(0);
|
||||
job.value = { completed: 0, current: { downloadedPages: 2 } };
|
||||
await service.checkStalls(500000);
|
||||
service.state.paused = true;
|
||||
await service.checkStalls(1000000);
|
||||
assert.equal(item.stopReason, undefined);
|
||||
// Paused time does not count: the clock restarts on resume.
|
||||
service.state.paused = false;
|
||||
await service.checkStalls(1000050);
|
||||
assert.equal(item.stopReason, undefined);
|
||||
await service.checkStalls(1190000);
|
||||
assert.equal(item.stopReason, 'recovering');
|
||||
assert.equal(item.stallRecoveries, 1);
|
||||
assert.equal(service.state.incidents[0].failure.kind, 'stall');
|
||||
});
|
||||
|
||||
test('a second stall quarantines the job', async () => {
|
||||
const service = stallService({ value: { completed: 2, total: 5 } }), item = service.state.queue[0];
|
||||
item.stallRecoveries = 1;
|
||||
await service.checkStalls(0); await service.checkStalls(1000000);
|
||||
assert.equal(item.stopReason, 'quarantined');
|
||||
assert.equal(item.failure.kind, 'stall');
|
||||
});
|
||||
|
||||
test('missing checkpoint does not trigger a false stall recovery', async () => {
|
||||
const service = stallService({ value: null });
|
||||
for (const now of [0, 1000000, 2000000]) await service.checkStalls(now);
|
||||
assert.equal(service.state.queue[0].stopReason, undefined);
|
||||
assert.equal(service.state.incidents.length, 0);
|
||||
});
|
||||
Reference in new issue
Block a user