toro-queue 0.1.0__py3-none-any.whl

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.
toro/scripts.py ADDED
@@ -0,0 +1,433 @@
1
+ """Lua scripts for atomic state transitions.
2
+
3
+ Every job state change goes through one of these scripts so transitions are
4
+ atomic on the Redis server: no client-side race windows between "check state"
5
+ and "act on state".
6
+
7
+ Job ordering is a single GLOBAL priority order: every waiting job lives in the
8
+ `prioritized` ZSET, scored by (priority, sequence). Higher `priority` number =
9
+ more urgent; ties break FIFO by an enqueue sequence counter (`pc`). There is no
10
+ separate "wait" fast lane — `priority 0` (the default) is simply the least urgent
11
+ band, ordered FIFO among itself.
12
+
13
+ Wakeup uses a base marker: producers do an idempotent `ZADD marker 0 "0"`, and an
14
+ idle worker blocks on `BZPOPMIN marker`. The marker only signals "there may be
15
+ work"; the actual job move is the atomic `MOVE_TO_ACTIVE` (ZPOPMIN prioritized ->
16
+ active -> lock), so a job is never lost between wakeup and claim.
17
+
18
+ Lua conventions (Redis best practices) followed here:
19
+ * No globals — every variable is `local` (Redis rejects globals).
20
+ * Deterministic — the clock (`now`) is always passed in via ARGV; scripts never
21
+ call `TIME`/`random`, so they replicate and unit-test reproducibly.
22
+ * `tonumber()` before any arithmetic on ARGV (which arrive as strings).
23
+ * ZSET scores stay < 2^53 (the priority packing) so doubles stay exact.
24
+ * Big fan-out is chunked (see MOVE_STALLED's `unpack` in 1000s) to respect Lua's
25
+ argument limit.
26
+ SINGLE-NODE assumption: per-job keys are derived from the key `base` inside the
27
+ scripts (`base .. jobId`, `base .. "de:" .. id`) rather than all being passed via
28
+ KEYS[]. This keeps the scripts simple for a single Redis; running on Redis Cluster
29
+ would require hash-tagging the keys (e.g. `{queue}`) so a queue's keys share a slot.
30
+ """
31
+
32
+ # Priority score packing constants (kept well under 2^53 so ZSET double scores
33
+ # stay exact). priority in [0, PRIORITY_OFFSET]; sequence in [0, SEQ_MOD).
34
+ PRIORITY_OFFSET = 1048576 # 2^20 — max priority (most urgent)
35
+ SEQ_MOD = 4294967296 # 2^32 — sequence wrap window
36
+
37
+ # Lua → Python return protocol: sentinels the scripts emit, decoded in worker.py.
38
+ RL_SENTINEL = "__rl__" # ACQUIRE hit the rate limiter; res[1] = ms until a token frees
39
+ LOCK_LOST = -2 # a finish script: the worker's lock was lost (job already reclaimed)
40
+ NOT_ACTIVE = -3 # a finish script: the job was no longer in `active`
41
+ OUTCOME_FAILED = 1 # MOVE_TO_FAILED outcome: terminally failed (vs 0 = will retry)
42
+
43
+ # Shared routines, prepended to every script that enqueues or acquires a job.
44
+ # This is the single definition of "how a job is ordered, woken, claimed":
45
+ # priorityScore — (priority, seq) -> ZSET score (lower score = sooner)
46
+ # enqueue — put a job into `prioritized` + arm the base marker
47
+ # lockAndLoad — lock a job already on `active`, stamp it, return its hash
48
+ # acquireNext — ZPOPMIN the next job into `active`, then lockAndLoad it
49
+ # To add markers-with-delay or grouping later, we change only these functions.
50
+ _LIB = """
51
+ local function priorityScore(priority, pcKey)
52
+ local seq = redis.call("INCR", pcKey) % 4294967296
53
+ return (1048576 - priority) * 4294967296 + seq
54
+ end
55
+ local function enqueue(prioritizedKey, markerKey, jobId, priority, pcKey)
56
+ redis.call("ZADD", prioritizedKey, priorityScore(priority, pcKey), jobId)
57
+ redis.call("ZADD", markerKey, 0, "0")
58
+ end
59
+ local function lockAndLoad(jobId, stalledKey, base, token, lockMs, now)
60
+ local jobKey = base .. jobId
61
+ redis.call("SET", jobKey .. ":lock", token, "PX", lockMs)
62
+ redis.call("SREM", stalledKey, jobId)
63
+ redis.call("HINCRBY", jobKey, "attemptsMade", 1)
64
+ redis.call("HSET", jobKey, "processedOn", now, "state", "active")
65
+ return {redis.call("HGETALL", jobKey), jobId}
66
+ end
67
+ -- Queue-wide token bucket (shared by all workers). capacity = maxJobs, refilled
68
+ -- at maxJobs/durationMs tokens per ms. Returns 0 if a token was consumed (allowed),
69
+ -- else the ms until one frees up. `now` is injected (never redis TIME) so the
70
+ -- script stays deterministic and unit-testable. maxJobs <= 0 disables it.
71
+ local function tryRateLimit(rlKey, maxJobs, durationMs, now)
72
+ if not maxJobs or maxJobs <= 0 then return 0 end
73
+ now = tonumber(now)
74
+ local cur = redis.call("HMGET", rlKey, "tokens", "ts")
75
+ local tokens = tonumber(cur[1])
76
+ local ts = tonumber(cur[2])
77
+ if tokens == nil then tokens = maxJobs; ts = now end
78
+ local refill = maxJobs / durationMs
79
+ tokens = math.min(maxJobs, tokens + (now - ts) * refill)
80
+ if tokens >= 1 then
81
+ redis.call("HSET", rlKey, "tokens", tokens - 1, "ts", now)
82
+ redis.call("PEXPIRE", rlKey, durationMs + 1000)
83
+ return 0
84
+ end
85
+ return math.ceil((1 - tokens) / refill) -- ms until one token is available
86
+ end
87
+ local function acquireNext(prioritizedKey, activeKey, markerKey, stalledKey,
88
+ base, pcKey, metaKey, token, lockMs, now,
89
+ rlKey, rlMax, rlDuration)
90
+ if redis.call("EXISTS", metaKey) == 1 then return false end -- queue paused
91
+ local res = redis.call("ZPOPMIN", prioritizedKey)
92
+ if #res == 0 then
93
+ redis.call("DEL", pcKey)
94
+ return false
95
+ end
96
+ local jobId = res[1]
97
+ -- Spend a token only once we actually have a job; if rate limited, put the job
98
+ -- back untouched and tell the caller when to retry (it re-delays, never fails).
99
+ local retry = tryRateLimit(rlKey, rlMax, rlDuration, now)
100
+ if retry > 0 then
101
+ redis.call("ZADD", prioritizedKey, res[2], jobId)
102
+ return {"__rl__", retry}
103
+ end
104
+ redis.call("LPUSH", activeKey, jobId)
105
+ if redis.call("ZCARD", prioritizedKey) > 0 then
106
+ redis.call("ZADD", markerKey, 0, "0") -- re-arm so another idle worker wakes
107
+ end
108
+ return lockAndLoad(jobId, stalledKey, base, token, lockMs, now)
109
+ end
110
+ local function delJobs(ids, base)
111
+ for _, id in ipairs(ids) do
112
+ redis.call("DEL", base .. id, base .. id .. ":lock", base .. id .. ":logs")
113
+ end
114
+ end
115
+ -- Record a terminal job in a finished set, applying auto-removal:
116
+ -- keepCount: -1 keep all, 0 remove immediately (don't record), N keep newest N
117
+ -- keepAge: -1 no age limit, S keep only those finished within S seconds
118
+ local function recordFinished(setKey, jobKey, base, jobId, now, prop, val,
119
+ state, keepCount, keepAge)
120
+ if keepCount == 0 and keepAge < 0 then
121
+ redis.call("DEL", jobKey, jobKey .. ":logs")
122
+ return
123
+ end
124
+ redis.call("ZADD", setKey, now, jobId)
125
+ redis.call("HSET", jobKey, prop, val, "finishedOn", now, "state", state)
126
+ if keepAge >= 0 then
127
+ local cutoff = now - keepAge * 1000
128
+ delJobs(redis.call("ZRANGEBYSCORE", setKey, "-inf", "(" .. cutoff), base)
129
+ redis.call("ZREMRANGEBYSCORE", setKey, "-inf", "(" .. cutoff)
130
+ end
131
+ if keepCount > 0 then
132
+ delJobs(redis.call("ZREVRANGE", setKey, keepCount, -1), base)
133
+ redis.call("ZREMRANGEBYRANK", setKey, 0, -(keepCount + 1))
134
+ end
135
+ end
136
+ """
137
+
138
+ # Add a job. With no custom id, generates one server-side (INCR) so concurrent
139
+ # producers never collide. With a custom id, the add is IDEMPOTENT: if a job with
140
+ # that id already exists it's returned unchanged (dedup).
141
+ # KEYS[1] id counter KEYS[2] prioritized KEYS[3] marker KEYS[4] delayed
142
+ # KEYS[5] key base KEYS[6] pc (priority counter)
143
+ # ARGV[1] name ARGV[2] data(json) ARGV[3] opts(json)
144
+ # ARGV[4] now(ms) ARGV[5] delay(ms) ARGV[6] priority ARGV[7] custom id ("" = auto)
145
+ # ARGV[8] dedup id ("" = none) ARGV[9] dedup ttl(ms) -- throttle window
146
+ ADD_JOB = (
147
+ _LIB
148
+ + """
149
+ local base = KEYS[5]
150
+ -- Throttle dedup: within the TTL window, a repeat dedup id is ignored and the
151
+ -- already-queued job's id is returned (self-expiring, no finish-side cleanup).
152
+ local dedupKey
153
+ if ARGV[8] ~= "" then
154
+ dedupKey = base .. "de:" .. ARGV[8]
155
+ local existing = redis.call("GET", dedupKey)
156
+ if existing then return existing end
157
+ end
158
+ local jobId = ARGV[7]
159
+ if jobId == "" then
160
+ jobId = redis.call("INCR", KEYS[1])
161
+ elseif redis.call("EXISTS", base .. jobId) == 1 then
162
+ return jobId
163
+ end
164
+ local jobKey = base .. jobId
165
+ redis.call("HSET", jobKey,
166
+ "id", jobId, "name", ARGV[1], "data", ARGV[2], "opts", ARGV[3],
167
+ "timestamp", ARGV[4], "attemptsMade", 0, "priority", ARGV[6])
168
+ if dedupKey then
169
+ redis.call("SET", dedupKey, jobId, "PX", tonumber(ARGV[9]))
170
+ redis.call("HSET", jobKey, "deid", ARGV[8])
171
+ end
172
+ local delay = tonumber(ARGV[5])
173
+ if delay > 0 then
174
+ redis.call("HSET", jobKey, "delay", delay, "state", "delayed")
175
+ redis.call("ZADD", KEYS[4], tonumber(ARGV[4]) + delay, jobId)
176
+ else
177
+ redis.call("HSET", jobKey, "state", "wait")
178
+ enqueue(KEYS[2], KEYS[3], jobId, tonumber(ARGV[6]), KEYS[6])
179
+ end
180
+ return jobId
181
+ """
182
+ )
183
+
184
+ # Claim the next job: pop highest-priority from `prioritized` into `active`, lock
185
+ # it, and return its hash. The blocking BZPOPMIN on the marker only wakes the
186
+ # worker; THIS is the atomic move. Returns {jobHash, jobId} or nil if none.
187
+ # KEYS[1] prioritized KEYS[2] active KEYS[3] marker KEYS[4] stalled
188
+ # KEYS[5] key base KEYS[6] pc KEYS[7] meta-paused KEYS[8] limiter
189
+ # ARGV[1] token ARGV[2] lockDuration(ms) ARGV[3] now(ms)
190
+ # ARGV[4] rlMax (0 = no limit) ARGV[5] rlDuration(ms)
191
+ # Returns false (none/paused), {jobHash, jobId}, or {"__rl__", retryMs} when rate limited.
192
+ MOVE_TO_ACTIVE = (
193
+ _LIB
194
+ + """
195
+ return acquireNext(KEYS[1], KEYS[2], KEYS[3], KEYS[4], KEYS[5], KEYS[6], KEYS[7],
196
+ ARGV[1], tonumber(ARGV[2]), ARGV[3],
197
+ KEYS[8], tonumber(ARGV[4]), tonumber(ARGV[5]))
198
+ """
199
+ )
200
+
201
+ # Renew a lock we still own. Token-guarded: we can NEVER renew a lock another
202
+ # worker has taken over. A successful renew also resets the stalled window.
203
+ # KEYS[1] lock key KEYS[2] stalled set
204
+ # ARGV[1] token ARGV[2] lockDuration(ms) ARGV[3] jobId
205
+ EXTEND_LOCK = """
206
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
207
+ redis.call("SET", KEYS[1], ARGV[1], "PX", tonumber(ARGV[2]))
208
+ redis.call("SREM", KEYS[2], ARGV[3])
209
+ return 1
210
+ end
211
+ return 0
212
+ """
213
+
214
+ # Commit a completed job, then (when fetch=1) acquire the next job in the SAME
215
+ # round trip. Token-guarded: a worker that lost its lock commits NOTHING.
216
+ # KEYS[1] active KEYS[2] completed KEYS[3] job hash KEYS[4] lock
217
+ # KEYS[5] prioritized KEYS[6] marker KEYS[7] stalled KEYS[8] base KEYS[9] pc
218
+ # KEYS[10] events channel KEYS[11] meta-paused KEYS[12] limiter
219
+ # ARGV[1] jobId ARGV[2] returnvalue(json) ARGV[3] now(ms) ARGV[4] token
220
+ # ARGV[5] fetch(1/0) ARGV[6] lockDuration(ms) ARGV[7] keepCount ARGV[8] keepAge(s)
221
+ # ARGV[9] rlMax ARGV[10] rlDuration(ms)
222
+ # Returns -2 lock lost, -3 not active, {1} committed, {1, nextHash, nextId}.
223
+ MOVE_TO_COMPLETED = (
224
+ _LIB
225
+ + """
226
+ if redis.call("GET", KEYS[4]) ~= ARGV[4] then return -2 end
227
+ redis.call("DEL", KEYS[4])
228
+ if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then return -3 end
229
+ recordFinished(KEYS[2], KEYS[3], KEYS[8], ARGV[1], tonumber(ARGV[3]),
230
+ "returnvalue", ARGV[2], "completed", tonumber(ARGV[7]), tonumber(ARGV[8]))
231
+ redis.call("PUBLISH", KEYS[10],
232
+ '{"jobId":' .. cjson.encode(ARGV[1]) .. ',"event":"completed","result":' .. ARGV[2] .. '}')
233
+ if ARGV[5] == "1" then
234
+ local nxt = acquireNext(KEYS[5], KEYS[1], KEYS[6], KEYS[7], KEYS[8], KEYS[9], KEYS[11],
235
+ ARGV[4], tonumber(ARGV[6]), ARGV[3],
236
+ KEYS[12], tonumber(ARGV[9]), tonumber(ARGV[10]))
237
+ if nxt then
238
+ if nxt[1] == "__rl__" then
239
+ redis.call("ZADD", KEYS[6], 0, "0") -- rate limited: wake a worker to re-check
240
+ else
241
+ return {1, nxt[1], nxt[2]}
242
+ end
243
+ end
244
+ end
245
+ return {1}
246
+ """
247
+ )
248
+
249
+ # Decide a failed job's fate (retry vs `failed`), then fetch-next. Retries
250
+ # re-enqueue at the job's stored priority.
251
+ # KEYS[1] active KEYS[2] prioritized KEYS[3] delayed KEYS[4] failed
252
+ # KEYS[5] job hash KEYS[6] lock KEYS[7] marker KEYS[8] stalled KEYS[9] base KEYS[10] pc
253
+ # KEYS[11] events channel KEYS[12] meta-paused KEYS[13] limiter
254
+ # ARGV[1] jobId ARGV[2] failedReason ARGV[3] now(ms) ARGV[4] attemptsMade
255
+ # ARGV[5] maxAttempts ARGV[6] backoff(ms) ARGV[7] token ARGV[8] fetch(1/0)
256
+ # ARGV[9] lockDuration(ms) ARGV[10] keepCount ARGV[11] keepAge(s)
257
+ # ARGV[12] rlMax ARGV[13] rlDuration(ms)
258
+ # Returns -2/-3, else {outcome} or {outcome, nextHash, nextId}; outcome 1=failed 0=retry.
259
+ MOVE_TO_FAILED = (
260
+ _LIB
261
+ + """
262
+ if redis.call("GET", KEYS[6]) ~= ARGV[7] then return -2 end
263
+ redis.call("DEL", KEYS[6])
264
+ if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then return -3 end
265
+ local attemptsMade = tonumber(ARGV[4])
266
+ local maxAttempts = tonumber(ARGV[5])
267
+ redis.call("HSET", KEYS[5], "failedReason", ARGV[2], "attemptsMade", attemptsMade)
268
+ local outcome
269
+ if attemptsMade < maxAttempts then
270
+ local backoff = tonumber(ARGV[6])
271
+ if backoff > 0 then
272
+ redis.call("HSET", KEYS[5], "state", "delayed")
273
+ redis.call("ZADD", KEYS[3], tonumber(ARGV[3]) + backoff, ARGV[1])
274
+ else
275
+ local priority = tonumber(redis.call("HGET", KEYS[5], "priority")) or 0
276
+ redis.call("HSET", KEYS[5], "state", "wait")
277
+ enqueue(KEYS[2], KEYS[7], ARGV[1], priority, KEYS[10])
278
+ end
279
+ outcome = 0
280
+ else
281
+ recordFinished(KEYS[4], KEYS[5], KEYS[9], ARGV[1], tonumber(ARGV[3]),
282
+ "failedReason", ARGV[2], "failed", tonumber(ARGV[10]), tonumber(ARGV[11]))
283
+ redis.call("PUBLISH", KEYS[11], '{"jobId":' .. cjson.encode(ARGV[1])
284
+ .. ',"event":"failed","reason":' .. cjson.encode(ARGV[2]) .. '}')
285
+ outcome = 1
286
+ end
287
+ if ARGV[8] == "1" then
288
+ local nxt = acquireNext(KEYS[2], KEYS[1], KEYS[7], KEYS[8], KEYS[9], KEYS[10], KEYS[12],
289
+ ARGV[7], tonumber(ARGV[9]), ARGV[3],
290
+ KEYS[13], tonumber(ARGV[12]), tonumber(ARGV[13]))
291
+ if nxt then
292
+ if nxt[1] == "__rl__" then
293
+ redis.call("ZADD", KEYS[7], 0, "0") -- rate limited: wake a worker to re-check
294
+ else
295
+ return {outcome, nxt[1], nxt[2]}
296
+ end
297
+ end
298
+ end
299
+ return {outcome}
300
+ """
301
+ )
302
+
303
+ # Add a delayed job with a caller-provided id, idempotently. Used by schedulers:
304
+ # the deterministic id `repeat:<schedulerId>:<nextMillis>` means the same
305
+ # occurrence can never be enqueued twice. Returns 1 if added, 0 if it existed.
306
+ # KEYS[1] delayed KEYS[2] key base
307
+ # ARGV[1] jobId ARGV[2] name ARGV[3] data(json) ARGV[4] opts(json)
308
+ # ARGV[5] now(ms) ARGV[6] processAt(ms) ARGV[7] priority ARGV[8] schedulerId
309
+ ADD_SCHEDULED = """
310
+ local jobKey = KEYS[2] .. ARGV[1]
311
+ if redis.call("EXISTS", jobKey) == 1 then return 0 end
312
+ redis.call("HSET", jobKey,
313
+ "id", ARGV[1], "name", ARGV[2], "data", ARGV[3], "opts", ARGV[4],
314
+ "timestamp", ARGV[5], "attemptsMade", 0, "priority", ARGV[7],
315
+ "delay", tonumber(ARGV[6]) - tonumber(ARGV[5]), "state", "delayed",
316
+ "schedulerId", ARGV[8])
317
+ redis.call("ZADD", KEYS[1], tonumber(ARGV[6]), ARGV[1])
318
+ return 1
319
+ """
320
+
321
+ # Promote a delayed job to run now (admin/dashboard action).
322
+ # KEYS[1] delayed KEYS[2] prioritized KEYS[3] marker KEYS[4] job hash KEYS[5] pc
323
+ # ARGV[1] jobId
324
+ PROMOTE_JOB = (
325
+ _LIB
326
+ + """
327
+ if redis.call("ZREM", KEYS[1], ARGV[1]) == 0 then return 0 end
328
+ local priority = tonumber(redis.call("HGET", KEYS[4], "priority")) or 0
329
+ redis.call("HSET", KEYS[4], "state", "wait", "delay", 0)
330
+ enqueue(KEYS[2], KEYS[3], ARGV[1], priority, KEYS[5])
331
+ return 1
332
+ """
333
+ )
334
+
335
+ # Re-queue a failed job for another attempt (admin/dashboard action).
336
+ # KEYS[1] failed KEYS[2] prioritized KEYS[3] marker KEYS[4] job hash KEYS[5] pc
337
+ # ARGV[1] jobId
338
+ RETRY_JOB = (
339
+ _LIB
340
+ + """
341
+ if redis.call("ZREM", KEYS[1], ARGV[1]) == 0 then return 0 end
342
+ redis.call("HDEL", KEYS[4], "failedReason", "finishedOn")
343
+ local priority = tonumber(redis.call("HGET", KEYS[4], "priority")) or 0
344
+ redis.call("HSET", KEYS[4], "state", "wait")
345
+ enqueue(KEYS[2], KEYS[3], ARGV[1], priority, KEYS[5])
346
+ return 1
347
+ """
348
+ )
349
+
350
+ # Remove a job from wherever it lives and delete its hash (admin/dashboard action).
351
+ # KEYS[1] prioritized KEYS[2] active KEYS[3] delayed KEYS[4] completed
352
+ # KEYS[5] failed KEYS[6] job hash ARGV[1] jobId
353
+ REMOVE_JOB = """
354
+ redis.call("ZREM", KEYS[1], ARGV[1])
355
+ redis.call("LREM", KEYS[2], 0, ARGV[1])
356
+ redis.call("ZREM", KEYS[3], ARGV[1])
357
+ redis.call("ZREM", KEYS[4], ARGV[1])
358
+ redis.call("ZREM", KEYS[5], ARGV[1])
359
+ redis.call("DEL", KEYS[6] .. ":lock", KEYS[6] .. ":logs")
360
+ return redis.call("DEL", KEYS[6])
361
+ """
362
+
363
+ # Mark-and-sweep recovery of jobs whose worker died. Recovered jobs are
364
+ # re-enqueued at their stored priority; jobs past maxStalledCount go to `failed`.
365
+ # KEYS[1] stalled KEYS[2] active KEYS[3] prioritized KEYS[4] failed
366
+ # KEYS[5] stalled-check KEYS[6] key base KEYS[7] marker KEYS[8] pc
367
+ # ARGV[1] maxStalledCount ARGV[2] now(ms) ARGV[3] throttle(ms), 0 disables
368
+ # Returns {failedIds, recoveredIds}.
369
+ MOVE_STALLED = (
370
+ _LIB
371
+ + """
372
+ local throttle = tonumber(ARGV[3])
373
+ if throttle > 0 then
374
+ if redis.call("EXISTS", KEYS[5]) == 1 then return {{}, {}} end
375
+ redis.call("SET", KEYS[5], ARGV[2], "PX", throttle)
376
+ end
377
+
378
+ local failed = {}
379
+ local recovered = {}
380
+ local stalling = redis.call("SMEMBERS", KEYS[1])
381
+ if #stalling > 0 then
382
+ redis.call("DEL", KEYS[1])
383
+ local maxStalled = tonumber(ARGV[1])
384
+ for _, jobId in ipairs(stalling) do
385
+ local jobKey = KEYS[6] .. jobId
386
+ if redis.call("EXISTS", jobKey .. ":lock") == 0 then
387
+ if redis.call("LREM", KEYS[2], 1, jobId) > 0 then
388
+ local count = redis.call("HINCRBY", jobKey, "stalledCounter", 1)
389
+ if count > maxStalled then
390
+ redis.call("ZADD", KEYS[4], tonumber(ARGV[2]), jobId)
391
+ redis.call("HSET", jobKey, "state", "failed",
392
+ "failedReason", "job stalled more than allowable limit",
393
+ "finishedOn", ARGV[2])
394
+ table.insert(failed, jobId)
395
+ else
396
+ local priority = tonumber(redis.call("HGET", jobKey, "priority")) or 0
397
+ redis.call("HSET", jobKey, "state", "wait")
398
+ enqueue(KEYS[3], KEYS[7], jobId, priority, KEYS[8])
399
+ table.insert(recovered, jobId)
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end
405
+
406
+ local active = redis.call("LRANGE", KEYS[2], 0, -1)
407
+ local i = 1
408
+ while i <= #active do
409
+ local j = math.min(i + 999, #active)
410
+ redis.call("SADD", KEYS[1], unpack(active, i, j))
411
+ i = j + 1
412
+ end
413
+ return {failed, recovered}
414
+ """
415
+ )
416
+
417
+ # Move every delayed job whose time has come into `prioritized` (at its priority).
418
+ # KEYS[1] delayed KEYS[2] prioritized KEYS[3] marker KEYS[4] key base KEYS[5] pc
419
+ # ARGV[1] now(ms)
420
+ PROMOTE_DELAYED = (
421
+ _LIB
422
+ + """
423
+ local jobs = redis.call("ZRANGEBYSCORE", KEYS[1], 0, ARGV[1])
424
+ for _, jobId in ipairs(jobs) do
425
+ redis.call("ZREM", KEYS[1], jobId)
426
+ local jobKey = KEYS[4] .. jobId
427
+ local priority = tonumber(redis.call("HGET", jobKey, "priority")) or 0
428
+ redis.call("HSET", jobKey, "state", "wait", "delay", 0)
429
+ enqueue(KEYS[2], KEYS[3], jobId, priority, KEYS[5])
430
+ end
431
+ return #jobs
432
+ """
433
+ )