@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.
Files changed (2) hide show
  1. package/package.json +5 -5
  2. package/src/worker.ts +33 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/jobs",
3
- "version": "1.1.0",
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.1.0",
34
- "@ultimat3/entity": "1.1.0",
35
- "@ultimat3/schema": "1.1.0",
36
- "@ultimat3/time": "1.1.0"
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) {