@ultimat3/jobs 23.0.0 → 24.0.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/CLAUDE.md +51 -52
- package/README.md +34 -5
- package/package.json +6 -6
- package/src/backfill-ledger.ts +24 -2
- package/src/backfill-pass.ts +22 -5
- package/src/claim-exhausted.ts +76 -0
- package/src/driver-memory-operator.ts +31 -9
- package/src/driver-memory.ts +68 -23
- package/src/driver-nats.ts +5 -2
- package/src/driver-pg-jobs-sql.ts +27 -19
- package/src/driver-pg-operator-sql.ts +32 -12
- package/src/driver-pg-operator.ts +19 -3
- package/src/driver-pg-rows.ts +3 -3
- package/src/driver-pg-sql.ts +98 -29
- package/src/driver-pg.ts +86 -81
- package/src/driver-redis.ts +5 -2
- package/src/driver.ts +37 -1
- package/src/errors-operator.ts +12 -4
- package/src/errors-requeue.ts +21 -3
- package/src/errors.ts +5 -13
- package/src/events-pg.ts +8 -18
- package/src/events.ts +20 -9
- package/src/execute.ts +1 -0
- package/src/index.ts +10 -4
- package/src/inspect-operator.ts +3 -3
- package/src/inspect.ts +4 -3
- package/src/introspection.ts +23 -11
- package/src/job.ts +5 -6
- package/src/limits.ts +33 -7
- package/src/outbox-pg.ts +1 -2
- package/src/outbox.ts +37 -9
- package/src/purge.ts +1 -1
- package/src/queue-wake.ts +1 -2
- package/src/scheduler-pg.ts +3 -2
- package/src/scheduler-state.ts +8 -1
- package/src/scheduler.ts +9 -10
- package/src/steps-suspension.ts +46 -0
- package/src/steps-timeout.ts +33 -0
- package/src/steps.ts +55 -81
- package/src/worker-admit.ts +7 -3
- package/src/worker-fleet-slots.ts +78 -44
- package/src/worker-key-busy.ts +38 -10
- package/src/worker-loop.ts +35 -11
- package/src/worker-registry.ts +16 -1
- package/src/worker.ts +52 -9
package/src/worker.ts
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ShutdownReason } from '@ultimat3/core';
|
|
7
7
|
import { beginWork, logger, onShutdown, recordJob, renderThrowable, uuid } from '@ultimat3/core';
|
|
8
|
+
import { announceExhausted } from './claim-exhausted';
|
|
8
9
|
import { nowMs } from './clock';
|
|
9
10
|
import { createDrainBudget, settleAllBy } from './drain-wait';
|
|
10
|
-
import type { ClaimedJob } from './driver';
|
|
11
|
+
import type { ClaimedJob, JobRecord } from './driver';
|
|
11
12
|
import { DEFAULT_QUEUE } from './driver';
|
|
12
13
|
import { JobDrainedError } from './errors';
|
|
13
14
|
import type { JobExecution } from './execute';
|
|
15
|
+
import { registeredJobs } from './job';
|
|
14
16
|
import { createLimiter } from './limits';
|
|
15
17
|
import { JOB_OUTCOME_LABELS } from './metrics';
|
|
16
18
|
import { claimAsks, createAdmission } from './worker-admit';
|
|
@@ -43,6 +45,7 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
43
45
|
ttlMs: visibilityTimeoutMs,
|
|
44
46
|
renewIntervalMs: heartbeatIntervalMs,
|
|
45
47
|
...(options.schedule === undefined ? {} : { schedule: options.schedule }),
|
|
48
|
+
...(options.clock === undefined ? {} : { clock: options.clock }),
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
const admit = createAdmission({
|
|
@@ -94,8 +97,12 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
94
97
|
let interrupted = 0;
|
|
95
98
|
let refused = 0;
|
|
96
99
|
let dropped = 0;
|
|
97
|
-
/**
|
|
98
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The ids this worker holds right now, with how many RUNS hold each — what its registry row
|
|
102
|
+
* reports. A count, because one worker can hold a job twice (a claim that lapsed under a body
|
|
103
|
+
* still running, then its own re-claim): a set dropped the id when the first run ended.
|
|
104
|
+
*/
|
|
105
|
+
const holding = new Map<string, number>();
|
|
99
106
|
let registration: WorkerRegistration | undefined;
|
|
100
107
|
/** `queue_depth` and its two siblings, republished on their own interval (`worker-queue-depth.ts`). */
|
|
101
108
|
const publishQueueDepth = createQueueDepthPublisher({
|
|
@@ -147,8 +154,36 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
147
154
|
// claiming" now, not at the next tick. What this round already holds still runs to the end
|
|
148
155
|
// — that is the drain, and `stop()` waits for it.
|
|
149
156
|
if (!claiming()) break;
|
|
150
|
-
|
|
157
|
+
// A lease that lapsed on a row's final attempt is settled by the claim itself and handed to
|
|
158
|
+
// `onExhausted`, never out as work: this round is the only thing that can count it, log it
|
|
159
|
+
// and tell the job. `dropExhausted` is the policy the row does not carry.
|
|
160
|
+
const buried: JobRecord[] = [];
|
|
161
|
+
const claimed = await options.driver.claim({
|
|
162
|
+
...ask,
|
|
163
|
+
visibilityTimeoutMs,
|
|
164
|
+
workerId,
|
|
165
|
+
dropExhausted: registeredJobs()
|
|
166
|
+
.filter((handle) => handle.retry.deadLetter === false)
|
|
167
|
+
.map((handle) => handle.name),
|
|
168
|
+
onExhausted: (dead) => {
|
|
169
|
+
buried.push(...dead);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
151
172
|
if (claimed.length > 0) found = true;
|
|
173
|
+
if (buried.length > 0) {
|
|
174
|
+
found = true;
|
|
175
|
+
const ended = await announceExhausted({
|
|
176
|
+
exhausted: buried,
|
|
177
|
+
workerId,
|
|
178
|
+
context: options.context,
|
|
179
|
+
});
|
|
180
|
+
deadLettered += ended.deadLettered;
|
|
181
|
+
dropped += ended.dropped;
|
|
182
|
+
for (const row of buried) {
|
|
183
|
+
const label = JOB_OUTCOME_LABELS[row.state === 'failed' ? 'dropped' : 'dead-lettered'];
|
|
184
|
+
if (label !== null) recordJob(row.queue, label);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
152
187
|
|
|
153
188
|
for (const [index, job] of claimed.entries()) {
|
|
154
189
|
const queue = job.queue;
|
|
@@ -170,7 +205,7 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
170
205
|
// core's own in-flight wait sits between `accept` and `inflight` and exists for exactly
|
|
171
206
|
// this. Counted nowhere, the worker had to wait for its own jobs inside a hook.
|
|
172
207
|
const finishWork = beginWork();
|
|
173
|
-
holding.
|
|
208
|
+
holding.set(job.id, (holding.get(job.id) ?? 0) + 1);
|
|
174
209
|
const running = runClaimed(job, admission.refusal)
|
|
175
210
|
.then((execution) => {
|
|
176
211
|
if (execution.outcome === 'completed') processed += 1;
|
|
@@ -193,7 +228,9 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
193
228
|
return execution;
|
|
194
229
|
})
|
|
195
230
|
.finally(async () => {
|
|
196
|
-
holding.
|
|
231
|
+
const held = (holding.get(job.id) ?? 1) - 1;
|
|
232
|
+
if (held === 0) holding.delete(job.id);
|
|
233
|
+
else holding.set(job.id, held);
|
|
197
234
|
lease.release();
|
|
198
235
|
// The slot is free NOW. A full worker asks the queue for nothing, so its loop backs
|
|
199
236
|
// off like an idle one — and waiting that out with a backlog behind the job that
|
|
@@ -335,9 +372,15 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
335
372
|
fix: 'raise the drain budget past the slowest job — configureLifecycle({ deadlineMs: 600_000 }) — and set terminationGracePeriodSeconds to at least as many seconds',
|
|
336
373
|
});
|
|
337
374
|
}
|
|
338
|
-
// Before the close, and awaited: the row is deleted through the driver being closed.
|
|
339
|
-
|
|
375
|
+
// Before the close, and awaited: the row is deleted through the driver being closed. Under
|
|
376
|
+
// the SAME budget as the jobs: `forgetWorker` is a statement on the pool that may be the
|
|
377
|
+
// thing failing, and unbounded it held a SIGTERM teardown open with the driver never closed.
|
|
378
|
+
// Abandoned, the row expires on its own TTL — what a killed worker's does.
|
|
379
|
+
const registered = registration;
|
|
340
380
|
registration = undefined;
|
|
381
|
+
if (registered !== undefined && !(await settleAllBy([registered.stop()], budget))) {
|
|
382
|
+
logger.warn('jobs.worker.registry-abandoned', { workerId, reason });
|
|
383
|
+
}
|
|
341
384
|
await options.driver.close?.();
|
|
342
385
|
} finally {
|
|
343
386
|
// Whatever the close did, this worker is done: a state left at 'draining' is a drain that
|
|
@@ -395,7 +438,7 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
395
438
|
host: options.host,
|
|
396
439
|
queues,
|
|
397
440
|
concurrency: queues.reduce((slots, queue) => slots + slotsFor(queue), 0),
|
|
398
|
-
inFlight: () => [...holding],
|
|
441
|
+
inFlight: () => [...holding.keys()],
|
|
399
442
|
// The visibility timeout, renewed on the heartbeat interval: a killed worker leaves the
|
|
400
443
|
// registry on exactly the schedule the queue takes its jobs back.
|
|
401
444
|
ttlMs: visibilityTimeoutMs,
|