@avada-falcon/worker-sdk 0.5.0 → 0.5.1
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 +1 -1
- package/src/worker/worker.js +34 -15
package/package.json
CHANGED
package/src/worker/worker.js
CHANGED
|
@@ -12,10 +12,17 @@ import {FileLogger} from '../logging/fileLogger.js';
|
|
|
12
12
|
import {LokiShipper} from '../logging/lokiShipper.js';
|
|
13
13
|
import {Heartbeat} from './heartbeat.js';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
const REQUEUE_COUNT_KEY = '__requeues';
|
|
17
|
-
const
|
|
15
|
+
/** Keys stored under job.data to track admission back-off across requeues. */
|
|
16
|
+
const REQUEUE_COUNT_KEY = '__requeues'; // how many times declined — for logging only
|
|
17
|
+
const REQUEUE_FIRST_KEY = '__firstDeclinedAt'; // epoch ms of the FIRST decline — drives the cap
|
|
18
18
|
const DEFAULT_REQUEUE_DELAY_MS = 200;
|
|
19
|
+
// Runaway BACKSTOP for a stuck admission gate (e.g. a leaked memory budget that never
|
|
20
|
+
// frees), NOT normal backpressure. Wall-clock based, not a requeue COUNT: a count cap of
|
|
21
|
+
// 50 × ~200ms was only ~15s of tolerance — far less than a single job's timeout (up to
|
|
22
|
+
// 540s) — so a job legitimately queued behind long-running jobs failed for merely waiting.
|
|
23
|
+
// 30 min is comfortably longer than any single job, so only a truly stuck fleet trips it.
|
|
24
|
+
// Override per job with jobConfig.maxRequeueWaitMs.
|
|
25
|
+
const DEFAULT_MAX_REQUEUE_WAIT_MS = 30 * 60 * 1000;
|
|
19
26
|
|
|
20
27
|
function requeueCount(job) {
|
|
21
28
|
const count = job.data?.[REQUEUE_COUNT_KEY];
|
|
@@ -83,7 +90,7 @@ export function createWorker(configPath) {
|
|
|
83
90
|
const handler = registry.get(job.name);
|
|
84
91
|
const jobConfig = config.jobs[job.name] || {};
|
|
85
92
|
const timeout = jobConfig.timeout || 30000;
|
|
86
|
-
const
|
|
93
|
+
const maxRequeueWaitMs = jobConfig.maxRequeueWaitMs ?? DEFAULT_MAX_REQUEUE_WAIT_MS;
|
|
87
94
|
|
|
88
95
|
// Let a handler DECLINE to run on this worker and hand the job back to the queue,
|
|
89
96
|
// delayed briefly, instead of blocking — a job blocked inside the handler stays
|
|
@@ -99,21 +106,31 @@ export function createWorker(configPath) {
|
|
|
99
106
|
// then returns normally makes BullMQ complete a job that is also sitting in the
|
|
100
107
|
// delayed set — it runs twice. Call it as `return ctx.requeue()` or let it throw.
|
|
101
108
|
const requeue = async (retryMs = DEFAULT_REQUEUE_DELAY_MS) => {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
const data = job.data || {};
|
|
111
|
+
const firstDeclinedAt = data[REQUEUE_FIRST_KEY] || now;
|
|
112
|
+
const waitedMs = now - firstDeclinedAt;
|
|
102
113
|
const count = requeueCount(job) + 1;
|
|
103
114
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
115
|
+
// A requeue burns no attempt, so a gate that never opens (leaked memory budget,
|
|
116
|
+
// gate bug) would ping-pong this job forever, silently. Fail loudly — but only
|
|
117
|
+
// after a long WALL-CLOCK wait, so a job legitimately queued behind long-running
|
|
118
|
+
// jobs (up to their timeout) is never failed for merely waiting; only a truly
|
|
119
|
+
// stuck gate trips this.
|
|
120
|
+
if (waitedMs > maxRequeueWaitMs) {
|
|
108
121
|
throw new Error(
|
|
109
|
-
`Job ${job.name}:${job.id}
|
|
110
|
-
`(
|
|
122
|
+
`Job ${job.name}:${job.id} not admitted after ${Math.round(waitedMs / 1000)}s ` +
|
|
123
|
+
`over ${count} tries (maxRequeueWaitMs: ${maxRequeueWaitMs}ms) — admission gate stuck.`
|
|
111
124
|
);
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
// Persist the
|
|
115
|
-
// goes back to Redis
|
|
116
|
-
await job.updateData({
|
|
127
|
+
// Persist across the re-schedule: the in-memory job.data copy is dropped once the
|
|
128
|
+
// job goes back to Redis. Keep the FIRST-decline stamp stable; bump the info count.
|
|
129
|
+
await job.updateData({
|
|
130
|
+
...data,
|
|
131
|
+
[REQUEUE_FIRST_KEY]: firstDeclinedAt,
|
|
132
|
+
[REQUEUE_COUNT_KEY]: count
|
|
133
|
+
});
|
|
117
134
|
|
|
118
135
|
// Jitter the delay. Without it, every job this worker declines wakes at the
|
|
119
136
|
// same instant and gets declined again in lockstep (thundering herd).
|
|
@@ -121,10 +138,12 @@ export function createWorker(configPath) {
|
|
|
121
138
|
|
|
122
139
|
console.warn(
|
|
123
140
|
`[worker-sdk] Declined ${job.name}:${job.id} — requeued in ${delay}ms ` +
|
|
124
|
-
`(${count}/${
|
|
141
|
+
`(try ${count}, waited ${Math.round(waitedMs / 1000)}s/${Math.round(
|
|
142
|
+
maxRequeueWaitMs / 1000
|
|
143
|
+
)}s)`
|
|
125
144
|
);
|
|
126
145
|
|
|
127
|
-
await job.moveToDelayed(
|
|
146
|
+
await job.moveToDelayed(now + delay, token);
|
|
128
147
|
throw new DelayedError();
|
|
129
148
|
};
|
|
130
149
|
|