@semiont/jobs 0.6.1 → 0.6.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/index.d.ts +60 -4
- package/dist/index.js +73 -8
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.d.ts
CHANGED
|
@@ -369,6 +369,23 @@ interface JobQueue {
|
|
|
369
369
|
* starvation, 2026-09-15).
|
|
370
370
|
*/
|
|
371
371
|
declare const JOB_QUEUE_EMITS: readonly ["job:queued"];
|
|
372
|
+
/**
|
|
373
|
+
* How long a job's record survives after it reaches a terminal state
|
|
374
|
+
* (complete/failed/cancelled), and how often a driver enforces that.
|
|
375
|
+
*
|
|
376
|
+
* ONE pair for every driver. The window is a CONTRACT fact — a caller that
|
|
377
|
+
* reads a finished job's result gets the same day whichever backing store the
|
|
378
|
+
* stack runs — and the cadence is what turns the window from an aspiration
|
|
379
|
+
* into a promise. Restated per driver the two numbers drift, and the drift is
|
|
380
|
+
* invisible until someone compares two deployments.
|
|
381
|
+
*
|
|
382
|
+
* The METHOD that enforces retention is not on the interface and cannot be:
|
|
383
|
+
* it is an unlink in one driver and a stream purge in another. The NUMBER is
|
|
384
|
+
* here because it is the same number.
|
|
385
|
+
*/
|
|
386
|
+
declare const TERMINAL_JOB_RETENTION_MS: number;
|
|
387
|
+
/** How often a driver sweeps for records past `TERMINAL_JOB_RETENTION_MS`. */
|
|
388
|
+
declare const TERMINAL_JOB_SWEEP_INTERVAL_MS: number;
|
|
372
389
|
|
|
373
390
|
/**
|
|
374
391
|
* Job Queue Manager
|
|
@@ -500,9 +517,12 @@ declare class FsJobQueue implements JobQueue {
|
|
|
500
517
|
*/
|
|
501
518
|
recoverStaleRunningJobs(): Promise<number>;
|
|
502
519
|
/**
|
|
503
|
-
*
|
|
520
|
+
* Delete terminal jobs whose `completedAt` is older than `retentionMs`.
|
|
521
|
+
* The window is a parameter with no default: the ONE value the queue runs
|
|
522
|
+
* on is `TERMINAL_JOB_RETENTION_MS`, and a default here would be a second
|
|
523
|
+
* copy of it that nobody notices going stale.
|
|
504
524
|
*/
|
|
505
|
-
cleanupOldJobs(
|
|
525
|
+
cleanupOldJobs(retentionMs: number): Promise<number>;
|
|
506
526
|
/**
|
|
507
527
|
* Get job file path
|
|
508
528
|
*/
|
|
@@ -530,7 +550,10 @@ declare class FsJobQueue implements JobQueue {
|
|
|
530
550
|
* - **KV bucket `jobs`** is the AUTHORITATIVE operational state: one entry
|
|
531
551
|
* per job (`{ job, lastProgressAt }`), every transition a revision-CAS —
|
|
532
552
|
* which is what makes `claimJob` atomic: simultaneous claims race on one
|
|
533
|
-
* revision and exactly one update wins.
|
|
553
|
+
* revision and exactly one update wins. It holds LIVE jobs plus a day of
|
|
554
|
+
* concluded ones, not a history: `pruneTerminalJobs` is the retention the
|
|
555
|
+
* fs driver's janitor holds, and without it every scan over the bucket
|
|
556
|
+
* — `getStats` is one, on the metrics interval — grows forever.
|
|
534
557
|
* - **Stream `JOBS`** (subjects `jobs.<category>.<type>`, work-queue
|
|
535
558
|
* retention) is the DELIVERY vehicle and redelivery timer. A delivered
|
|
536
559
|
* message is the lease this process holds for a job; `working()`
|
|
@@ -598,6 +621,7 @@ declare class JetStreamJobQueue implements JobQueue {
|
|
|
598
621
|
private reconciling;
|
|
599
622
|
private tick;
|
|
600
623
|
private ticking;
|
|
624
|
+
private cleanupTimer;
|
|
601
625
|
private readonly lastProgressWrite;
|
|
602
626
|
private readonly staleRunningMs;
|
|
603
627
|
private readonly ackWaitMs;
|
|
@@ -642,6 +666,11 @@ declare class JetStreamJobQueue implements JobQueue {
|
|
|
642
666
|
recordProgress(jobIdArg: JobId, progress: Record<string, unknown>): Promise<void>;
|
|
643
667
|
cancelPendingJobs(category: 'annotation' | 'generation'): Promise<number>;
|
|
644
668
|
cancelJob(jobIdArg: JobId): Promise<boolean>;
|
|
669
|
+
/**
|
|
670
|
+
* Counts by status. The three terminal counts are the RETENTION WINDOW, not
|
|
671
|
+
* lifetime totals — `pruneTerminalJobs` drops a record a day after it
|
|
672
|
+
* concluded — so they answer "recently finished", never "finished ever".
|
|
673
|
+
*/
|
|
645
674
|
getStats(): Promise<{
|
|
646
675
|
pending: number;
|
|
647
676
|
running: number;
|
|
@@ -656,6 +685,33 @@ declare class JetStreamJobQueue implements JobQueue {
|
|
|
656
685
|
* through here; that is AckWait redelivery.
|
|
657
686
|
*/
|
|
658
687
|
recoverStaleRunningJobs(): Promise<number>;
|
|
688
|
+
/**
|
|
689
|
+
* Terminal-job retention: the fs driver's hourly janitor, in this driver's
|
|
690
|
+
* terms. Without it the bucket keeps every job it has ever seen, and
|
|
691
|
+
* `getStats` — an OTel observable gauge, so a full bucket scan every metric
|
|
692
|
+
* interval — pays for all of them forever. Measured on a live stack
|
|
693
|
+
* 2026-09-24: 139 concluded records re-read every 30 seconds by a knowledge
|
|
694
|
+
* base that had done very little work.
|
|
695
|
+
*
|
|
696
|
+
* A SWEEP AND NOT A BUCKET TTL, deliberately. `ttl` on `views.kv()` is a
|
|
697
|
+
* BUCKET limit — the backing stream's `max_age` — so it expires every key
|
|
698
|
+
* by age, not the terminal ones by status: a pending job nobody claimed for
|
|
699
|
+
* a day, a running job whose worker went quiet, both evaporate alongside
|
|
700
|
+
* the finished ones. That is not a slower version of this bug, it is a
|
|
701
|
+
* worse one. This bucket is the AUTHORITATIVE record: with the entry gone
|
|
702
|
+
* `read` returns null, every CAS falls through to its `onMissing`, and the
|
|
703
|
+
* next delivery for that job is `term()`ed as unknown — the job disappears
|
|
704
|
+
* with no failure and no trace. The rule retention needs to express is
|
|
705
|
+
* about STATUS, and status is exactly what a bucket-wide clock cannot see.
|
|
706
|
+
* (A per-message TTL set at the terminal write would express it, but that
|
|
707
|
+
* is nats-server 2.11 plus a client that exposes it; nats.js 2.x does not.)
|
|
708
|
+
*
|
|
709
|
+
* Terminal is absorbing — no transition leaves `complete`/`failed`/
|
|
710
|
+
* `cancelled` — so a record this sweep reads as expired cannot come back to
|
|
711
|
+
* life under it, and the lease was settled at the transition that made it
|
|
712
|
+
* terminal.
|
|
713
|
+
*/
|
|
714
|
+
pruneTerminalJobs(retentionMs: number): Promise<number>;
|
|
659
715
|
/** The heartbeat body — extend live leases, settle concluded ones. */
|
|
660
716
|
private reconcileHeld;
|
|
661
717
|
/**
|
|
@@ -1077,5 +1133,5 @@ declare const WORKER_CONSUMED_BROADCASTS: readonly ["job:queued", "job:cancel-re
|
|
|
1077
1133
|
*/
|
|
1078
1134
|
declare const WORKER_CHANNELS: readonly (keyof EventMap)[];
|
|
1079
1135
|
|
|
1080
|
-
export { AnnotationDetection, FsJobQueue, JOBS_STREAM_SUBJECTS, JOB_QUEUE_EMITS, JetStreamJobQueue, STALL_THRESHOLD_MS, WORKER_CHANNELS, WORKER_CONSUMED_BROADCASTS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
|
|
1136
|
+
export { AnnotationDetection, FsJobQueue, JOBS_STREAM_SUBJECTS, JOB_QUEUE_EMITS, JetStreamJobQueue, STALL_THRESHOLD_MS, TERMINAL_JOB_RETENTION_MS, TERMINAL_JOB_SWEEP_INTERVAL_MS, WORKER_CHANNELS, WORKER_CONSUMED_BROADCASTS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
|
|
1081
1137
|
export type { AnyJob, AssessmentDetectionJob, AssessmentDetectionParams, AssessmentDetectionProgress, CancelledJob, CommentDetectionJob, CommentDetectionParams, CommentDetectionProgress, CompleteJob, DetectionJob, DetectionParams, DetectionProgress, FailedJob, GenerationJob, GenerationResult, HighlightDetectionJob, HighlightDetectionParams, HighlightDetectionProgress, JetStreamJobQueueOptions, JobMetadata, JobQueryFilters, JobQueue, JobStatus, JobType, OnProgress, PendingJob, ProcessorResult, RunningJob, TagDetectionJob, TagDetectionParams, TagDetectionProgress, YieldProgress };
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import '@semiont/http-transport';
|
|
|
14
14
|
|
|
15
15
|
// src/job-queue-interface.ts
|
|
16
16
|
var JOB_QUEUE_EMITS = ["job:queued"];
|
|
17
|
+
var TERMINAL_JOB_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
18
|
+
var TERMINAL_JOB_SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
17
19
|
|
|
18
20
|
// src/will-retry.ts
|
|
19
21
|
function willRetryAfter(metadata, failureClass) {
|
|
@@ -39,8 +41,6 @@ function mergeUnitCursors(existing, incoming, completed) {
|
|
|
39
41
|
var REANNOUNCE_INTERVAL_MS = 3e4;
|
|
40
42
|
var STALE_RUNNING_MS = 30 * 6e4;
|
|
41
43
|
var PROGRESS_WRITE_MIN_INTERVAL_MS = 5e3;
|
|
42
|
-
var RETENTION_HOURS = 24;
|
|
43
|
-
var CLEANUP_INTERVAL_MS = 36e5;
|
|
44
44
|
var FsJobQueue = class {
|
|
45
45
|
constructor(state, logger, eventBus) {
|
|
46
46
|
this.eventBus = eventBus;
|
|
@@ -82,12 +82,12 @@ var FsJobQueue = class {
|
|
|
82
82
|
}
|
|
83
83
|
if (!this.cleanupTimer) {
|
|
84
84
|
this.cleanupTimer = setInterval(() => {
|
|
85
|
-
this.cleanupOldJobs(
|
|
85
|
+
this.cleanupOldJobs(TERMINAL_JOB_RETENTION_MS).catch((error) => {
|
|
86
86
|
this.logger.warn("Job retention cleanup failed", {
|
|
87
87
|
error: error instanceof Error ? error.message : String(error)
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
|
-
},
|
|
90
|
+
}, TERMINAL_JOB_SWEEP_INTERVAL_MS);
|
|
91
91
|
this.cleanupTimer.unref?.();
|
|
92
92
|
}
|
|
93
93
|
this.logger.info("Job queue initialized");
|
|
@@ -450,10 +450,13 @@ var FsJobQueue = class {
|
|
|
450
450
|
return recovered;
|
|
451
451
|
}
|
|
452
452
|
/**
|
|
453
|
-
*
|
|
453
|
+
* Delete terminal jobs whose `completedAt` is older than `retentionMs`.
|
|
454
|
+
* The window is a parameter with no default: the ONE value the queue runs
|
|
455
|
+
* on is `TERMINAL_JOB_RETENTION_MS`, and a default here would be a second
|
|
456
|
+
* copy of it that nobody notices going stale.
|
|
454
457
|
*/
|
|
455
|
-
async cleanupOldJobs(
|
|
456
|
-
const cutoffTime = Date.now() -
|
|
458
|
+
async cleanupOldJobs(retentionMs) {
|
|
459
|
+
const cutoffTime = Date.now() - retentionMs;
|
|
457
460
|
let deletedCount = 0;
|
|
458
461
|
const cleanupStatuses = ["complete", "failed", "cancelled"];
|
|
459
462
|
for (const status of cleanupStatuses) {
|
|
@@ -514,6 +517,8 @@ var FsJobQueue = class {
|
|
|
514
517
|
var STREAM = "JOBS";
|
|
515
518
|
var CONSUMER = "gateway-claims";
|
|
516
519
|
var BUCKET = "jobs";
|
|
520
|
+
var BUCKET_STREAM = `KV_${BUCKET}`;
|
|
521
|
+
var bucketSubject = (key) => `$KV.${BUCKET}.${key}`;
|
|
517
522
|
var PROGRESS_WRITE_MIN_INTERVAL_MS2 = 5e3;
|
|
518
523
|
var MAX_CAS_ATTEMPTS = 20;
|
|
519
524
|
var enc = new TextEncoder();
|
|
@@ -549,6 +554,7 @@ var JetStreamJobQueue = class {
|
|
|
549
554
|
reconciling = false;
|
|
550
555
|
tick = null;
|
|
551
556
|
ticking = false;
|
|
557
|
+
cleanupTimer = null;
|
|
552
558
|
lastProgressWrite = /* @__PURE__ */ new Map();
|
|
553
559
|
staleRunningMs;
|
|
554
560
|
ackWaitMs;
|
|
@@ -633,6 +639,14 @@ var JetStreamJobQueue = class {
|
|
|
633
639
|
});
|
|
634
640
|
}, this.tickMs);
|
|
635
641
|
this.tick.unref?.();
|
|
642
|
+
this.cleanupTimer = setInterval(() => {
|
|
643
|
+
this.pruneTerminalJobs(TERMINAL_JOB_RETENTION_MS).catch((error) => {
|
|
644
|
+
this.logger.warn("Job retention cleanup failed", {
|
|
645
|
+
error: error instanceof Error ? error.message : String(error)
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}, TERMINAL_JOB_SWEEP_INTERVAL_MS);
|
|
649
|
+
this.cleanupTimer.unref?.();
|
|
636
650
|
}
|
|
637
651
|
/** An outage is never silent: one line down, one line back, one line if the client gives up. */
|
|
638
652
|
watchConnection() {
|
|
@@ -661,6 +675,8 @@ var JetStreamJobQueue = class {
|
|
|
661
675
|
this.heartbeat = null;
|
|
662
676
|
if (this.tick) clearInterval(this.tick);
|
|
663
677
|
this.tick = null;
|
|
678
|
+
if (this.cleanupTimer) clearInterval(this.cleanupTimer);
|
|
679
|
+
this.cleanupTimer = null;
|
|
664
680
|
this.held.clear();
|
|
665
681
|
void this.nc?.close();
|
|
666
682
|
}
|
|
@@ -940,6 +956,11 @@ var JetStreamJobQueue = class {
|
|
|
940
956
|
if (done) this.settleLease(jobIdArg, "term");
|
|
941
957
|
return done;
|
|
942
958
|
}
|
|
959
|
+
/**
|
|
960
|
+
* Counts by status. The three terminal counts are the RETENTION WINDOW, not
|
|
961
|
+
* lifetime totals — `pruneTerminalJobs` drops a record a day after it
|
|
962
|
+
* concluded — so they answer "recently finished", never "finished ever".
|
|
963
|
+
*/
|
|
943
964
|
async getStats() {
|
|
944
965
|
const stats = { pending: 0, running: 0, complete: 0, failed: 0, cancelled: 0 };
|
|
945
966
|
for (const key of await this.allKeys()) {
|
|
@@ -972,6 +993,50 @@ var JetStreamJobQueue = class {
|
|
|
972
993
|
}
|
|
973
994
|
return recovered;
|
|
974
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Terminal-job retention: the fs driver's hourly janitor, in this driver's
|
|
998
|
+
* terms. Without it the bucket keeps every job it has ever seen, and
|
|
999
|
+
* `getStats` — an OTel observable gauge, so a full bucket scan every metric
|
|
1000
|
+
* interval — pays for all of them forever. Measured on a live stack
|
|
1001
|
+
* 2026-09-24: 139 concluded records re-read every 30 seconds by a knowledge
|
|
1002
|
+
* base that had done very little work.
|
|
1003
|
+
*
|
|
1004
|
+
* A SWEEP AND NOT A BUCKET TTL, deliberately. `ttl` on `views.kv()` is a
|
|
1005
|
+
* BUCKET limit — the backing stream's `max_age` — so it expires every key
|
|
1006
|
+
* by age, not the terminal ones by status: a pending job nobody claimed for
|
|
1007
|
+
* a day, a running job whose worker went quiet, both evaporate alongside
|
|
1008
|
+
* the finished ones. That is not a slower version of this bug, it is a
|
|
1009
|
+
* worse one. This bucket is the AUTHORITATIVE record: with the entry gone
|
|
1010
|
+
* `read` returns null, every CAS falls through to its `onMissing`, and the
|
|
1011
|
+
* next delivery for that job is `term()`ed as unknown — the job disappears
|
|
1012
|
+
* with no failure and no trace. The rule retention needs to express is
|
|
1013
|
+
* about STATUS, and status is exactly what a bucket-wide clock cannot see.
|
|
1014
|
+
* (A per-message TTL set at the terminal write would express it, but that
|
|
1015
|
+
* is nats-server 2.11 plus a client that exposes it; nats.js 2.x does not.)
|
|
1016
|
+
*
|
|
1017
|
+
* Terminal is absorbing — no transition leaves `complete`/`failed`/
|
|
1018
|
+
* `cancelled` — so a record this sweep reads as expired cannot come back to
|
|
1019
|
+
* life under it, and the lease was settled at the transition that made it
|
|
1020
|
+
* terminal.
|
|
1021
|
+
*/
|
|
1022
|
+
async pruneTerminalJobs(retentionMs) {
|
|
1023
|
+
const cutoff = Date.now() - retentionMs;
|
|
1024
|
+
let pruned = 0;
|
|
1025
|
+
for (const key of await this.allKeys()) {
|
|
1026
|
+
const envelope = await this.read(jobId(key));
|
|
1027
|
+
if (!envelope) continue;
|
|
1028
|
+
const { job } = envelope;
|
|
1029
|
+
if (job.status !== "complete" && job.status !== "failed" && job.status !== "cancelled") continue;
|
|
1030
|
+
if (Date.parse(job.completedAt) >= cutoff) continue;
|
|
1031
|
+
await this.jsm.streams.purge(BUCKET_STREAM, { filter: bucketSubject(key) });
|
|
1032
|
+
this.lastProgressWrite.delete(key);
|
|
1033
|
+
pruned++;
|
|
1034
|
+
}
|
|
1035
|
+
if (pruned > 0) {
|
|
1036
|
+
this.logger.info("Jobs cleaned up", { deletedCount: pruned });
|
|
1037
|
+
}
|
|
1038
|
+
return pruned;
|
|
1039
|
+
}
|
|
975
1040
|
/** The heartbeat body — extend live leases, settle concluded ones. */
|
|
976
1041
|
async reconcileHeld() {
|
|
977
1042
|
for (const [id, held] of [...this.held]) {
|
|
@@ -2975,6 +3040,6 @@ var WORKER_CHANNELS = [
|
|
|
2975
3040
|
...WORKER_CONSUMED_BROADCASTS
|
|
2976
3041
|
];
|
|
2977
3042
|
|
|
2978
|
-
export { AnnotationDetection, FsJobQueue, JOBS_STREAM_SUBJECTS, JOB_QUEUE_EMITS, JetStreamJobQueue, STALL_THRESHOLD_MS, WORKER_CHANNELS, WORKER_CONSUMED_BROADCASTS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
|
|
3043
|
+
export { AnnotationDetection, FsJobQueue, JOBS_STREAM_SUBJECTS, JOB_QUEUE_EMITS, JetStreamJobQueue, STALL_THRESHOLD_MS, TERMINAL_JOB_RETENTION_MS, TERMINAL_JOB_SWEEP_INTERVAL_MS, WORKER_CHANNELS, WORKER_CONSUMED_BROADCASTS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
|
|
2979
3044
|
//# sourceMappingURL=index.js.map
|
|
2980
3045
|
//# sourceMappingURL=index.js.map
|