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/__init__.py +9 -0
- toro/connection.py +31 -0
- toro/errors.py +15 -0
- toro/job.py +154 -0
- toro/keys.py +108 -0
- toro/py.typed +0 -0
- toro/queue.py +545 -0
- toro/scheduler.py +37 -0
- toro/scripts.py +433 -0
- toro/worker.py +525 -0
- toro_queue-0.1.0.dist-info/METADATA +127 -0
- toro_queue-0.1.0.dist-info/RECORD +14 -0
- toro_queue-0.1.0.dist-info/WHEEL +4 -0
- toro_queue-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|