effect-mq 0.1.0 → 0.2.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 +167 -22
- package/dist/Job.d.ts +67 -4
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +76 -3
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +174 -3
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +89 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +37 -6
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +201 -25
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts +5 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +116 -10
- package/dist/Worker.js.map +1 -1
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.d.ts +18 -2
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/DrizzleJobStore.js +287 -40
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -0
- package/dist/drizzle-postgres/index.d.ts.map +1 -0
- package/dist/drizzle-postgres/index.js.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/schema.d.ts +306 -4
- package/dist/drizzle-postgres/schema.d.ts.map +1 -0
- package/dist/{drizzle → drizzle-postgres}/schema.js +36 -2
- package/dist/drizzle-postgres/schema.js.map +1 -0
- package/dist/redis/RedisJobStore.d.ts +56 -0
- package/dist/redis/RedisJobStore.d.ts.map +1 -0
- package/dist/redis/RedisJobStore.js +385 -0
- package/dist/redis/RedisJobStore.js.map +1 -0
- package/dist/redis/index.d.ts +9 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +9 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/scripts.d.ts +168 -0
- package/dist/redis/scripts.d.ts.map +1 -0
- package/dist/redis/scripts.js +755 -0
- package/dist/redis/scripts.js.map +1 -0
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +332 -3
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +8 -4
- package/src/Job.ts +189 -5
- package/src/JobStore.ts +252 -4
- package/src/MemoryJobStore.ts +273 -26
- package/src/Worker.ts +152 -10
- package/src/{drizzle → drizzle-postgres}/DrizzleJobStore.ts +412 -40
- package/src/{drizzle → drizzle-postgres}/schema.ts +52 -3
- package/src/redis/RedisJobStore.ts +597 -0
- package/src/redis/index.ts +8 -0
- package/src/redis/scripts.ts +862 -0
- package/src/testing/conformance.ts +421 -3
- package/dist/drizzle/DrizzleJobStore.d.ts.map +0 -1
- package/dist/drizzle/DrizzleJobStore.js.map +0 -1
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js.map +0 -1
- /package/dist/{drizzle → drizzle-postgres}/index.d.ts +0 -0
- /package/dist/{drizzle → drizzle-postgres}/index.js +0 -0
- /package/src/{drizzle → drizzle-postgres}/index.ts +0 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lua scripts backing `RedisJobStore`.
|
|
3
|
+
*
|
|
4
|
+
* Every mutation is one script, so every `JobStore` method is atomic on the
|
|
5
|
+
* server. Two disciplines apply throughout:
|
|
6
|
+
*
|
|
7
|
+
* - **All time comes in via ARGV** (from the Effect `Clock`) — never Redis
|
|
8
|
+
* `TIME` — so the conformance suite drives this driver under `TestClock`
|
|
9
|
+
* against a real server.
|
|
10
|
+
* - **Numbers that become strings are formatted with `%.0f`** — Lua 5.1's
|
|
11
|
+
* `tostring` renders large integers as `1.7e+12`, which would corrupt ids
|
|
12
|
+
* and stored timestamps.
|
|
13
|
+
*
|
|
14
|
+
* Key layout (all under a configurable prefix, non-cluster):
|
|
15
|
+
*
|
|
16
|
+
* - `p:seq` counter for seq + default job ids
|
|
17
|
+
* - `p:job:<id>` HASH of the job record
|
|
18
|
+
* - `p:attempts:<id>` LIST of JSON attempt-ledger entries
|
|
19
|
+
* - `p:waiting:<queue>` ZSET, score `-priority`, member `<seq %016d>:<id>`
|
|
20
|
+
* (score = priority desc; member lex = FIFO)
|
|
21
|
+
* - `p:delayed:<queue>` ZSET, score `runAt`
|
|
22
|
+
* - `p:active` ZSET, score `lockExpiresAt`
|
|
23
|
+
* - `p:all` ZSET, score `enqueuedAt` (list pagination)
|
|
24
|
+
* - `p:finished` ZSET, score `finishedAt` (history TTL)
|
|
25
|
+
* - `p:terminal:<name>:<state>` ZSET, score `finishedAt` (keep pruning)
|
|
26
|
+
* - `p:counts` HASH `<queue>|<state>` -> integer
|
|
27
|
+
* - `p:paused` SET of paused queues
|
|
28
|
+
* - `p:schedules` / `p:schedule:<key>` ZSET by nextRunAt + HASH per record
|
|
29
|
+
*
|
|
30
|
+
* @since 0.2.0
|
|
31
|
+
*/
|
|
32
|
+
import { Redis } from "effect/unstable/persistence";
|
|
33
|
+
/**
|
|
34
|
+
* Shared helpers textually prepended to every script (the `Redis.script`
|
|
35
|
+
* runner has no include mechanism). `ARGV[1]` is always the key prefix.
|
|
36
|
+
*/
|
|
37
|
+
const HELPERS = `
|
|
38
|
+
local prefix = ARGV[1]
|
|
39
|
+
local function fmt(x) return string.format("%.0f", x) end
|
|
40
|
+
local function jobKey(id) return prefix .. ":job:" .. id end
|
|
41
|
+
local function attemptsKey(id) return prefix .. ":attempts:" .. id end
|
|
42
|
+
local function waitingKey(queue) return prefix .. ":waiting:" .. queue end
|
|
43
|
+
local function delayedKey(queue) return prefix .. ":delayed:" .. queue end
|
|
44
|
+
local function terminalKey(name, state) return prefix .. ":terminal:" .. name .. ":" .. state end
|
|
45
|
+
-- Waiting order: score = -priority (higher priority first, full number range);
|
|
46
|
+
-- FIFO within a priority via lexicographic members "<seq %016d>:<id>". A
|
|
47
|
+
-- composite numeric score would clip either priority or seq past float53.
|
|
48
|
+
local function waitingMember(seq, id) return string.format("%016.0f", seq) .. ":" .. id end
|
|
49
|
+
local function waitingId(member) return string.sub(member, 18) end
|
|
50
|
+
local function addWaiting(queue, priority, seq, id)
|
|
51
|
+
redis.call("ZADD", waitingKey(queue), -priority, waitingMember(seq, id))
|
|
52
|
+
end
|
|
53
|
+
local function remWaiting(queue, id)
|
|
54
|
+
local seq = tonumber(redis.call("HGET", jobKey(id), "seq")) or 0
|
|
55
|
+
redis.call("ZREM", waitingKey(queue), waitingMember(seq, id))
|
|
56
|
+
end
|
|
57
|
+
local function countsAdd(queue, state, n)
|
|
58
|
+
redis.call("HINCRBY", prefix .. ":counts", queue .. "|" .. state, n)
|
|
59
|
+
end
|
|
60
|
+
-- One ledger entry. startedAt/finishedAt/exitJson are strings ("" = absent).
|
|
61
|
+
local function appendAttempt(id, outcome, startedAt, finishedAt, exitJson)
|
|
62
|
+
local n = redis.call("LLEN", attemptsKey(id)) + 1
|
|
63
|
+
local started = (startedAt == nil or startedAt == "") and "null" or startedAt
|
|
64
|
+
-- The exit key is OMITTED (not null) when absent, so a legitimate encoded
|
|
65
|
+
-- null exit stays distinguishable on the read side.
|
|
66
|
+
local ex = (exitJson == nil or exitJson == "") and "" or (',"exit":' .. exitJson)
|
|
67
|
+
redis.call("RPUSH", attemptsKey(id),
|
|
68
|
+
'{"attempt":' .. n .. ',"startedAt":' .. started .. ',"finishedAt":' .. finishedAt ..
|
|
69
|
+
',"outcome":"' .. outcome .. '"' .. ex .. '}')
|
|
70
|
+
end
|
|
71
|
+
-- Remove a job and every index entry that references it.
|
|
72
|
+
local function deleteJob(id)
|
|
73
|
+
local jk = jobKey(id)
|
|
74
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
75
|
+
if not queue then return end
|
|
76
|
+
local state = redis.call("HGET", jk, "state")
|
|
77
|
+
local name = redis.call("HGET", jk, "name")
|
|
78
|
+
countsAdd(queue, state, -1)
|
|
79
|
+
redis.call("ZREM", prefix .. ":all", id)
|
|
80
|
+
redis.call("ZREM", prefix .. ":finished", id)
|
|
81
|
+
redis.call("ZREM", prefix .. ":active", id)
|
|
82
|
+
redis.call("ZREM", terminalKey(name, state), id)
|
|
83
|
+
remWaiting(queue, id)
|
|
84
|
+
redis.call("ZREM", delayedKey(queue), id)
|
|
85
|
+
redis.call("DEL", jk, attemptsKey(id))
|
|
86
|
+
end
|
|
87
|
+
-- Terminal retention for one name+state group. Correctness over speed: the
|
|
88
|
+
-- count rule sorts the group by (finishedAt DESC, seq DESC) in Lua because a
|
|
89
|
+
-- zset score alone cannot carry the seq tie-break exactly.
|
|
90
|
+
local function applyKeep(name, state, keepJson, now)
|
|
91
|
+
if keepJson == nil or keepJson == "" then return end
|
|
92
|
+
local ok, keep = pcall(cjson.decode, keepJson)
|
|
93
|
+
if not ok or type(keep) ~= "table" then return end
|
|
94
|
+
local tkey = terminalKey(name, state)
|
|
95
|
+
if keep.ageMs ~= nil then
|
|
96
|
+
local old = redis.call("ZRANGEBYSCORE", tkey, "-inf", now - keep.ageMs)
|
|
97
|
+
for i = 1, #old do deleteJob(old[i]) end
|
|
98
|
+
end
|
|
99
|
+
-- Floor + clamp: a fractional/negative count must degrade like the memory
|
|
100
|
+
-- driver's slice(), never error mid-script (writes before an error stick).
|
|
101
|
+
local count = keep.count ~= nil and math.floor(math.max(0, tonumber(keep.count) or 0)) or nil
|
|
102
|
+
if count ~= nil and redis.call("ZCARD", tkey) > count then
|
|
103
|
+
local members = redis.call("ZRANGE", tkey, 0, -1, "WITHSCORES")
|
|
104
|
+
local arr = {}
|
|
105
|
+
for i = 1, #members, 2 do
|
|
106
|
+
arr[#arr + 1] = {
|
|
107
|
+
id = members[i],
|
|
108
|
+
fa = tonumber(members[i + 1]),
|
|
109
|
+
seq = tonumber(redis.call("HGET", jobKey(members[i]), "seq")) or 0
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
table.sort(arr, function(a, b)
|
|
113
|
+
if a.fa ~= b.fa then return a.fa > b.fa end
|
|
114
|
+
return a.seq > b.seq
|
|
115
|
+
end)
|
|
116
|
+
for i = count + 1, #arr do deleteJob(arr[i].id) end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
-- Move an active job (whose lock bookkeeping was already cleared by the
|
|
120
|
+
-- caller) to the terminal cancelled state.
|
|
121
|
+
local function finishCancelled(id, queue, name, startedAt, now, nowStr)
|
|
122
|
+
local jk = jobKey(id)
|
|
123
|
+
redis.call("HSET", jk, "state", "cancelled", "finishedAt", nowStr, "cancelRequested", "0",
|
|
124
|
+
"lockToken", "", "lockExpiresAt", "")
|
|
125
|
+
countsAdd(queue, "active", -1)
|
|
126
|
+
countsAdd(queue, "cancelled", 1)
|
|
127
|
+
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
128
|
+
redis.call("ZADD", terminalKey(name, "cancelled"), now, id)
|
|
129
|
+
appendAttempt(id, "cancelled", startedAt, nowStr, "")
|
|
130
|
+
applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
|
|
131
|
+
end
|
|
132
|
+
`;
|
|
133
|
+
/**
|
|
134
|
+
* enqueue(prefix, idMode, id, name, queue, payloadJson, metadataJson,
|
|
135
|
+
* priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now)
|
|
136
|
+
* idMode: "user" (dedup no-op), "generated" (collision -> retry sentinel),
|
|
137
|
+
* "auto" (j-<seq>, in-script collision loop).
|
|
138
|
+
*/
|
|
139
|
+
export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now) => [
|
|
140
|
+
prefix,
|
|
141
|
+
idMode,
|
|
142
|
+
id,
|
|
143
|
+
name,
|
|
144
|
+
queue,
|
|
145
|
+
payloadJson,
|
|
146
|
+
metadataJson,
|
|
147
|
+
priority,
|
|
148
|
+
attemptsMax,
|
|
149
|
+
backoffJson,
|
|
150
|
+
keepJson,
|
|
151
|
+
timeoutMs,
|
|
152
|
+
delayMs,
|
|
153
|
+
now
|
|
154
|
+
], {
|
|
155
|
+
numberOfKeys: 0,
|
|
156
|
+
lua: `${HELPERS}
|
|
157
|
+
local idMode, id = ARGV[2], ARGV[3]
|
|
158
|
+
local queue = ARGV[5]
|
|
159
|
+
local nowStr = ARGV[14]
|
|
160
|
+
local now = tonumber(nowStr)
|
|
161
|
+
local delayMs = tonumber(ARGV[13])
|
|
162
|
+
if idMode == "user" or idMode == "generated" then
|
|
163
|
+
if redis.call("EXISTS", jobKey(id)) == 1 then
|
|
164
|
+
if idMode == "user" then return '{"duplicate":true,"id":' .. cjson.encode(id) .. '}' end
|
|
165
|
+
return '{"collision":true}'
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
id = ""
|
|
169
|
+
for i = 1, 5 do
|
|
170
|
+
local candidate = "j-" .. fmt(redis.call("INCR", prefix .. ":seq"))
|
|
171
|
+
if redis.call("EXISTS", jobKey(candidate)) == 0 then
|
|
172
|
+
id = candidate
|
|
173
|
+
break
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
if id == "" then return '{"error":"id"}' end
|
|
177
|
+
end
|
|
178
|
+
local seq = redis.call("INCR", prefix .. ":seq")
|
|
179
|
+
local state = delayMs > 0 and "delayed" or "waiting"
|
|
180
|
+
local runAt = now + delayMs
|
|
181
|
+
redis.call("HSET", jobKey(id),
|
|
182
|
+
"id", id, "name", ARGV[4], "queue", queue,
|
|
183
|
+
"payload", ARGV[6], "metadata", ARGV[7], "state", state,
|
|
184
|
+
"priority", ARGV[8], "attemptsMax", ARGV[9], "attemptsMade", "0", "stalledCount", "0",
|
|
185
|
+
"backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
|
|
186
|
+
"cancelRequested", "0", "runAt", fmt(runAt), "enqueuedAt", nowStr,
|
|
187
|
+
"processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
|
|
188
|
+
"lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
|
|
189
|
+
redis.call("ZADD", prefix .. ":all", now, id)
|
|
190
|
+
if state == "waiting" then
|
|
191
|
+
addWaiting(queue, tonumber(ARGV[8]), seq, id)
|
|
192
|
+
else
|
|
193
|
+
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
194
|
+
end
|
|
195
|
+
countsAdd(queue, state, 1)
|
|
196
|
+
-- Wake on EVERY insert (delayed too): idle workers must re-claim to learn
|
|
197
|
+
-- the new nextRunAt, exactly like the memory and Postgres drivers.
|
|
198
|
+
return '{"id":' .. cjson.encode(id) .. ',"duplicate":false,"wake":true}'
|
|
199
|
+
`
|
|
200
|
+
}).withReturnType();
|
|
201
|
+
/**
|
|
202
|
+
* claim(prefix, queue, namesJson, token, lockDurationMs, now)
|
|
203
|
+
* Promotes due delayed jobs first (also while paused), then claims the best
|
|
204
|
+
* waiting job whose name matches. Returns the claimed record (HGETALL pairs)
|
|
205
|
+
* or an Empty result with the earliest matching delayed runAt.
|
|
206
|
+
*/
|
|
207
|
+
export const claim = Redis.script((prefix, queue, namesJson, token, lockDurationMs, now) => [
|
|
208
|
+
prefix,
|
|
209
|
+
queue,
|
|
210
|
+
namesJson,
|
|
211
|
+
token,
|
|
212
|
+
lockDurationMs,
|
|
213
|
+
now
|
|
214
|
+
], {
|
|
215
|
+
numberOfKeys: 0,
|
|
216
|
+
lua: `${HELPERS}
|
|
217
|
+
local queue = ARGV[2]
|
|
218
|
+
local nameSet = {}
|
|
219
|
+
for _, n in ipairs(cjson.decode(ARGV[3])) do nameSet[n] = true end
|
|
220
|
+
local token = ARGV[4]
|
|
221
|
+
local lockDurationMs = tonumber(ARGV[5])
|
|
222
|
+
local nowStr = ARGV[6]
|
|
223
|
+
local now = tonumber(nowStr)
|
|
224
|
+
|
|
225
|
+
-- Promote due delayed jobs (state change is visible even while paused).
|
|
226
|
+
local due = redis.call("ZRANGEBYSCORE", delayedKey(queue), "-inf", now)
|
|
227
|
+
for i = 1, #due do
|
|
228
|
+
local id = due[i]
|
|
229
|
+
local jk = jobKey(id)
|
|
230
|
+
redis.call("ZREM", delayedKey(queue), id)
|
|
231
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
232
|
+
local seq = tonumber(redis.call("HGET", jk, "seq")) or 0
|
|
233
|
+
redis.call("HSET", jk, "state", "waiting")
|
|
234
|
+
addWaiting(queue, priority, seq, id)
|
|
235
|
+
countsAdd(queue, "delayed", -1)
|
|
236
|
+
countsAdd(queue, "waiting", 1)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
-- Earliest matching delayed job: how long an idle worker may sleep.
|
|
240
|
+
local nextRunAt = nil
|
|
241
|
+
local delayed = redis.call("ZRANGE", delayedKey(queue), 0, -1, "WITHSCORES")
|
|
242
|
+
for i = 1, #delayed, 2 do
|
|
243
|
+
if nameSet[redis.call("HGET", jobKey(delayed[i]), "name")] then
|
|
244
|
+
nextRunAt = tonumber(delayed[i + 1])
|
|
245
|
+
break
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
if redis.call("SISMEMBER", prefix .. ":paused", queue) == 1 then
|
|
250
|
+
return cjson.encode({ empty = true, nextRunAt = nextRunAt })
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
local offset = 0
|
|
254
|
+
while true do
|
|
255
|
+
local batch = redis.call("ZRANGE", waitingKey(queue), offset, offset + 99)
|
|
256
|
+
if #batch == 0 then break end
|
|
257
|
+
for i = 1, #batch do
|
|
258
|
+
local id = waitingId(batch[i])
|
|
259
|
+
local jk = jobKey(id)
|
|
260
|
+
if nameSet[redis.call("HGET", jk, "name")] then
|
|
261
|
+
redis.call("ZREM", waitingKey(queue), batch[i])
|
|
262
|
+
redis.call("HSET", jk, "state", "active", "lockToken", token,
|
|
263
|
+
"lockExpiresAt", fmt(now + lockDurationMs), "processedAt", nowStr)
|
|
264
|
+
redis.call("ZADD", prefix .. ":active", now + lockDurationMs, id)
|
|
265
|
+
countsAdd(queue, "waiting", -1)
|
|
266
|
+
countsAdd(queue, "active", 1)
|
|
267
|
+
return cjson.encode({ job = redis.call("HGETALL", jk) })
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
offset = offset + 100
|
|
271
|
+
end
|
|
272
|
+
return cjson.encode({ empty = true, nextRunAt = nextRunAt })
|
|
273
|
+
`
|
|
274
|
+
}).withReturnType();
|
|
275
|
+
/**
|
|
276
|
+
* ack(prefix, id, token, outcomeTag, exitJson, delayMs, now)
|
|
277
|
+
* Token-guarded. Retry on a cancel-requested job finishes it as cancelled
|
|
278
|
+
* (cancellation wins over revival, mirroring release/recoverStalled).
|
|
279
|
+
*/
|
|
280
|
+
export const ack = Redis.script((prefix, id, token, outcomeTag, exitJson, delayMs, now) => [
|
|
281
|
+
prefix,
|
|
282
|
+
id,
|
|
283
|
+
token,
|
|
284
|
+
outcomeTag,
|
|
285
|
+
exitJson,
|
|
286
|
+
delayMs,
|
|
287
|
+
now
|
|
288
|
+
], {
|
|
289
|
+
numberOfKeys: 0,
|
|
290
|
+
lua: `${HELPERS}
|
|
291
|
+
local id = ARGV[2]
|
|
292
|
+
local jk = jobKey(id)
|
|
293
|
+
if redis.call("EXISTS", jk) == 0 then return '{"error":"notfound"}' end
|
|
294
|
+
if redis.call("HGET", jk, "state") ~= "active" or redis.call("HGET", jk, "lockToken") ~= ARGV[3] then
|
|
295
|
+
return '{"error":"locklost"}'
|
|
296
|
+
end
|
|
297
|
+
local tag = ARGV[4]
|
|
298
|
+
local exitJson = ARGV[5]
|
|
299
|
+
local delayMs = tonumber(ARGV[6])
|
|
300
|
+
local nowStr = ARGV[7]
|
|
301
|
+
local now = tonumber(nowStr)
|
|
302
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
303
|
+
local name = redis.call("HGET", jk, "name")
|
|
304
|
+
local startedAt = redis.call("HGET", jk, "processedAt")
|
|
305
|
+
local cancelRequested = redis.call("HGET", jk, "cancelRequested") == "1"
|
|
306
|
+
|
|
307
|
+
redis.call("HINCRBY", jk, "attemptsMade", 1)
|
|
308
|
+
redis.call("ZREM", prefix .. ":active", id)
|
|
309
|
+
|
|
310
|
+
local function finish(newState, storeExit, outcome, ledgerExit)
|
|
311
|
+
redis.call("HSET", jk, "state", newState, "finishedAt", nowStr, "cancelRequested", "0",
|
|
312
|
+
"lockToken", "", "lockExpiresAt", "")
|
|
313
|
+
if storeExit ~= nil then redis.call("HSET", jk, "exit", storeExit) end
|
|
314
|
+
countsAdd(queue, "active", -1)
|
|
315
|
+
countsAdd(queue, newState, 1)
|
|
316
|
+
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
317
|
+
redis.call("ZADD", terminalKey(name, newState), now, id)
|
|
318
|
+
appendAttempt(id, outcome, startedAt, nowStr, ledgerExit)
|
|
319
|
+
applyKeep(name, newState, redis.call("HGET", jk, "keep"), now)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
if tag == "Complete" then
|
|
323
|
+
finish("completed", exitJson, "completed", exitJson)
|
|
324
|
+
elseif tag == "Fail" then
|
|
325
|
+
finish("failed", exitJson, "failed", exitJson)
|
|
326
|
+
elseif tag == "Cancelled" then
|
|
327
|
+
finish("cancelled", nil, "cancelled", "")
|
|
328
|
+
elseif cancelRequested then
|
|
329
|
+
finish("cancelled", nil, "cancelled", "")
|
|
330
|
+
else
|
|
331
|
+
appendAttempt(id, "retried", startedAt, nowStr, exitJson)
|
|
332
|
+
local seq = redis.call("INCR", prefix .. ":seq")
|
|
333
|
+
local runAt = now + delayMs
|
|
334
|
+
local state = delayMs > 0 and "delayed" or "waiting"
|
|
335
|
+
redis.call("HSET", jk, "state", state, "seq", fmt(seq), "runAt", fmt(runAt),
|
|
336
|
+
"lockToken", "", "lockExpiresAt", "")
|
|
337
|
+
countsAdd(queue, "active", -1)
|
|
338
|
+
countsAdd(queue, state, 1)
|
|
339
|
+
if state == "waiting" then
|
|
340
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
341
|
+
addWaiting(queue, priority, seq, id)
|
|
342
|
+
else
|
|
343
|
+
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
344
|
+
end
|
|
345
|
+
return '{"ok":true,"wake":true}'
|
|
346
|
+
end
|
|
347
|
+
return '{"ok":true}'
|
|
348
|
+
`
|
|
349
|
+
}).withReturnType();
|
|
350
|
+
/**
|
|
351
|
+
* release(prefix, id, token, now) — hand the job back without consuming an
|
|
352
|
+
* attempt; a pending cancel wins and finishes the job instead.
|
|
353
|
+
*/
|
|
354
|
+
export const release = Redis.script((prefix, id, token, now) => [prefix, id, token, now], {
|
|
355
|
+
numberOfKeys: 0,
|
|
356
|
+
lua: `${HELPERS}
|
|
357
|
+
local id = ARGV[2]
|
|
358
|
+
local jk = jobKey(id)
|
|
359
|
+
if redis.call("EXISTS", jk) == 0 then return '{"error":"notfound"}' end
|
|
360
|
+
if redis.call("HGET", jk, "state") ~= "active" or redis.call("HGET", jk, "lockToken") ~= ARGV[3] then
|
|
361
|
+
return '{"error":"locklost"}'
|
|
362
|
+
end
|
|
363
|
+
local nowStr = ARGV[4]
|
|
364
|
+
local now = tonumber(nowStr)
|
|
365
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
366
|
+
redis.call("ZREM", prefix .. ":active", id)
|
|
367
|
+
if redis.call("HGET", jk, "cancelRequested") == "1" then
|
|
368
|
+
finishCancelled(id, queue, redis.call("HGET", jk, "name"), redis.call("HGET", jk, "processedAt"), now, nowStr)
|
|
369
|
+
return '{"ok":true}'
|
|
370
|
+
end
|
|
371
|
+
local seq = tonumber(redis.call("HGET", jk, "seq")) or 0
|
|
372
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
373
|
+
redis.call("HSET", jk, "state", "waiting", "lockToken", "", "lockExpiresAt", "")
|
|
374
|
+
addWaiting(queue, priority, seq, id)
|
|
375
|
+
countsAdd(queue, "active", -1)
|
|
376
|
+
countsAdd(queue, "waiting", 1)
|
|
377
|
+
return '{"ok":true,"wake":true}'
|
|
378
|
+
`
|
|
379
|
+
}).withReturnType();
|
|
380
|
+
/**
|
|
381
|
+
* extendLocks(prefix, locksJson, durationMs, now) -> { lost, cancel }
|
|
382
|
+
* Cancel-requested locks are reported, not extended.
|
|
383
|
+
*/
|
|
384
|
+
export const extendLocks = Redis.script((prefix, locksJson, durationMs, now) => [prefix, locksJson, durationMs, now], {
|
|
385
|
+
numberOfKeys: 0,
|
|
386
|
+
lua: `${HELPERS}
|
|
387
|
+
local locks = cjson.decode(ARGV[2])
|
|
388
|
+
local durationMs = tonumber(ARGV[3])
|
|
389
|
+
local now = tonumber(ARGV[4])
|
|
390
|
+
local lost, cancel = {}, {}
|
|
391
|
+
for _, lock in ipairs(locks) do
|
|
392
|
+
local jk = jobKey(lock.id)
|
|
393
|
+
if redis.call("HGET", jk, "state") ~= "active" or redis.call("HGET", jk, "lockToken") ~= lock.token then
|
|
394
|
+
lost[#lost + 1] = lock.id
|
|
395
|
+
elseif redis.call("HGET", jk, "cancelRequested") == "1" then
|
|
396
|
+
cancel[#cancel + 1] = lock.id
|
|
397
|
+
else
|
|
398
|
+
redis.call("HSET", jk, "lockExpiresAt", fmt(now + durationMs))
|
|
399
|
+
redis.call("ZADD", prefix .. ":active", now + durationMs, lock.id)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
return cjson.encode({ lost = lost, cancel = cancel })
|
|
403
|
+
`
|
|
404
|
+
}).withReturnType();
|
|
405
|
+
/**
|
|
406
|
+
* recoverStalled(prefix, maxStalledCount, now) -> recovered [{id, failed}]
|
|
407
|
+
* A pending cancel finishes the job as cancelled (not reported as recovered).
|
|
408
|
+
*/
|
|
409
|
+
export const recoverStalled = Redis.script((prefix, maxStalledCount, now) => [prefix, maxStalledCount, now], {
|
|
410
|
+
numberOfKeys: 0,
|
|
411
|
+
lua: `${HELPERS}
|
|
412
|
+
local maxStalledCount = tonumber(ARGV[2])
|
|
413
|
+
local nowStr = ARGV[3]
|
|
414
|
+
local now = tonumber(nowStr)
|
|
415
|
+
local expired = redis.call("ZRANGEBYSCORE", prefix .. ":active", "-inf", now)
|
|
416
|
+
local recovered = {}
|
|
417
|
+
for _, id in ipairs(expired) do
|
|
418
|
+
local jk = jobKey(id)
|
|
419
|
+
if redis.call("HGET", jk, "state") == "active" then
|
|
420
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
421
|
+
local name = redis.call("HGET", jk, "name")
|
|
422
|
+
local startedAt = redis.call("HGET", jk, "processedAt")
|
|
423
|
+
redis.call("ZREM", prefix .. ":active", id)
|
|
424
|
+
if redis.call("HGET", jk, "cancelRequested") == "1" then
|
|
425
|
+
finishCancelled(id, queue, name, startedAt, now, nowStr)
|
|
426
|
+
else
|
|
427
|
+
local stalled = (tonumber(redis.call("HGET", jk, "stalledCount")) or 0) + 1
|
|
428
|
+
redis.call("HSET", jk, "stalledCount", fmt(stalled), "lockToken", "", "lockExpiresAt", "")
|
|
429
|
+
appendAttempt(id, "stalled", startedAt, nowStr, "")
|
|
430
|
+
if stalled > maxStalledCount then
|
|
431
|
+
redis.call("HSET", jk, "state", "failed", "finishedAt", nowStr,
|
|
432
|
+
"failedReason", "job stalled more than allowable limit")
|
|
433
|
+
countsAdd(queue, "active", -1)
|
|
434
|
+
countsAdd(queue, "failed", 1)
|
|
435
|
+
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
436
|
+
redis.call("ZADD", terminalKey(name, "failed"), now, id)
|
|
437
|
+
recovered[#recovered + 1] = { id = id, failed = true }
|
|
438
|
+
else
|
|
439
|
+
local seq = tonumber(redis.call("HGET", jk, "seq")) or 0
|
|
440
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
441
|
+
redis.call("HSET", jk, "state", "waiting")
|
|
442
|
+
addWaiting(queue, priority, seq, id)
|
|
443
|
+
countsAdd(queue, "active", -1)
|
|
444
|
+
countsAdd(queue, "waiting", 1)
|
|
445
|
+
recovered[#recovered + 1] = { id = id, failed = false }
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
if #recovered == 0 then return "[]" end
|
|
451
|
+
return cjson.encode(recovered)
|
|
452
|
+
`
|
|
453
|
+
}).withReturnType();
|
|
454
|
+
/** getJob(prefix, id) -> HGETALL pairs (empty array when missing). */
|
|
455
|
+
export const getJob = Redis.script((prefix, id) => [prefix, id], {
|
|
456
|
+
numberOfKeys: 0,
|
|
457
|
+
lua: `${HELPERS}
|
|
458
|
+
local record = redis.call("HGETALL", jobKey(ARGV[2]))
|
|
459
|
+
if #record == 0 then return "[]" end
|
|
460
|
+
return cjson.encode(record)
|
|
461
|
+
`
|
|
462
|
+
}).withReturnType();
|
|
463
|
+
/**
|
|
464
|
+
* list(prefix, filtersJson, cursor, limit)
|
|
465
|
+
* Keyset pagination over p:all, newest first (enqueuedAt DESC, id DESC).
|
|
466
|
+
*/
|
|
467
|
+
export const list = Redis.script((prefix, filtersJson, cursor, limit) => [prefix, filtersJson, cursor, limit], {
|
|
468
|
+
numberOfKeys: 0,
|
|
469
|
+
lua: `${HELPERS}
|
|
470
|
+
-- A filter that Redis's cjson cannot decode (e.g. lone-surrogate escapes)
|
|
471
|
+
-- degrades to an empty page instead of a script error.
|
|
472
|
+
local okFilters, filters = pcall(cjson.decode, ARGV[2])
|
|
473
|
+
if not okFilters then return '{"items":[],"more":false}' end
|
|
474
|
+
local stateSet = nil
|
|
475
|
+
if filters.states ~= nil then
|
|
476
|
+
stateSet = {}
|
|
477
|
+
for _, s in ipairs(filters.states) do stateSet[s] = true end
|
|
478
|
+
end
|
|
479
|
+
local cursorAt, cursorId = nil, nil
|
|
480
|
+
if ARGV[3] ~= "" then
|
|
481
|
+
local split = string.find(ARGV[3], ":", 1, true)
|
|
482
|
+
if split ~= nil then
|
|
483
|
+
cursorAt = tonumber(string.sub(ARGV[3], 1, split - 1))
|
|
484
|
+
cursorId = string.sub(ARGV[3], split + 1)
|
|
485
|
+
end
|
|
486
|
+
if cursorAt == nil then cursorId = nil end
|
|
487
|
+
end
|
|
488
|
+
local limit = tonumber(ARGV[4])
|
|
489
|
+
local items = {}
|
|
490
|
+
local moreMatches = false
|
|
491
|
+
local max = cursorAt == nil and "+inf" or fmt(cursorAt)
|
|
492
|
+
local offset = 0
|
|
493
|
+
while true do
|
|
494
|
+
local batch = redis.call("ZREVRANGEBYSCORE", prefix .. ":all", max, "-inf", "WITHSCORES", "LIMIT", offset, 100)
|
|
495
|
+
if #batch == 0 then break end
|
|
496
|
+
for i = 1, #batch, 2 do
|
|
497
|
+
local id = batch[i]
|
|
498
|
+
local at = tonumber(batch[i + 1])
|
|
499
|
+
-- Skip up to and including the cursor position within its score.
|
|
500
|
+
if cursorAt == nil or at < cursorAt or (at == cursorAt and id < cursorId) then
|
|
501
|
+
local jk = jobKey(id)
|
|
502
|
+
local matches = true
|
|
503
|
+
if filters.queue ~= nil and redis.call("HGET", jk, "queue") ~= filters.queue then matches = false end
|
|
504
|
+
if matches and filters.name ~= nil and redis.call("HGET", jk, "name") ~= filters.name then matches = false end
|
|
505
|
+
if matches and stateSet ~= nil and not stateSet[redis.call("HGET", jk, "state")] then matches = false end
|
|
506
|
+
if matches and filters.metadata ~= nil then
|
|
507
|
+
local okMeta, meta = pcall(cjson.decode, redis.call("HGET", jk, "metadata"))
|
|
508
|
+
if not okMeta then
|
|
509
|
+
matches = false
|
|
510
|
+
else
|
|
511
|
+
for k, v in pairs(filters.metadata) do
|
|
512
|
+
if meta[k] ~= v then
|
|
513
|
+
matches = false
|
|
514
|
+
break
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
if matches then
|
|
520
|
+
if #items >= limit then
|
|
521
|
+
moreMatches = true
|
|
522
|
+
break
|
|
523
|
+
end
|
|
524
|
+
items[#items + 1] = redis.call("HGETALL", jk)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
if moreMatches then break end
|
|
529
|
+
offset = offset + 100
|
|
530
|
+
end
|
|
531
|
+
if #items == 0 then return '{"items":[],"more":false}' end
|
|
532
|
+
return cjson.encode({ items = items, more = moreMatches })
|
|
533
|
+
`
|
|
534
|
+
}).withReturnType();
|
|
535
|
+
/** counts(prefix) -> HGETALL pairs of p:counts. */
|
|
536
|
+
export const counts = Redis.script((prefix) => [prefix], {
|
|
537
|
+
numberOfKeys: 0,
|
|
538
|
+
lua: `${HELPERS}
|
|
539
|
+
local pairs_ = redis.call("HGETALL", prefix .. ":counts")
|
|
540
|
+
if #pairs_ == 0 then return "[]" end
|
|
541
|
+
return cjson.encode(pairs_)
|
|
542
|
+
`
|
|
543
|
+
}).withReturnType();
|
|
544
|
+
/** remove(prefix, id) -> removed boolean (active jobs are refused). */
|
|
545
|
+
export const remove = Redis.script((prefix, id) => [prefix, id], {
|
|
546
|
+
numberOfKeys: 0,
|
|
547
|
+
lua: `${HELPERS}
|
|
548
|
+
local id = ARGV[2]
|
|
549
|
+
local jk = jobKey(id)
|
|
550
|
+
if redis.call("EXISTS", jk) == 0 or redis.call("HGET", jk, "state") == "active" then
|
|
551
|
+
return "0"
|
|
552
|
+
end
|
|
553
|
+
deleteJob(id)
|
|
554
|
+
return "1"
|
|
555
|
+
`
|
|
556
|
+
}).withReturnType();
|
|
557
|
+
/** retry(prefix, id, now) — failed -> waiting with a fresh budget. */
|
|
558
|
+
export const retry = Redis.script((prefix, id, now) => [prefix, id, now], {
|
|
559
|
+
numberOfKeys: 0,
|
|
560
|
+
lua: `${HELPERS}
|
|
561
|
+
local id = ARGV[2]
|
|
562
|
+
local jk = jobKey(id)
|
|
563
|
+
if redis.call("EXISTS", jk) == 0 then return '{"error":"notfound"}' end
|
|
564
|
+
local state = redis.call("HGET", jk, "state")
|
|
565
|
+
if state ~= "failed" then return '{"error":"state","state":' .. cjson.encode(state) .. '}' end
|
|
566
|
+
local nowStr = ARGV[3]
|
|
567
|
+
local now = tonumber(nowStr)
|
|
568
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
569
|
+
local name = redis.call("HGET", jk, "name")
|
|
570
|
+
local seq = redis.call("INCR", prefix .. ":seq")
|
|
571
|
+
redis.call("HSET", jk, "state", "waiting", "attemptsMade", "0", "stalledCount", "0",
|
|
572
|
+
"cancelRequested", "0", "exit", "", "failedReason", "", "finishedAt", "",
|
|
573
|
+
"processedAt", "", "runAt", nowStr, "seq", fmt(seq))
|
|
574
|
+
redis.call("ZREM", prefix .. ":finished", id)
|
|
575
|
+
redis.call("ZREM", terminalKey(name, "failed"), id)
|
|
576
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
577
|
+
addWaiting(queue, priority, seq, id)
|
|
578
|
+
countsAdd(queue, "failed", -1)
|
|
579
|
+
countsAdd(queue, "waiting", 1)
|
|
580
|
+
return '{"ok":true}'
|
|
581
|
+
`
|
|
582
|
+
}).withReturnType();
|
|
583
|
+
/**
|
|
584
|
+
* cancel(prefix, id, now) — waiting/delayed become terminal; active gets the
|
|
585
|
+
* cancel-request flag; terminal states are refused.
|
|
586
|
+
*/
|
|
587
|
+
export const cancel = Redis.script((prefix, id, now) => [prefix, id, now], {
|
|
588
|
+
numberOfKeys: 0,
|
|
589
|
+
lua: `${HELPERS}
|
|
590
|
+
local id = ARGV[2]
|
|
591
|
+
local jk = jobKey(id)
|
|
592
|
+
if redis.call("EXISTS", jk) == 0 then return '{"error":"notfound"}' end
|
|
593
|
+
local state = redis.call("HGET", jk, "state")
|
|
594
|
+
if state == "active" then
|
|
595
|
+
redis.call("HSET", jk, "cancelRequested", "1")
|
|
596
|
+
return '{"ok":true}'
|
|
597
|
+
end
|
|
598
|
+
if state ~= "waiting" and state ~= "delayed" then
|
|
599
|
+
return '{"error":"state","state":' .. cjson.encode(state) .. '}'
|
|
600
|
+
end
|
|
601
|
+
local nowStr = ARGV[3]
|
|
602
|
+
local now = tonumber(nowStr)
|
|
603
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
604
|
+
local name = redis.call("HGET", jk, "name")
|
|
605
|
+
remWaiting(queue, id)
|
|
606
|
+
redis.call("ZREM", delayedKey(queue), id)
|
|
607
|
+
redis.call("HSET", jk, "state", "cancelled", "finishedAt", nowStr, "cancelRequested", "0")
|
|
608
|
+
countsAdd(queue, state, -1)
|
|
609
|
+
countsAdd(queue, "cancelled", 1)
|
|
610
|
+
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
611
|
+
redis.call("ZADD", terminalKey(name, "cancelled"), now, id)
|
|
612
|
+
appendAttempt(id, "cancelled", redis.call("HGET", jk, "processedAt"), nowStr, "")
|
|
613
|
+
applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
|
|
614
|
+
return '{"ok":true}'
|
|
615
|
+
`
|
|
616
|
+
}).withReturnType();
|
|
617
|
+
/** promote(prefix, id, now) — delayed -> waiting now. */
|
|
618
|
+
export const promote = Redis.script((prefix, id, now) => [prefix, id, now], {
|
|
619
|
+
numberOfKeys: 0,
|
|
620
|
+
lua: `${HELPERS}
|
|
621
|
+
local id = ARGV[2]
|
|
622
|
+
local jk = jobKey(id)
|
|
623
|
+
if redis.call("EXISTS", jk) == 0 then return '{"error":"notfound"}' end
|
|
624
|
+
local state = redis.call("HGET", jk, "state")
|
|
625
|
+
if state ~= "delayed" then return '{"error":"state","state":' .. cjson.encode(state) .. '}' end
|
|
626
|
+
local queue = redis.call("HGET", jk, "queue")
|
|
627
|
+
local seq = tonumber(redis.call("HGET", jk, "seq")) or 0
|
|
628
|
+
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
629
|
+
redis.call("ZREM", delayedKey(queue), id)
|
|
630
|
+
redis.call("HSET", jk, "state", "waiting", "runAt", ARGV[3])
|
|
631
|
+
addWaiting(queue, priority, seq, id)
|
|
632
|
+
countsAdd(queue, "delayed", -1)
|
|
633
|
+
countsAdd(queue, "waiting", 1)
|
|
634
|
+
return '{"ok":true}'
|
|
635
|
+
`
|
|
636
|
+
}).withReturnType();
|
|
637
|
+
/**
|
|
638
|
+
* upsertSchedule(prefix, key, jobName, queue, cron, tz, everyMs, payloadJson,
|
|
639
|
+
* metadataJson, priority, attemptsMax, backoffJson, keepJson,
|
|
640
|
+
* timeoutMs, nextRunAt)
|
|
641
|
+
* Every field arrives pre-encoded from TS and is stored VERBATIM — routing a
|
|
642
|
+
* record through cjson would corrupt high-precision numbers (14 significant
|
|
643
|
+
* digits) and empty arrays ({}). An unchanged cadence (cron/tz/everyMs)
|
|
644
|
+
* preserves the stored nextRunAt.
|
|
645
|
+
*/
|
|
646
|
+
export const upsertSchedule = Redis.script((prefix, key, jobName, queue, cron, tz, everyMs, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, nextRunAt) => [
|
|
647
|
+
prefix,
|
|
648
|
+
key,
|
|
649
|
+
jobName,
|
|
650
|
+
queue,
|
|
651
|
+
cron,
|
|
652
|
+
tz,
|
|
653
|
+
everyMs,
|
|
654
|
+
payloadJson,
|
|
655
|
+
metadataJson,
|
|
656
|
+
priority,
|
|
657
|
+
attemptsMax,
|
|
658
|
+
backoffJson,
|
|
659
|
+
keepJson,
|
|
660
|
+
timeoutMs,
|
|
661
|
+
nextRunAt
|
|
662
|
+
], {
|
|
663
|
+
numberOfKeys: 0,
|
|
664
|
+
lua: `${HELPERS}
|
|
665
|
+
local key = ARGV[2]
|
|
666
|
+
local sk = prefix .. ":schedule:" .. key
|
|
667
|
+
local nextRunAt = tonumber(ARGV[15])
|
|
668
|
+
local prevCron = redis.call("HGET", sk, "cron")
|
|
669
|
+
if prevCron ~= false
|
|
670
|
+
and prevCron == ARGV[5]
|
|
671
|
+
and redis.call("HGET", sk, "tz") == ARGV[6]
|
|
672
|
+
and redis.call("HGET", sk, "everyMs") == ARGV[7]
|
|
673
|
+
then
|
|
674
|
+
nextRunAt = tonumber(redis.call("HGET", sk, "nextRunAt")) or nextRunAt
|
|
675
|
+
end
|
|
676
|
+
redis.call("DEL", sk)
|
|
677
|
+
redis.call("HSET", sk,
|
|
678
|
+
"key", key, "jobName", ARGV[3], "queue", ARGV[4],
|
|
679
|
+
"cron", ARGV[5], "tz", ARGV[6], "everyMs", ARGV[7],
|
|
680
|
+
"payload", ARGV[8], "metadata", ARGV[9],
|
|
681
|
+
"priority", ARGV[10], "attemptsMax", ARGV[11],
|
|
682
|
+
"backoff", ARGV[12], "keep", ARGV[13], "timeoutMs", ARGV[14],
|
|
683
|
+
"nextRunAt", fmt(nextRunAt))
|
|
684
|
+
redis.call("ZADD", prefix .. ":schedules", nextRunAt, key)
|
|
685
|
+
return '{"ok":true}'
|
|
686
|
+
`
|
|
687
|
+
}).withReturnType();
|
|
688
|
+
/** removeSchedule(prefix, key) -> existed boolean. */
|
|
689
|
+
export const removeSchedule = Redis.script((prefix, key) => [prefix, key], {
|
|
690
|
+
numberOfKeys: 0,
|
|
691
|
+
lua: `${HELPERS}
|
|
692
|
+
local removed = redis.call("ZREM", prefix .. ":schedules", ARGV[2])
|
|
693
|
+
redis.call("DEL", prefix .. ":schedule:" .. ARGV[2])
|
|
694
|
+
return tostring(removed)
|
|
695
|
+
`
|
|
696
|
+
}).withReturnType();
|
|
697
|
+
/** listSchedules(prefix, filtersJson) ordered by nextRunAt ascending. */
|
|
698
|
+
export const listSchedules = Redis.script((prefix, filtersJson) => [prefix, filtersJson], {
|
|
699
|
+
numberOfKeys: 0,
|
|
700
|
+
lua: `${HELPERS}
|
|
701
|
+
local filters = cjson.decode(ARGV[2])
|
|
702
|
+
local keys = redis.call("ZRANGE", prefix .. ":schedules", 0, -1)
|
|
703
|
+
local out = {}
|
|
704
|
+
for _, key in ipairs(keys) do
|
|
705
|
+
local record = redis.call("HGETALL", prefix .. ":schedule:" .. key)
|
|
706
|
+
local byName = {}
|
|
707
|
+
for i = 1, #record, 2 do byName[record[i]] = record[i + 1] end
|
|
708
|
+
local matches = true
|
|
709
|
+
if filters.jobName ~= nil and byName.jobName ~= filters.jobName then matches = false end
|
|
710
|
+
if matches and filters.queue ~= nil and byName.queue ~= filters.queue then matches = false end
|
|
711
|
+
if matches then out[#out + 1] = record end
|
|
712
|
+
end
|
|
713
|
+
if #out == 0 then return "[]" end
|
|
714
|
+
return cjson.encode(out)
|
|
715
|
+
`
|
|
716
|
+
}).withReturnType();
|
|
717
|
+
/** dueSchedules(prefix, now) ordered by nextRunAt ascending. */
|
|
718
|
+
export const dueSchedules = Redis.script((prefix, now) => [prefix, now], {
|
|
719
|
+
numberOfKeys: 0,
|
|
720
|
+
lua: `${HELPERS}
|
|
721
|
+
local keys = redis.call("ZRANGEBYSCORE", prefix .. ":schedules", "-inf", tonumber(ARGV[2]))
|
|
722
|
+
local out = {}
|
|
723
|
+
for _, key in ipairs(keys) do
|
|
724
|
+
out[#out + 1] = redis.call("HGETALL", prefix .. ":schedule:" .. key)
|
|
725
|
+
end
|
|
726
|
+
if #out == 0 then return "[]" end
|
|
727
|
+
return cjson.encode(out)
|
|
728
|
+
`
|
|
729
|
+
}).withReturnType();
|
|
730
|
+
/** advanceSchedule(prefix, key, expectedRunAt, nextRunAt) — conditional CAS. */
|
|
731
|
+
export const advanceSchedule = Redis.script((prefix, key, expectedRunAt, nextRunAt) => [prefix, key, expectedRunAt, nextRunAt], {
|
|
732
|
+
numberOfKeys: 0,
|
|
733
|
+
lua: `${HELPERS}
|
|
734
|
+
local key = ARGV[2]
|
|
735
|
+
local sk = prefix .. ":schedule:" .. key
|
|
736
|
+
local current = redis.call("HGET", sk, "nextRunAt")
|
|
737
|
+
if current == false or tonumber(current) ~= tonumber(ARGV[3]) then return "0" end
|
|
738
|
+
redis.call("HSET", sk, "nextRunAt", fmt(tonumber(ARGV[4])))
|
|
739
|
+
redis.call("ZADD", prefix .. ":schedules", tonumber(ARGV[4]), key)
|
|
740
|
+
return "1"
|
|
741
|
+
`
|
|
742
|
+
}).withReturnType();
|
|
743
|
+
/**
|
|
744
|
+
* sweepHistory(prefix, cutoff, limit) -> number swept (bounded batch; the
|
|
745
|
+
* caller loops until 0).
|
|
746
|
+
*/
|
|
747
|
+
export const sweepHistory = Redis.script((prefix, cutoff, limit) => [prefix, cutoff, limit], {
|
|
748
|
+
numberOfKeys: 0,
|
|
749
|
+
lua: `${HELPERS}
|
|
750
|
+
local old = redis.call("ZRANGEBYSCORE", prefix .. ":finished", "-inf", tonumber(ARGV[2]), "LIMIT", 0, tonumber(ARGV[3]))
|
|
751
|
+
for _, id in ipairs(old) do deleteJob(id) end
|
|
752
|
+
return tostring(#old)
|
|
753
|
+
`
|
|
754
|
+
}).withReturnType();
|
|
755
|
+
//# sourceMappingURL=scripts.js.map
|