@boringnode/queue 0.5.1 → 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 +103 -20
- package/build/chunk-6IO4P6RB.js +145 -0
- package/build/chunk-6IO4P6RB.js.map +1 -0
- package/build/{chunk-VHN3XZDC.js → chunk-AHUVTAI7.js} +278 -29
- 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 +34 -8
- package/build/index.js +187 -31
- package/build/index.js.map +1 -1
- package/build/{job-DImdhRFO.d.ts → job-C4oyCVxR.d.ts} +275 -15
- package/build/src/contracts/adapter.d.ts +1 -1
- package/build/src/drivers/fake_adapter.d.ts +12 -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 +166 -402
- 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/otel.js +3 -0
- package/build/src/otel.js.map +1 -1
- 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 +18 -19
- package/build/chunk-VHN3XZDC.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,361 +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 schedule_key = KEYS[1]
|
|
302
|
-
local now = tonumber(ARGV[1])
|
|
303
|
-
|
|
304
|
-
-- Get schedule data
|
|
305
|
-
local data = redis.call('HGETALL', schedule_key)
|
|
306
|
-
if #data == 0 then
|
|
307
|
-
return nil
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
-- Convert HGETALL result to table
|
|
311
|
-
local schedule = {}
|
|
312
|
-
for j = 1, #data, 2 do
|
|
313
|
-
schedule[data[j]] = data[j + 1]
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
-- Check if schedule is due
|
|
317
|
-
if schedule.status ~= 'active' then
|
|
318
|
-
return nil
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
local next_run_at = tonumber(schedule.next_run_at)
|
|
322
|
-
if not next_run_at or next_run_at > now then
|
|
323
|
-
return nil
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
local run_count = tonumber(schedule.run_count or '0')
|
|
327
|
-
local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
|
|
328
|
-
local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
|
|
329
|
-
|
|
330
|
-
-- Check limits
|
|
331
|
-
if run_limit and run_count >= run_limit then
|
|
332
|
-
return nil
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
if to_date and now > to_date then
|
|
336
|
-
return nil
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
-- This schedule is claimable - atomically update it
|
|
340
|
-
local new_run_count = run_count + 1
|
|
341
|
-
|
|
342
|
-
-- Calculate new next_run_at (simple interval-based for now)
|
|
343
|
-
-- Complex cron calculation happens in the caller
|
|
344
|
-
local new_next_run_at = ''
|
|
345
|
-
local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
|
|
346
|
-
if every_ms then
|
|
347
|
-
new_next_run_at = tostring(now + every_ms)
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
-- Check if we've hit the limit after this run
|
|
351
|
-
if run_limit and new_run_count >= run_limit then
|
|
352
|
-
new_next_run_at = ''
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
-- Check if past end date
|
|
356
|
-
if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
|
|
357
|
-
new_next_run_at = ''
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
-- Update the schedule atomically
|
|
361
|
-
redis.call('HSET', schedule_key,
|
|
362
|
-
'next_run_at', new_next_run_at,
|
|
363
|
-
'last_run_at', tostring(now),
|
|
364
|
-
'run_count', tostring(new_run_count))
|
|
365
|
-
|
|
366
|
-
-- Return the schedule data (before update) as JSON
|
|
367
|
-
return cjson.encode(schedule)
|
|
368
|
-
`;
|
|
369
31
|
function redis(config) {
|
|
370
32
|
return () => {
|
|
371
33
|
if (config instanceof Redis) {
|
|
@@ -396,12 +58,19 @@ var RedisAdapter = class {
|
|
|
396
58
|
pending: `${redisKey}::${queue}::pending`,
|
|
397
59
|
delayed: `${redisKey}::${queue}::delayed`,
|
|
398
60
|
active: `${redisKey}::${queue}::active`,
|
|
61
|
+
overlay: `${redisKey}::${queue}::metadata`,
|
|
399
62
|
completed: `${redisKey}::${queue}::completed`,
|
|
400
63
|
completedIndex: `${redisKey}::${queue}::completed::index`,
|
|
401
64
|
failed: `${redisKey}::${queue}::failed`,
|
|
402
65
|
failedIndex: `${redisKey}::${queue}::failed::index`
|
|
403
66
|
};
|
|
404
67
|
}
|
|
68
|
+
#getDedupKey(queue, dedupId) {
|
|
69
|
+
return `${this.#getDedupPrefix(queue)}${dedupId}`;
|
|
70
|
+
}
|
|
71
|
+
#getDedupPrefix(queue) {
|
|
72
|
+
return `${redisKey}::${queue}::dedup::`;
|
|
73
|
+
}
|
|
405
74
|
setWorkerId(workerId) {
|
|
406
75
|
this.#workerId = workerId;
|
|
407
76
|
}
|
|
@@ -418,59 +87,83 @@ var RedisAdapter = class {
|
|
|
418
87
|
const now = Date.now();
|
|
419
88
|
const result = await this.#connection.eval(
|
|
420
89
|
ACQUIRE_JOB_SCRIPT,
|
|
421
|
-
|
|
90
|
+
5,
|
|
422
91
|
keys.data,
|
|
423
92
|
keys.pending,
|
|
424
93
|
keys.active,
|
|
425
94
|
keys.delayed,
|
|
95
|
+
keys.overlay,
|
|
426
96
|
this.#workerId,
|
|
427
97
|
now.toString()
|
|
428
98
|
);
|
|
429
99
|
if (!result) {
|
|
430
100
|
return null;
|
|
431
101
|
}
|
|
432
|
-
|
|
102
|
+
const { data, overlay, acquiredAt } = JSON.parse(result);
|
|
103
|
+
return { ...hydrateRedisJob(data, overlay), acquiredAt };
|
|
433
104
|
}
|
|
434
105
|
async completeJob(jobId, queue, removeOnComplete) {
|
|
435
106
|
const keys = this.#getKeys(queue);
|
|
107
|
+
const dedupPrefix = this.#getDedupPrefix(queue);
|
|
436
108
|
const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete);
|
|
437
109
|
if (!keep) {
|
|
438
|
-
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
|
+
);
|
|
439
119
|
return;
|
|
440
120
|
}
|
|
441
121
|
await this.#connection.eval(
|
|
442
122
|
FINALIZE_JOB_SCRIPT,
|
|
443
|
-
|
|
123
|
+
5,
|
|
444
124
|
keys.data,
|
|
445
125
|
keys.active,
|
|
446
126
|
keys.completed,
|
|
447
127
|
keys.completedIndex,
|
|
128
|
+
keys.overlay,
|
|
448
129
|
jobId,
|
|
449
130
|
Date.now().toString(),
|
|
450
131
|
maxAge.toString(),
|
|
451
132
|
maxCount.toString(),
|
|
452
|
-
""
|
|
133
|
+
"",
|
|
134
|
+
dedupPrefix
|
|
453
135
|
);
|
|
454
136
|
}
|
|
455
137
|
async failJob(jobId, queue, error, removeOnFail) {
|
|
456
138
|
const keys = this.#getKeys(queue);
|
|
139
|
+
const dedupPrefix = this.#getDedupPrefix(queue);
|
|
457
140
|
const { keep, maxAge, maxCount } = resolveRetention(removeOnFail);
|
|
458
141
|
if (!keep) {
|
|
459
|
-
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
|
+
);
|
|
460
151
|
return;
|
|
461
152
|
}
|
|
462
153
|
await this.#connection.eval(
|
|
463
154
|
FINALIZE_JOB_SCRIPT,
|
|
464
|
-
|
|
155
|
+
5,
|
|
465
156
|
keys.data,
|
|
466
157
|
keys.active,
|
|
467
158
|
keys.failed,
|
|
468
159
|
keys.failedIndex,
|
|
160
|
+
keys.overlay,
|
|
469
161
|
jobId,
|
|
470
162
|
Date.now().toString(),
|
|
471
163
|
maxAge.toString(),
|
|
472
164
|
maxCount.toString(),
|
|
473
|
-
error?.message || ""
|
|
165
|
+
error?.message || "",
|
|
166
|
+
dedupPrefix
|
|
474
167
|
);
|
|
475
168
|
}
|
|
476
169
|
async retryJob(jobId, queue, retryAt) {
|
|
@@ -478,11 +171,12 @@ var RedisAdapter = class {
|
|
|
478
171
|
const now = Date.now();
|
|
479
172
|
await this.#connection.eval(
|
|
480
173
|
RETRY_JOB_SCRIPT,
|
|
481
|
-
|
|
174
|
+
5,
|
|
482
175
|
keys.data,
|
|
483
176
|
keys.active,
|
|
484
177
|
keys.pending,
|
|
485
178
|
keys.delayed,
|
|
179
|
+
keys.overlay,
|
|
486
180
|
jobId,
|
|
487
181
|
retryAt ? retryAt.getTime().toString() : "0",
|
|
488
182
|
now.toString()
|
|
@@ -492,19 +186,21 @@ var RedisAdapter = class {
|
|
|
492
186
|
const keys = this.#getKeys(queue);
|
|
493
187
|
const result = await this.#connection.eval(
|
|
494
188
|
GET_JOB_SCRIPT,
|
|
495
|
-
|
|
189
|
+
7,
|
|
496
190
|
keys.data,
|
|
497
191
|
keys.pending,
|
|
498
192
|
keys.delayed,
|
|
499
193
|
keys.active,
|
|
500
194
|
keys.completed,
|
|
501
195
|
keys.failed,
|
|
196
|
+
keys.overlay,
|
|
502
197
|
jobId
|
|
503
198
|
);
|
|
504
199
|
if (!result) {
|
|
505
200
|
return null;
|
|
506
201
|
}
|
|
507
|
-
|
|
202
|
+
const record = JSON.parse(result);
|
|
203
|
+
return { ...record, data: hydrateRedisJob(record.data, record.overlay) };
|
|
508
204
|
}
|
|
509
205
|
push(jobData) {
|
|
510
206
|
return this.pushOn("default", jobData);
|
|
@@ -515,11 +211,34 @@ var RedisAdapter = class {
|
|
|
515
211
|
async pushLaterOn(queue, jobData, delay) {
|
|
516
212
|
const keys = this.#getKeys(queue);
|
|
517
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
|
+
}
|
|
518
236
|
await this.#connection.eval(
|
|
519
237
|
PUSH_DELAYED_JOB_SCRIPT,
|
|
520
|
-
|
|
238
|
+
3,
|
|
521
239
|
keys.data,
|
|
522
240
|
keys.delayed,
|
|
241
|
+
keys.overlay,
|
|
523
242
|
jobData.id,
|
|
524
243
|
JSON.stringify(jobData),
|
|
525
244
|
executeAt.toString()
|
|
@@ -530,11 +249,34 @@ var RedisAdapter = class {
|
|
|
530
249
|
const priority = jobData.priority ?? DEFAULT_PRIORITY;
|
|
531
250
|
const timestamp = Date.now();
|
|
532
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
|
+
}
|
|
533
274
|
await this.#connection.eval(
|
|
534
275
|
PUSH_JOB_SCRIPT,
|
|
535
|
-
|
|
276
|
+
3,
|
|
536
277
|
keys.data,
|
|
537
278
|
keys.pending,
|
|
279
|
+
keys.overlay,
|
|
538
280
|
jobData.id,
|
|
539
281
|
JSON.stringify(jobData),
|
|
540
282
|
score.toString()
|
|
@@ -545,12 +287,16 @@ var RedisAdapter = class {
|
|
|
545
287
|
}
|
|
546
288
|
async pushManyOn(queue, jobs) {
|
|
547
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
|
+
}
|
|
548
293
|
const keys = this.#getKeys(queue);
|
|
549
294
|
const now = Date.now();
|
|
550
295
|
const multi = this.#connection.multi();
|
|
551
296
|
for (const job of jobs) {
|
|
552
297
|
const priority = job.priority ?? DEFAULT_PRIORITY;
|
|
553
298
|
const score = calculateScore(priority, now);
|
|
299
|
+
multi.hdel(keys.overlay, job.id);
|
|
554
300
|
multi.hset(keys.data, job.id, JSON.stringify(job));
|
|
555
301
|
multi.zadd(keys.pending, score, job.id);
|
|
556
302
|
}
|
|
@@ -568,16 +314,34 @@ var RedisAdapter = class {
|
|
|
568
314
|
const now = Date.now();
|
|
569
315
|
const recovered = await this.#connection.eval(
|
|
570
316
|
RECOVER_STALLED_JOBS_SCRIPT,
|
|
571
|
-
|
|
317
|
+
4,
|
|
572
318
|
keys.data,
|
|
573
319
|
keys.active,
|
|
574
320
|
keys.pending,
|
|
321
|
+
keys.overlay,
|
|
575
322
|
now.toString(),
|
|
576
323
|
stalledThreshold.toString(),
|
|
577
|
-
maxStalledCount.toString()
|
|
324
|
+
maxStalledCount.toString(),
|
|
325
|
+
this.#getDedupPrefix(queue)
|
|
578
326
|
);
|
|
579
327
|
return recovered;
|
|
580
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
|
+
}
|
|
581
345
|
async upsertSchedule(config) {
|
|
582
346
|
const id = config.id ?? randomUUID();
|
|
583
347
|
const now = Date.now();
|
|
@@ -665,40 +429,40 @@ var RedisAdapter = class {
|
|
|
665
429
|
}
|
|
666
430
|
async claimDueSchedule() {
|
|
667
431
|
const now = Date.now();
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
} else if (toDate && nextRun > toDate) {
|
|
695
|
-
newNextRunAt = "";
|
|
696
|
-
}
|
|
697
|
-
await this.#connection.hset(scheduleKey, "next_run_at", newNextRunAt.toString());
|
|
432
|
+
const result = await this.#connection.eval(
|
|
433
|
+
CLAIM_SCHEDULE_SCRIPT,
|
|
434
|
+
2,
|
|
435
|
+
schedulesIndexKey,
|
|
436
|
+
`${schedulesKey}::`,
|
|
437
|
+
now.toString()
|
|
438
|
+
);
|
|
439
|
+
if (!result) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const data = JSON.parse(result);
|
|
443
|
+
if (data.cron_expression) {
|
|
444
|
+
const { CronExpressionParser } = await import("cron-parser");
|
|
445
|
+
const cron = CronExpressionParser.parse(data.cron_expression, {
|
|
446
|
+
currentDate: new Date(now),
|
|
447
|
+
tz: data.timezone || "UTC"
|
|
448
|
+
});
|
|
449
|
+
const nextRun = cron.next().toDate().getTime();
|
|
450
|
+
const runCount = Number.parseInt(data.run_count || "0", 10) + 1;
|
|
451
|
+
const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null;
|
|
452
|
+
const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null;
|
|
453
|
+
let newNextRunAt = nextRun;
|
|
454
|
+
if (runLimit !== null && runCount >= runLimit) {
|
|
455
|
+
newNextRunAt = "";
|
|
456
|
+
} else if (toDate && nextRun > toDate) {
|
|
457
|
+
newNextRunAt = "";
|
|
698
458
|
}
|
|
699
|
-
|
|
459
|
+
await this.#connection.hset(
|
|
460
|
+
`${schedulesKey}::${data.id}`,
|
|
461
|
+
"next_run_at",
|
|
462
|
+
newNextRunAt.toString()
|
|
463
|
+
);
|
|
700
464
|
}
|
|
701
|
-
return
|
|
465
|
+
return this.#hashToScheduleData(data);
|
|
702
466
|
}
|
|
703
467
|
#hashToScheduleData(data) {
|
|
704
468
|
return {
|