@ultimat3/jobs 1.1.0 → 1.2.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/package.json +5 -5
- package/src/worker.ts +33 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/jobs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"test": "bun test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@ultimat3/core": "1.
|
|
34
|
-
"@ultimat3/entity": "1.
|
|
35
|
-
"@ultimat3/schema": "1.
|
|
36
|
-
"@ultimat3/time": "1.
|
|
33
|
+
"@ultimat3/core": "1.2.0",
|
|
34
|
+
"@ultimat3/entity": "1.2.0",
|
|
35
|
+
"@ultimat3/schema": "1.2.0",
|
|
36
|
+
"@ultimat3/time": "1.2.0"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/worker.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// deploy turns "at least once" into "always twice", so draining is on by default.
|
|
5
5
|
|
|
6
6
|
import type { Clock, Ctx } from '@ultimat3/core';
|
|
7
|
-
import { logger, onShutdown, uuid, withSpan } from '@ultimat3/core';
|
|
7
|
+
import { logger, onShutdown, recordQueueDepth, uuid, withSpan } from '@ultimat3/core';
|
|
8
8
|
import { nowMs } from './clock';
|
|
9
9
|
import type { ClaimedJob, JobDriver, QueueStats } from './driver';
|
|
10
10
|
import { DEFAULT_QUEUE, DEFAULT_VISIBILITY_TIMEOUT_MS } from './driver';
|
|
@@ -18,6 +18,14 @@ import { nextRetry } from './retry';
|
|
|
18
18
|
import type { EventLookup, StepRecord } from './steps';
|
|
19
19
|
import { createStepRunner, isStepSuspension } from './steps';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* How often the claim loop republishes `queue_depth`. Its own interval, not `pollIntervalMs`:
|
|
23
|
+
* `driver.stats()` is an aggregate over the whole jobs table and a scrape reads the gauge every
|
|
24
|
+
* ~15s, so publishing at the poll rate would multiply the queue's read load by sixty to write the
|
|
25
|
+
* same number sixty times.
|
|
26
|
+
*/
|
|
27
|
+
const QUEUE_DEPTH_INTERVAL_MS = 15_000;
|
|
28
|
+
|
|
21
29
|
export type JobOutcome = 'completed' | 'suspended' | 'retried' | 'dead-lettered';
|
|
22
30
|
|
|
23
31
|
export interface JobExecution {
|
|
@@ -205,6 +213,29 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
205
213
|
let failed = 0;
|
|
206
214
|
let suspended = 0;
|
|
207
215
|
let deadLettered = 0;
|
|
216
|
+
let depthPublishedAt = Number.NEGATIVE_INFINITY;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* This package's ONE metrics call site: the `queue_depth` series `docker/helm`'s worker HPA
|
|
220
|
+
* scales on. `ready` and not `ready + delayed` — the gauge means "waiting to be picked up", and
|
|
221
|
+
* a job parked until Tuesday is not backlog no matter how many workers are added. Every queue
|
|
222
|
+
* the driver reports, not only the ones this process serves, because depth is the queue's fact
|
|
223
|
+
* and a queue no pod published is a queue no autoscaler can see.
|
|
224
|
+
*/
|
|
225
|
+
const publishQueueDepth = async (): Promise<void> => {
|
|
226
|
+
const now = nowMs(options.clock);
|
|
227
|
+
if (now - depthPublishedAt < QUEUE_DEPTH_INTERVAL_MS) return;
|
|
228
|
+
depthPublishedAt = now;
|
|
229
|
+
try {
|
|
230
|
+
for (const stat of await options.driver.stats()) recordQueueDepth(stat.queue, stat.ready);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// Instrumentation never costs a tick: a queue that cannot be measured must still be worked.
|
|
233
|
+
logger.warn('jobs.worker.depth-failed', {
|
|
234
|
+
workerId,
|
|
235
|
+
error: error instanceof Error ? error.message : String(error),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
208
239
|
|
|
209
240
|
const runClaimed = async (claimed: ClaimedJob): Promise<JobExecution> => {
|
|
210
241
|
const handle = getJob(claimed.name);
|
|
@@ -249,6 +280,7 @@ export function createWorker(options: WorkerOptions): Worker {
|
|
|
249
280
|
|
|
250
281
|
const tick = async (): Promise<readonly JobExecution[]> => {
|
|
251
282
|
if (state === 'draining' || state === 'stopped') return [];
|
|
283
|
+
await publishQueueDepth();
|
|
252
284
|
const results: JobExecution[] = [];
|
|
253
285
|
|
|
254
286
|
for (const queue of queues) {
|