@boringnode/queue 0.5.2 → 0.6.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/README.md +94 -14
- package/build/chunk-6IO4P6RB.js +145 -0
- package/build/chunk-6IO4P6RB.js.map +1 -0
- package/build/{chunk-VRXHCWNK.js → chunk-AHUVTAI7.js} +220 -15
- package/build/chunk-AHUVTAI7.js.map +1 -0
- package/build/chunk-S37X3CBO.js +500 -0
- package/build/chunk-S37X3CBO.js.map +1 -0
- package/build/index.d.ts +10 -2
- package/build/index.js +187 -31
- package/build/index.js.map +1 -1
- package/build/{job-Z5fBSzRX.d.ts → job-C4oyCVxR.d.ts} +131 -10
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/fake_adapter.d.ts +7 -6
- package/build/src/drivers/fake_adapter.js +1 -1
- package/build/src/drivers/knex_adapter.d.ts +6 -5
- package/build/src/drivers/knex_adapter.js +112 -0
- package/build/src/drivers/knex_adapter.js.map +1 -1
- package/build/src/drivers/redis_adapter.d.ts +6 -5
- package/build/src/drivers/redis_adapter.js +134 -368
- package/build/src/drivers/redis_adapter.js.map +1 -1
- package/build/src/drivers/redis_job_storage.d.ts +17 -0
- package/build/src/drivers/redis_job_storage.js +14 -0
- package/build/src/drivers/redis_job_storage.js.map +1 -0
- package/build/src/drivers/redis_scripts.d.ts +87 -0
- package/build/src/drivers/redis_scripts.js +29 -0
- package/build/src/drivers/redis_scripts.js.map +1 -0
- package/build/src/drivers/sync_adapter.d.ts +2 -1
- package/build/src/drivers/sync_adapter.js +7 -1
- package/build/src/drivers/sync_adapter.js.map +1 -1
- package/build/src/otel.d.ts +2 -2
- package/build/src/types/index.d.ts +1 -1
- package/build/src/types/main.d.ts +1 -1
- package/build/src/types/tracing_channels.d.ts +7 -1
- package/package.json +17 -17
- package/build/chunk-VRXHCWNK.js.map +0 -1
|
@@ -3,6 +3,23 @@ import {
|
|
|
3
3
|
calculateScore,
|
|
4
4
|
resolveRetention
|
|
5
5
|
} from "../../chunk-QEFYHCL7.js";
|
|
6
|
+
import {
|
|
7
|
+
ACQUIRE_JOB_SCRIPT,
|
|
8
|
+
CLAIM_SCHEDULE_SCRIPT,
|
|
9
|
+
FINALIZE_JOB_SCRIPT,
|
|
10
|
+
GET_JOB_SCRIPT,
|
|
11
|
+
PUSH_DEDUP_JOB_SCRIPT,
|
|
12
|
+
PUSH_DELAYED_JOB_SCRIPT,
|
|
13
|
+
PUSH_JOB_SCRIPT,
|
|
14
|
+
RECOVER_STALLED_JOBS_SCRIPT,
|
|
15
|
+
REMOVE_JOB_SCRIPT,
|
|
16
|
+
RENEW_JOBS_SCRIPT,
|
|
17
|
+
RETRY_JOB_SCRIPT
|
|
18
|
+
} from "../../chunk-S37X3CBO.js";
|
|
19
|
+
import {
|
|
20
|
+
encodeRedisJobPayloadOverlay,
|
|
21
|
+
hydrateRedisJob
|
|
22
|
+
} from "../../chunk-6IO4P6RB.js";
|
|
6
23
|
import "../../chunk-PZ5AY32C.js";
|
|
7
24
|
|
|
8
25
|
// src/drivers/redis_adapter.ts
|
|
@@ -11,359 +28,6 @@ import { Redis } from "ioredis";
|
|
|
11
28
|
var redisKey = "jobs";
|
|
12
29
|
var schedulesKey = "schedules";
|
|
13
30
|
var schedulesIndexKey = "schedules::index";
|
|
14
|
-
var PUSH_JOB_SCRIPT = `
|
|
15
|
-
local data_key = KEYS[1]
|
|
16
|
-
local pending_key = KEYS[2]
|
|
17
|
-
local job_id = ARGV[1]
|
|
18
|
-
local job_data = ARGV[2]
|
|
19
|
-
local score = tonumber(ARGV[3])
|
|
20
|
-
|
|
21
|
-
redis.call('HSET', data_key, job_id, job_data)
|
|
22
|
-
redis.call('ZADD', pending_key, score, job_id)
|
|
23
|
-
|
|
24
|
-
return 1
|
|
25
|
-
`;
|
|
26
|
-
var PUSH_DELAYED_JOB_SCRIPT = `
|
|
27
|
-
local data_key = KEYS[1]
|
|
28
|
-
local delayed_key = KEYS[2]
|
|
29
|
-
local job_id = ARGV[1]
|
|
30
|
-
local job_data = ARGV[2]
|
|
31
|
-
local execute_at = tonumber(ARGV[3])
|
|
32
|
-
|
|
33
|
-
redis.call('HSET', data_key, job_id, job_data)
|
|
34
|
-
redis.call('ZADD', delayed_key, execute_at, job_id)
|
|
35
|
-
|
|
36
|
-
return 1
|
|
37
|
-
`;
|
|
38
|
-
var ACQUIRE_JOB_SCRIPT = `
|
|
39
|
-
local data_key = KEYS[1]
|
|
40
|
-
local pending_key = KEYS[2]
|
|
41
|
-
local active_key = KEYS[3]
|
|
42
|
-
local delayed_key = KEYS[4]
|
|
43
|
-
local worker_id = ARGV[1]
|
|
44
|
-
local now = tonumber(ARGV[2])
|
|
45
|
-
|
|
46
|
-
-- Process delayed jobs: move ready jobs to pending
|
|
47
|
-
local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)
|
|
48
|
-
if #ready_job_ids > 0 then
|
|
49
|
-
for i = 1, #ready_job_ids do
|
|
50
|
-
local job_id = ready_job_ids[i]
|
|
51
|
-
local job_data = redis.call('HGET', data_key, job_id)
|
|
52
|
-
if job_data then
|
|
53
|
-
local job = cjson.decode(job_data)
|
|
54
|
-
local priority = job.priority or 5
|
|
55
|
-
local score = priority * 10000000000000 + now
|
|
56
|
-
redis.call('ZADD', pending_key, score, job_id)
|
|
57
|
-
redis.call('ZREM', delayed_key, job_id)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
-- Pop highest priority job (lowest score)
|
|
63
|
-
local result = redis.call('ZPOPMIN', pending_key)
|
|
64
|
-
if not result or #result == 0 then
|
|
65
|
-
return nil
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
local job_id = result[1]
|
|
69
|
-
local job_data = redis.call('HGET', data_key, job_id)
|
|
70
|
-
if not job_data then
|
|
71
|
-
return nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
-- Store in active hash (without data, it's in data_key)
|
|
75
|
-
local active_data = cjson.encode({
|
|
76
|
-
workerId = worker_id,
|
|
77
|
-
acquiredAt = now
|
|
78
|
-
})
|
|
79
|
-
redis.call('HSET', active_key, job_id, active_data)
|
|
80
|
-
|
|
81
|
-
-- Return job with acquiredAt
|
|
82
|
-
local job = cjson.decode(job_data)
|
|
83
|
-
job.acquiredAt = now
|
|
84
|
-
return cjson.encode(job)
|
|
85
|
-
`;
|
|
86
|
-
var REMOVE_JOB_SCRIPT = `
|
|
87
|
-
local data_key = KEYS[1]
|
|
88
|
-
local active_key = KEYS[2]
|
|
89
|
-
local job_id = ARGV[1]
|
|
90
|
-
|
|
91
|
-
if redis.call('HEXISTS', active_key, job_id) == 0 then
|
|
92
|
-
return 0
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
redis.call('HDEL', active_key, job_id)
|
|
96
|
-
redis.call('HDEL', data_key, job_id)
|
|
97
|
-
|
|
98
|
-
return 1
|
|
99
|
-
`;
|
|
100
|
-
var FINALIZE_JOB_SCRIPT = `
|
|
101
|
-
local data_key = KEYS[1]
|
|
102
|
-
local active_key = KEYS[2]
|
|
103
|
-
local history_key = KEYS[3]
|
|
104
|
-
local index_key = KEYS[4]
|
|
105
|
-
local job_id = ARGV[1]
|
|
106
|
-
local now = tonumber(ARGV[2])
|
|
107
|
-
local max_age = tonumber(ARGV[3])
|
|
108
|
-
local max_count = tonumber(ARGV[4])
|
|
109
|
-
local error_message = ARGV[5]
|
|
110
|
-
|
|
111
|
-
-- Verify job is active
|
|
112
|
-
if redis.call('HEXISTS', active_key, job_id) == 0 then
|
|
113
|
-
return 0
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
-- Remove from active
|
|
117
|
-
redis.call('HDEL', active_key, job_id)
|
|
118
|
-
|
|
119
|
-
-- Store finalization info (data stays in data_key)
|
|
120
|
-
local record = {
|
|
121
|
-
finishedAt = now
|
|
122
|
-
}
|
|
123
|
-
if error_message and error_message ~= '' then
|
|
124
|
-
record.error = error_message
|
|
125
|
-
end
|
|
126
|
-
redis.call('HSET', history_key, job_id, cjson.encode(record))
|
|
127
|
-
redis.call('ZADD', index_key, now, job_id)
|
|
128
|
-
|
|
129
|
-
-- Prune by age
|
|
130
|
-
if max_age and max_age > 0 then
|
|
131
|
-
local cutoff = now - max_age
|
|
132
|
-
local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)
|
|
133
|
-
if #expired > 0 then
|
|
134
|
-
redis.call('ZREM', index_key, unpack(expired))
|
|
135
|
-
redis.call('HDEL', history_key, unpack(expired))
|
|
136
|
-
redis.call('HDEL', data_key, unpack(expired))
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
-- Prune by count
|
|
141
|
-
if max_count and max_count > 0 then
|
|
142
|
-
local size = tonumber(redis.call('ZCARD', index_key))
|
|
143
|
-
if size > max_count then
|
|
144
|
-
local excess = size - max_count
|
|
145
|
-
local stale = redis.call('ZRANGE', index_key, 0, excess - 1)
|
|
146
|
-
if #stale > 0 then
|
|
147
|
-
redis.call('ZREM', index_key, unpack(stale))
|
|
148
|
-
redis.call('HDEL', history_key, unpack(stale))
|
|
149
|
-
redis.call('HDEL', data_key, unpack(stale))
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
return 1
|
|
155
|
-
`;
|
|
156
|
-
var RETRY_JOB_SCRIPT = `
|
|
157
|
-
local data_key = KEYS[1]
|
|
158
|
-
local active_key = KEYS[2]
|
|
159
|
-
local pending_key = KEYS[3]
|
|
160
|
-
local delayed_key = KEYS[4]
|
|
161
|
-
local job_id = ARGV[1]
|
|
162
|
-
local retry_at = tonumber(ARGV[2])
|
|
163
|
-
local now = tonumber(ARGV[3])
|
|
164
|
-
|
|
165
|
-
-- Verify job is active
|
|
166
|
-
if redis.call('HEXISTS', active_key, job_id) == 0 then
|
|
167
|
-
return 0
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
-- Get job data
|
|
171
|
-
local job_data = redis.call('HGET', data_key, job_id)
|
|
172
|
-
if not job_data then
|
|
173
|
-
return 0
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
-- Remove from active
|
|
177
|
-
redis.call('HDEL', active_key, job_id)
|
|
178
|
-
|
|
179
|
-
-- Increment attempts and update data
|
|
180
|
-
local job = cjson.decode(job_data)
|
|
181
|
-
job.attempts = (job.attempts or 0) + 1
|
|
182
|
-
redis.call('HSET', data_key, job_id, cjson.encode(job))
|
|
183
|
-
|
|
184
|
-
-- Add back to pending or delayed
|
|
185
|
-
if retry_at and retry_at > now then
|
|
186
|
-
redis.call('ZADD', delayed_key, retry_at, job_id)
|
|
187
|
-
else
|
|
188
|
-
-- Score = priority * 1e13 + timestamp
|
|
189
|
-
-- Lower score = higher priority, FIFO within same priority
|
|
190
|
-
local priority = job.priority or 5
|
|
191
|
-
local score = priority * 10000000000000 + now
|
|
192
|
-
redis.call('ZADD', pending_key, score, job_id)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
return 1
|
|
196
|
-
`;
|
|
197
|
-
var RECOVER_STALLED_JOBS_SCRIPT = `
|
|
198
|
-
local data_key = KEYS[1]
|
|
199
|
-
local active_key = KEYS[2]
|
|
200
|
-
local pending_key = KEYS[3]
|
|
201
|
-
local now = tonumber(ARGV[1])
|
|
202
|
-
local stalled_threshold = tonumber(ARGV[2])
|
|
203
|
-
local max_stalled_count = tonumber(ARGV[3])
|
|
204
|
-
|
|
205
|
-
local recovered = 0
|
|
206
|
-
local stalled_cutoff = now - stalled_threshold
|
|
207
|
-
|
|
208
|
-
-- Get all active jobs
|
|
209
|
-
local active_jobs = redis.call('HGETALL', active_key)
|
|
210
|
-
|
|
211
|
-
-- HGETALL returns [field1, value1, field2, value2, ...]
|
|
212
|
-
for i = 1, #active_jobs, 2 do
|
|
213
|
-
local job_id = active_jobs[i]
|
|
214
|
-
local active_data = active_jobs[i + 1]
|
|
215
|
-
local active = cjson.decode(active_data)
|
|
216
|
-
|
|
217
|
-
-- Check if job is stalled
|
|
218
|
-
if active.acquiredAt < stalled_cutoff then
|
|
219
|
-
local job_data = redis.call('HGET', data_key, job_id)
|
|
220
|
-
if job_data then
|
|
221
|
-
local job = cjson.decode(job_data)
|
|
222
|
-
local current_stalled_count = job.stalledCount or 0
|
|
223
|
-
|
|
224
|
-
-- Remove from active hash
|
|
225
|
-
redis.call('HDEL', active_key, job_id)
|
|
226
|
-
|
|
227
|
-
-- Check if job has exceeded max stalled count
|
|
228
|
-
if current_stalled_count >= max_stalled_count then
|
|
229
|
-
-- Job failed permanently, remove data too
|
|
230
|
-
redis.call('HDEL', data_key, job_id)
|
|
231
|
-
else
|
|
232
|
-
-- Recover: increment stalledCount and put back in pending
|
|
233
|
-
job.stalledCount = current_stalled_count + 1
|
|
234
|
-
redis.call('HSET', data_key, job_id, cjson.encode(job))
|
|
235
|
-
-- Score = priority * 1e13 + timestamp
|
|
236
|
-
local priority = job.priority or 5
|
|
237
|
-
local score = priority * 10000000000000 + now
|
|
238
|
-
redis.call('ZADD', pending_key, score, job_id)
|
|
239
|
-
recovered = recovered + 1
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
return recovered
|
|
246
|
-
`;
|
|
247
|
-
var GET_JOB_SCRIPT = `
|
|
248
|
-
local data_key = KEYS[1]
|
|
249
|
-
local pending_key = KEYS[2]
|
|
250
|
-
local delayed_key = KEYS[3]
|
|
251
|
-
local active_key = KEYS[4]
|
|
252
|
-
local completed_key = KEYS[5]
|
|
253
|
-
local failed_key = KEYS[6]
|
|
254
|
-
local job_id = ARGV[1]
|
|
255
|
-
|
|
256
|
-
local job_data = redis.call('HGET', data_key, job_id)
|
|
257
|
-
if not job_data then
|
|
258
|
-
return nil
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
local status = nil
|
|
262
|
-
local finished_at = nil
|
|
263
|
-
local error_msg = nil
|
|
264
|
-
|
|
265
|
-
-- Check status in order
|
|
266
|
-
if redis.call('HEXISTS', active_key, job_id) == 1 then
|
|
267
|
-
status = 'active'
|
|
268
|
-
elseif redis.call('ZSCORE', pending_key, job_id) then
|
|
269
|
-
status = 'pending'
|
|
270
|
-
elseif redis.call('ZSCORE', delayed_key, job_id) then
|
|
271
|
-
status = 'delayed'
|
|
272
|
-
else
|
|
273
|
-
local completed_data = redis.call('HGET', completed_key, job_id)
|
|
274
|
-
if completed_data then
|
|
275
|
-
status = 'completed'
|
|
276
|
-
local record = cjson.decode(completed_data)
|
|
277
|
-
finished_at = record.finishedAt
|
|
278
|
-
else
|
|
279
|
-
local failed_data = redis.call('HGET', failed_key, job_id)
|
|
280
|
-
if failed_data then
|
|
281
|
-
status = 'failed'
|
|
282
|
-
local record = cjson.decode(failed_data)
|
|
283
|
-
finished_at = record.finishedAt
|
|
284
|
-
error_msg = record.error
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
if not status then
|
|
290
|
-
return nil
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
return cjson.encode({
|
|
294
|
-
status = status,
|
|
295
|
-
data = cjson.decode(job_data),
|
|
296
|
-
finishedAt = finished_at,
|
|
297
|
-
error = error_msg
|
|
298
|
-
})
|
|
299
|
-
`;
|
|
300
|
-
var CLAIM_SCHEDULE_SCRIPT = `
|
|
301
|
-
local schedules_index_key = KEYS[1]
|
|
302
|
-
local schedule_key_prefix = KEYS[2]
|
|
303
|
-
local now = tonumber(ARGV[1])
|
|
304
|
-
|
|
305
|
-
local ids = redis.call('SMEMBERS', schedules_index_key)
|
|
306
|
-
|
|
307
|
-
for i = 1, #ids do
|
|
308
|
-
local schedule_key = schedule_key_prefix .. ids[i]
|
|
309
|
-
|
|
310
|
-
-- Get schedule data
|
|
311
|
-
local data = redis.call('HGETALL', schedule_key)
|
|
312
|
-
if #data > 0 then
|
|
313
|
-
-- Convert HGETALL result to table
|
|
314
|
-
local schedule = {}
|
|
315
|
-
for j = 1, #data, 2 do
|
|
316
|
-
schedule[data[j]] = data[j + 1]
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
-- Check if schedule is due
|
|
320
|
-
if schedule.status == 'active' then
|
|
321
|
-
local next_run_at = tonumber(schedule.next_run_at)
|
|
322
|
-
|
|
323
|
-
if next_run_at and next_run_at <= now then
|
|
324
|
-
local run_count = tonumber(schedule.run_count or '0')
|
|
325
|
-
local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
|
|
326
|
-
local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
|
|
327
|
-
|
|
328
|
-
-- Check limits
|
|
329
|
-
if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then
|
|
330
|
-
-- This schedule is claimable - atomically update it
|
|
331
|
-
local new_run_count = run_count + 1
|
|
332
|
-
|
|
333
|
-
-- Calculate new next_run_at (simple interval-based for now)
|
|
334
|
-
-- Complex cron calculation happens in the caller
|
|
335
|
-
local new_next_run_at = ''
|
|
336
|
-
local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
|
|
337
|
-
if every_ms then
|
|
338
|
-
new_next_run_at = tostring(now + every_ms)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
-- Check if we've hit the limit after this run
|
|
342
|
-
if run_limit and new_run_count >= run_limit then
|
|
343
|
-
new_next_run_at = ''
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
-- Check if past end date
|
|
347
|
-
if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
|
|
348
|
-
new_next_run_at = ''
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
-- Update the schedule atomically
|
|
352
|
-
redis.call('HSET', schedule_key,
|
|
353
|
-
'next_run_at', new_next_run_at,
|
|
354
|
-
'last_run_at', tostring(now),
|
|
355
|
-
'run_count', tostring(new_run_count))
|
|
356
|
-
|
|
357
|
-
-- Return the schedule data (before update) as JSON
|
|
358
|
-
return cjson.encode(schedule)
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
return nil
|
|
366
|
-
`;
|
|
367
31
|
function redis(config) {
|
|
368
32
|
return () => {
|
|
369
33
|
if (config instanceof Redis) {
|
|
@@ -394,12 +58,19 @@ var RedisAdapter = class {
|
|
|
394
58
|
pending: `${redisKey}::${queue}::pending`,
|
|
395
59
|
delayed: `${redisKey}::${queue}::delayed`,
|
|
396
60
|
active: `${redisKey}::${queue}::active`,
|
|
61
|
+
overlay: `${redisKey}::${queue}::metadata`,
|
|
397
62
|
completed: `${redisKey}::${queue}::completed`,
|
|
398
63
|
completedIndex: `${redisKey}::${queue}::completed::index`,
|
|
399
64
|
failed: `${redisKey}::${queue}::failed`,
|
|
400
65
|
failedIndex: `${redisKey}::${queue}::failed::index`
|
|
401
66
|
};
|
|
402
67
|
}
|
|
68
|
+
#getDedupKey(queue, dedupId) {
|
|
69
|
+
return `${this.#getDedupPrefix(queue)}${dedupId}`;
|
|
70
|
+
}
|
|
71
|
+
#getDedupPrefix(queue) {
|
|
72
|
+
return `${redisKey}::${queue}::dedup::`;
|
|
73
|
+
}
|
|
403
74
|
setWorkerId(workerId) {
|
|
404
75
|
this.#workerId = workerId;
|
|
405
76
|
}
|
|
@@ -416,59 +87,83 @@ var RedisAdapter = class {
|
|
|
416
87
|
const now = Date.now();
|
|
417
88
|
const result = await this.#connection.eval(
|
|
418
89
|
ACQUIRE_JOB_SCRIPT,
|
|
419
|
-
|
|
90
|
+
5,
|
|
420
91
|
keys.data,
|
|
421
92
|
keys.pending,
|
|
422
93
|
keys.active,
|
|
423
94
|
keys.delayed,
|
|
95
|
+
keys.overlay,
|
|
424
96
|
this.#workerId,
|
|
425
97
|
now.toString()
|
|
426
98
|
);
|
|
427
99
|
if (!result) {
|
|
428
100
|
return null;
|
|
429
101
|
}
|
|
430
|
-
|
|
102
|
+
const { data, overlay, acquiredAt } = JSON.parse(result);
|
|
103
|
+
return { ...hydrateRedisJob(data, overlay), acquiredAt };
|
|
431
104
|
}
|
|
432
105
|
async completeJob(jobId, queue, removeOnComplete) {
|
|
433
106
|
const keys = this.#getKeys(queue);
|
|
107
|
+
const dedupPrefix = this.#getDedupPrefix(queue);
|
|
434
108
|
const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete);
|
|
435
109
|
if (!keep) {
|
|
436
|
-
await this.#connection.eval(
|
|
110
|
+
await this.#connection.eval(
|
|
111
|
+
REMOVE_JOB_SCRIPT,
|
|
112
|
+
3,
|
|
113
|
+
keys.data,
|
|
114
|
+
keys.active,
|
|
115
|
+
keys.overlay,
|
|
116
|
+
jobId,
|
|
117
|
+
dedupPrefix
|
|
118
|
+
);
|
|
437
119
|
return;
|
|
438
120
|
}
|
|
439
121
|
await this.#connection.eval(
|
|
440
122
|
FINALIZE_JOB_SCRIPT,
|
|
441
|
-
|
|
123
|
+
5,
|
|
442
124
|
keys.data,
|
|
443
125
|
keys.active,
|
|
444
126
|
keys.completed,
|
|
445
127
|
keys.completedIndex,
|
|
128
|
+
keys.overlay,
|
|
446
129
|
jobId,
|
|
447
130
|
Date.now().toString(),
|
|
448
131
|
maxAge.toString(),
|
|
449
132
|
maxCount.toString(),
|
|
450
|
-
""
|
|
133
|
+
"",
|
|
134
|
+
dedupPrefix
|
|
451
135
|
);
|
|
452
136
|
}
|
|
453
137
|
async failJob(jobId, queue, error, removeOnFail) {
|
|
454
138
|
const keys = this.#getKeys(queue);
|
|
139
|
+
const dedupPrefix = this.#getDedupPrefix(queue);
|
|
455
140
|
const { keep, maxAge, maxCount } = resolveRetention(removeOnFail);
|
|
456
141
|
if (!keep) {
|
|
457
|
-
await this.#connection.eval(
|
|
142
|
+
await this.#connection.eval(
|
|
143
|
+
REMOVE_JOB_SCRIPT,
|
|
144
|
+
3,
|
|
145
|
+
keys.data,
|
|
146
|
+
keys.active,
|
|
147
|
+
keys.overlay,
|
|
148
|
+
jobId,
|
|
149
|
+
dedupPrefix
|
|
150
|
+
);
|
|
458
151
|
return;
|
|
459
152
|
}
|
|
460
153
|
await this.#connection.eval(
|
|
461
154
|
FINALIZE_JOB_SCRIPT,
|
|
462
|
-
|
|
155
|
+
5,
|
|
463
156
|
keys.data,
|
|
464
157
|
keys.active,
|
|
465
158
|
keys.failed,
|
|
466
159
|
keys.failedIndex,
|
|
160
|
+
keys.overlay,
|
|
467
161
|
jobId,
|
|
468
162
|
Date.now().toString(),
|
|
469
163
|
maxAge.toString(),
|
|
470
164
|
maxCount.toString(),
|
|
471
|
-
error?.message || ""
|
|
165
|
+
error?.message || "",
|
|
166
|
+
dedupPrefix
|
|
472
167
|
);
|
|
473
168
|
}
|
|
474
169
|
async retryJob(jobId, queue, retryAt) {
|
|
@@ -476,11 +171,12 @@ var RedisAdapter = class {
|
|
|
476
171
|
const now = Date.now();
|
|
477
172
|
await this.#connection.eval(
|
|
478
173
|
RETRY_JOB_SCRIPT,
|
|
479
|
-
|
|
174
|
+
5,
|
|
480
175
|
keys.data,
|
|
481
176
|
keys.active,
|
|
482
177
|
keys.pending,
|
|
483
178
|
keys.delayed,
|
|
179
|
+
keys.overlay,
|
|
484
180
|
jobId,
|
|
485
181
|
retryAt ? retryAt.getTime().toString() : "0",
|
|
486
182
|
now.toString()
|
|
@@ -490,19 +186,21 @@ var RedisAdapter = class {
|
|
|
490
186
|
const keys = this.#getKeys(queue);
|
|
491
187
|
const result = await this.#connection.eval(
|
|
492
188
|
GET_JOB_SCRIPT,
|
|
493
|
-
|
|
189
|
+
7,
|
|
494
190
|
keys.data,
|
|
495
191
|
keys.pending,
|
|
496
192
|
keys.delayed,
|
|
497
193
|
keys.active,
|
|
498
194
|
keys.completed,
|
|
499
195
|
keys.failed,
|
|
196
|
+
keys.overlay,
|
|
500
197
|
jobId
|
|
501
198
|
);
|
|
502
199
|
if (!result) {
|
|
503
200
|
return null;
|
|
504
201
|
}
|
|
505
|
-
|
|
202
|
+
const record = JSON.parse(result);
|
|
203
|
+
return { ...record, data: hydrateRedisJob(record.data, record.overlay) };
|
|
506
204
|
}
|
|
507
205
|
push(jobData) {
|
|
508
206
|
return this.pushOn("default", jobData);
|
|
@@ -513,11 +211,34 @@ var RedisAdapter = class {
|
|
|
513
211
|
async pushLaterOn(queue, jobData, delay) {
|
|
514
212
|
const keys = this.#getKeys(queue);
|
|
515
213
|
const executeAt = Date.now() + delay;
|
|
214
|
+
if (jobData.dedup) {
|
|
215
|
+
const dedupKey = this.#getDedupKey(queue, jobData.dedup.id);
|
|
216
|
+
const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload);
|
|
217
|
+
const result = await this.#connection.eval(
|
|
218
|
+
PUSH_DEDUP_JOB_SCRIPT,
|
|
219
|
+
5,
|
|
220
|
+
keys.data,
|
|
221
|
+
keys.delayed,
|
|
222
|
+
dedupKey,
|
|
223
|
+
keys.pending,
|
|
224
|
+
keys.overlay,
|
|
225
|
+
jobData.id,
|
|
226
|
+
JSON.stringify(jobData),
|
|
227
|
+
executeAt.toString(),
|
|
228
|
+
(jobData.dedup.ttl ?? 0).toString(),
|
|
229
|
+
jobData.dedup.extend ? "1" : "0",
|
|
230
|
+
jobData.dedup.replace ? "1" : "0",
|
|
231
|
+
payloadData,
|
|
232
|
+
payloadIsUndefined
|
|
233
|
+
);
|
|
234
|
+
return { outcome: result[0], jobId: result[1] };
|
|
235
|
+
}
|
|
516
236
|
await this.#connection.eval(
|
|
517
237
|
PUSH_DELAYED_JOB_SCRIPT,
|
|
518
|
-
|
|
238
|
+
3,
|
|
519
239
|
keys.data,
|
|
520
240
|
keys.delayed,
|
|
241
|
+
keys.overlay,
|
|
521
242
|
jobData.id,
|
|
522
243
|
JSON.stringify(jobData),
|
|
523
244
|
executeAt.toString()
|
|
@@ -528,11 +249,34 @@ var RedisAdapter = class {
|
|
|
528
249
|
const priority = jobData.priority ?? DEFAULT_PRIORITY;
|
|
529
250
|
const timestamp = Date.now();
|
|
530
251
|
const score = calculateScore(priority, timestamp);
|
|
252
|
+
if (jobData.dedup) {
|
|
253
|
+
const dedupKey = this.#getDedupKey(queue, jobData.dedup.id);
|
|
254
|
+
const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload);
|
|
255
|
+
const result = await this.#connection.eval(
|
|
256
|
+
PUSH_DEDUP_JOB_SCRIPT,
|
|
257
|
+
5,
|
|
258
|
+
keys.data,
|
|
259
|
+
keys.pending,
|
|
260
|
+
dedupKey,
|
|
261
|
+
keys.delayed,
|
|
262
|
+
keys.overlay,
|
|
263
|
+
jobData.id,
|
|
264
|
+
JSON.stringify(jobData),
|
|
265
|
+
score.toString(),
|
|
266
|
+
(jobData.dedup.ttl ?? 0).toString(),
|
|
267
|
+
jobData.dedup.extend ? "1" : "0",
|
|
268
|
+
jobData.dedup.replace ? "1" : "0",
|
|
269
|
+
payloadData,
|
|
270
|
+
payloadIsUndefined
|
|
271
|
+
);
|
|
272
|
+
return { outcome: result[0], jobId: result[1] };
|
|
273
|
+
}
|
|
531
274
|
await this.#connection.eval(
|
|
532
275
|
PUSH_JOB_SCRIPT,
|
|
533
|
-
|
|
276
|
+
3,
|
|
534
277
|
keys.data,
|
|
535
278
|
keys.pending,
|
|
279
|
+
keys.overlay,
|
|
536
280
|
jobData.id,
|
|
537
281
|
JSON.stringify(jobData),
|
|
538
282
|
score.toString()
|
|
@@ -543,12 +287,16 @@ var RedisAdapter = class {
|
|
|
543
287
|
}
|
|
544
288
|
async pushManyOn(queue, jobs) {
|
|
545
289
|
if (jobs.length === 0) return;
|
|
290
|
+
if (jobs.some((j) => j.dedup)) {
|
|
291
|
+
throw new Error("dedup is not supported in batch dispatch; use single dispatch");
|
|
292
|
+
}
|
|
546
293
|
const keys = this.#getKeys(queue);
|
|
547
294
|
const now = Date.now();
|
|
548
295
|
const multi = this.#connection.multi();
|
|
549
296
|
for (const job of jobs) {
|
|
550
297
|
const priority = job.priority ?? DEFAULT_PRIORITY;
|
|
551
298
|
const score = calculateScore(priority, now);
|
|
299
|
+
multi.hdel(keys.overlay, job.id);
|
|
552
300
|
multi.hset(keys.data, job.id, JSON.stringify(job));
|
|
553
301
|
multi.zadd(keys.pending, score, job.id);
|
|
554
302
|
}
|
|
@@ -566,16 +314,34 @@ var RedisAdapter = class {
|
|
|
566
314
|
const now = Date.now();
|
|
567
315
|
const recovered = await this.#connection.eval(
|
|
568
316
|
RECOVER_STALLED_JOBS_SCRIPT,
|
|
569
|
-
|
|
317
|
+
4,
|
|
570
318
|
keys.data,
|
|
571
319
|
keys.active,
|
|
572
320
|
keys.pending,
|
|
321
|
+
keys.overlay,
|
|
573
322
|
now.toString(),
|
|
574
323
|
stalledThreshold.toString(),
|
|
575
|
-
maxStalledCount.toString()
|
|
324
|
+
maxStalledCount.toString(),
|
|
325
|
+
this.#getDedupPrefix(queue)
|
|
576
326
|
);
|
|
577
327
|
return recovered;
|
|
578
328
|
}
|
|
329
|
+
async renewJobs(queue, jobIds) {
|
|
330
|
+
if (jobIds.length === 0) {
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
const keys = this.#getKeys(queue);
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const renewed = await this.#connection.eval(
|
|
336
|
+
RENEW_JOBS_SCRIPT,
|
|
337
|
+
1,
|
|
338
|
+
keys.active,
|
|
339
|
+
now.toString(),
|
|
340
|
+
this.#workerId,
|
|
341
|
+
...jobIds
|
|
342
|
+
);
|
|
343
|
+
return renewed;
|
|
344
|
+
}
|
|
579
345
|
async upsertSchedule(config) {
|
|
580
346
|
const id = config.id ?? randomUUID();
|
|
581
347
|
const now = Date.now();
|