@jarenjs/db 0.34.0 → 0.34.2
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/types/jobs.d.ts +7 -1
- package/docs/JOBS-FORMAT.md +12 -1
- package/package.json +4 -4
- package/src/jobs.js +36 -7
package/dist/types/jobs.d.ts
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* - **Shutdown is bounded.** Handlers receive an `AbortSignal` and
|
|
26
26
|
* `stop()` takes a deadline, so a handler that never settles cannot
|
|
27
27
|
* hold `stop()` — and therefore `store.close()`, and therefore the
|
|
28
|
-
* database file — open forever.
|
|
28
|
+
* database file — open forever. A loop the deadline could not drain
|
|
29
|
+
* is CANCELLED, not merely left behind: when its handler finally
|
|
30
|
+
* settles it exits without another claim, store write or poll
|
|
31
|
+
* timer, and its abandoned job recovers by lease expiry (§5).
|
|
29
32
|
*/
|
|
30
33
|
export declare const JOBS_TABLE = "_jaren_jobs";
|
|
31
34
|
export declare const JOB_CHECKPOINTS_TABLE = "_jaren_job_checkpoints";
|
|
@@ -117,6 +120,9 @@ export declare function createJobEngine(options: {
|
|
|
117
120
|
* the loops — but only up to `graceMs`. A handler that ignores its
|
|
118
121
|
* signal cannot hold the process open; the resolved record says so
|
|
119
122
|
* instead, and the lease expiry (§5) lets another worker re-claim.
|
|
123
|
+
* A loop the grace period could not drain is cancelled outright:
|
|
124
|
+
* when its handler finally settles it exits without another
|
|
125
|
+
* claim, store write or poll timer.
|
|
120
126
|
* @param {{ graceMs?: number }} [stopOptions]
|
|
121
127
|
* @returns {Promise<{ drained: boolean, inFlight: number }>}
|
|
122
128
|
*/
|
package/docs/JOBS-FORMAT.md
CHANGED
|
@@ -125,7 +125,8 @@ stats() }`:
|
|
|
125
125
|
plainly (§8);
|
|
126
126
|
- `stop({ graceMs })` aborts in-flight handlers and resolves once they
|
|
127
127
|
settle **or** the grace period expires (default 5 s), answering
|
|
128
|
-
`{ drained, inFlight }
|
|
128
|
+
`{ drained, inFlight }`; a loop the grace period could not drain is
|
|
129
|
+
**cancelled**, not left running — see §6.1;
|
|
129
130
|
- `stats()` reports claims, completions, failures, wakes, polls and the
|
|
130
131
|
in-flight handler count.
|
|
131
132
|
|
|
@@ -162,6 +163,16 @@ handle really is released. A handler that ignores its signal therefore
|
|
|
162
163
|
cannot hold the database file open for the life of the process, and the
|
|
163
164
|
lease expiry (§5) lets another worker re-claim its job.
|
|
164
165
|
|
|
166
|
+
A loop the grace period could not drain is **cancelled**, not merely
|
|
167
|
+
uncounted: when its wedged handler finally settles, the loop exits
|
|
168
|
+
without writing the completion or the failure, without claiming again,
|
|
169
|
+
and without re-arming its poll timer — the store it would touch is the
|
|
170
|
+
one the caller is closing, and the job it abandons recovers by lease
|
|
171
|
+
expiry (§5). A later `start()` builds fresh loops in a new session (and
|
|
172
|
+
re-registers the worker's wake-on-enqueue hook and its place in
|
|
173
|
+
`stopAll`), so a cancelled loop can never be revived as an extra
|
|
174
|
+
claimer.
|
|
175
|
+
|
|
165
176
|
## 7. The DAG composition
|
|
166
177
|
|
|
167
178
|
```js
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.34.
|
|
4
|
+
"version": "0.34.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"prepack": "npm run build:types"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@jarenjs/core": "^0.34.
|
|
75
|
-
"@jarenjs/json": "^0.34.
|
|
76
|
-
"@jarenjs/validate": "^0.34.
|
|
74
|
+
"@jarenjs/core": "^0.34.2",
|
|
75
|
+
"@jarenjs/json": "^0.34.2",
|
|
76
|
+
"@jarenjs/validate": "^0.34.2"
|
|
77
77
|
},
|
|
78
78
|
"bin": {
|
|
79
79
|
"jaren-db": "./src/cli.js"
|
package/src/jobs.js
CHANGED
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
* - **Shutdown is bounded.** Handlers receive an `AbortSignal` and
|
|
27
27
|
* `stop()` takes a deadline, so a handler that never settles cannot
|
|
28
28
|
* hold `stop()` — and therefore `store.close()`, and therefore the
|
|
29
|
-
* database file — open forever.
|
|
29
|
+
* database file — open forever. A loop the deadline could not drain
|
|
30
|
+
* is CANCELLED, not merely left behind: when its handler finally
|
|
31
|
+
* settles it exits without another claim, store write or poll
|
|
32
|
+
* timer, and its abandoned job recovers by lease expiry (§5).
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
35
|
import { chain } from './driver.js';
|
|
@@ -370,6 +373,17 @@ export function createJobEngine(options) {
|
|
|
370
373
|
|
|
371
374
|
/** Aborted when `stop()` is called: the handler's cue to wind up. */
|
|
372
375
|
let shutdown = new AbortController();
|
|
376
|
+
/**
|
|
377
|
+
* The per-`start()` loop session. When a `stop()` grace deadline
|
|
378
|
+
* expires the session is cancelled: a loop that could not be
|
|
379
|
+
* drained must abandon its job — no completion or failure write,
|
|
380
|
+
* no further claim, no re-armed poll timer — because the store it
|
|
381
|
+
* would touch is the one the caller is about to close. The
|
|
382
|
+
* abandoned lease expires and the next claim re-runs the job (§5).
|
|
383
|
+
* A later `start()` opens a NEW session, so a cancelled loop can
|
|
384
|
+
* never be revived.
|
|
385
|
+
*/
|
|
386
|
+
let session = { cancelled: false };
|
|
373
387
|
/** In-flight handler count, so `stop()` can report what it left. */
|
|
374
388
|
let inFlight = 0;
|
|
375
389
|
|
|
@@ -389,7 +403,8 @@ export function createJobEngine(options) {
|
|
|
389
403
|
}
|
|
390
404
|
};
|
|
391
405
|
|
|
392
|
-
|
|
406
|
+
/** @param {{ cancelled: boolean }} loopSession */
|
|
407
|
+
const runOne = async (job, loopSession) => {
|
|
393
408
|
stats.claims += 1;
|
|
394
409
|
inFlight += 1;
|
|
395
410
|
try {
|
|
@@ -399,9 +414,13 @@ export function createJobEngine(options) {
|
|
|
399
414
|
{ job, checkpointsFor, signal: shutdown.signal });
|
|
400
415
|
}
|
|
401
416
|
catch (error) {
|
|
417
|
+
// past cancellation the store is closing: leave the leased
|
|
418
|
+
// row to expiry-based recovery (§5) instead of racing it
|
|
419
|
+
if (loopSession.cancelled) return;
|
|
402
420
|
await recordFailure(job, error);
|
|
403
421
|
return;
|
|
404
422
|
}
|
|
423
|
+
if (loopSession.cancelled) return;
|
|
405
424
|
try {
|
|
406
425
|
// a §7 handler may have completed transactionally already; the
|
|
407
426
|
// guarded update makes this a no-op then
|
|
@@ -420,8 +439,9 @@ export function createJobEngine(options) {
|
|
|
420
439
|
}
|
|
421
440
|
};
|
|
422
441
|
|
|
423
|
-
|
|
424
|
-
|
|
442
|
+
/** @param {{ cancelled: boolean }} loopSession */
|
|
443
|
+
const loop = async (loopSession) => {
|
|
444
|
+
while (running && !loopSession.cancelled) {
|
|
425
445
|
let job;
|
|
426
446
|
try {
|
|
427
447
|
job = await Promise.resolve(claim({
|
|
@@ -430,13 +450,13 @@ export function createJobEngine(options) {
|
|
|
430
450
|
catch {
|
|
431
451
|
job = undefined; // a transient storage failure: back off to the poll
|
|
432
452
|
}
|
|
433
|
-
if (!running) return;
|
|
453
|
+
if (!running || loopSession.cancelled) return;
|
|
434
454
|
if (job === undefined) {
|
|
435
455
|
await sleep();
|
|
436
456
|
continue;
|
|
437
457
|
}
|
|
438
458
|
try {
|
|
439
|
-
await runOne(job);
|
|
459
|
+
await runOne(job, loopSession);
|
|
440
460
|
}
|
|
441
461
|
catch {
|
|
442
462
|
// `runOne` normalizes every handler outcome, so reaching here
|
|
@@ -453,7 +473,12 @@ export function createJobEngine(options) {
|
|
|
453
473
|
if (running) throw new TypeError('the worker is already started');
|
|
454
474
|
running = true;
|
|
455
475
|
shutdown = new AbortController();
|
|
456
|
-
|
|
476
|
+
session = { cancelled: false };
|
|
477
|
+
loops = Array.from({ length: concurrency }, () => loop(session));
|
|
478
|
+
// a restart re-registers what stop() removed: the
|
|
479
|
+
// wake-on-enqueue hook and the stopAll membership
|
|
480
|
+
wakers.add(onWake);
|
|
481
|
+
workers.add(worker);
|
|
457
482
|
return worker;
|
|
458
483
|
},
|
|
459
484
|
/**
|
|
@@ -461,6 +486,9 @@ export function createJobEngine(options) {
|
|
|
461
486
|
* the loops — but only up to `graceMs`. A handler that ignores its
|
|
462
487
|
* signal cannot hold the process open; the resolved record says so
|
|
463
488
|
* instead, and the lease expiry (§5) lets another worker re-claim.
|
|
489
|
+
* A loop the grace period could not drain is cancelled outright:
|
|
490
|
+
* when its handler finally settles it exits without another
|
|
491
|
+
* claim, store write or poll timer.
|
|
464
492
|
* @param {{ graceMs?: number }} [stopOptions]
|
|
465
493
|
* @returns {Promise<{ drained: boolean, inFlight: number }>}
|
|
466
494
|
*/
|
|
@@ -478,6 +506,7 @@ export function createJobEngine(options) {
|
|
|
478
506
|
]);
|
|
479
507
|
clearTimeout(timer);
|
|
480
508
|
if (drained) loops = [];
|
|
509
|
+
else session.cancelled = true; // cancel what could not be drained
|
|
481
510
|
wakers.delete(onWake);
|
|
482
511
|
workers.delete(worker);
|
|
483
512
|
return { drained: drained === true, inFlight };
|