effect-mq 0.2.0 → 0.3.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 +153 -18
- package/dist/Job.d.ts +51 -1
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +44 -2
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +86 -6
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js +27 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts +6 -5
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +160 -32
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +1 -0
- package/dist/Worker.js.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts +20 -3
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.js +352 -84
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
- package/dist/drizzle-postgres/schema.d.ts +255 -351
- package/dist/drizzle-postgres/schema.d.ts.map +1 -1
- package/dist/drizzle-postgres/schema.js +75 -27
- package/dist/drizzle-postgres/schema.js.map +1 -1
- package/dist/redis/RedisJobStore.d.ts +4 -2
- package/dist/redis/RedisJobStore.d.ts.map +1 -1
- package/dist/redis/RedisJobStore.js +67 -28
- package/dist/redis/RedisJobStore.js.map +1 -1
- package/dist/redis/scripts.d.ts +19 -6
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/scripts.js +208 -23
- package/dist/redis/scripts.js.map +1 -1
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +172 -7
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +1 -1
- package/src/Job.ts +113 -6
- package/src/JobStore.ts +122 -6
- package/src/MemoryJobStore.ts +184 -44
- package/src/Worker.ts +1 -0
- package/src/drizzle-postgres/DrizzleJobStore.ts +431 -91
- package/src/drizzle-postgres/schema.ts +177 -63
- package/src/redis/RedisJobStore.ts +90 -35
- package/src/redis/scripts.ts +217 -24
- package/src/testing/conformance.ts +246 -7
package/src/redis/scripts.ts
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
* - `p:delayed:<queue>` ZSET, score `runAt`
|
|
22
22
|
* - `p:active` ZSET, score `lockExpiresAt`
|
|
23
23
|
* - `p:all` ZSET, score `enqueuedAt` (list pagination)
|
|
24
|
-
* - `p:finished
|
|
24
|
+
* - `p:finished:<state>` ZSET, score `finishedAt` (history TTL)
|
|
25
25
|
* - `p:terminal:<name>:<state>` ZSET, score `finishedAt` (keep pruning)
|
|
26
26
|
* - `p:counts` HASH `<queue>|<state>` -> integer
|
|
27
27
|
* - `p:paused` SET of paused queues
|
|
28
28
|
* - `p:schedules` / `p:schedule:<key>` ZSET by nextRunAt + HASH per record
|
|
29
|
+
* - `p:dedupe:<name>\0<key>` HASH {jobId, expiresAt} + `p:dedupes` index
|
|
30
|
+
* ZSET (score = window expiry, +inf = pending)
|
|
29
31
|
*
|
|
30
32
|
* @since 0.2.0
|
|
31
33
|
*/
|
|
@@ -78,7 +80,7 @@ local function deleteJob(id)
|
|
|
78
80
|
local name = redis.call("HGET", jk, "name")
|
|
79
81
|
countsAdd(queue, state, -1)
|
|
80
82
|
redis.call("ZREM", prefix .. ":all", id)
|
|
81
|
-
redis.call("ZREM", prefix .. ":finished", id)
|
|
83
|
+
redis.call("ZREM", prefix .. ":finished:" .. state, id)
|
|
82
84
|
redis.call("ZREM", prefix .. ":active", id)
|
|
83
85
|
redis.call("ZREM", terminalKey(name, state), id)
|
|
84
86
|
remWaiting(queue, id)
|
|
@@ -90,8 +92,20 @@ end
|
|
|
90
92
|
-- zset score alone cannot carry the seq tie-break exactly.
|
|
91
93
|
local function applyKeep(name, state, keepJson, now)
|
|
92
94
|
if keepJson == nil or keepJson == "" then return end
|
|
93
|
-
local ok,
|
|
94
|
-
if not ok or type(
|
|
95
|
+
local ok, decoded = pcall(cjson.decode, keepJson)
|
|
96
|
+
if not ok or type(decoded) ~= "table" then return end
|
|
97
|
+
-- Policies are split per terminal state; rows persisted by 0.2.x carry the
|
|
98
|
+
-- flat {count, ageMs} shape and apply to every state.
|
|
99
|
+
local keep = decoded[state]
|
|
100
|
+
if type(keep) ~= "table" then
|
|
101
|
+
if decoded.completed == nil and decoded.failed == nil and decoded.cancelled == nil
|
|
102
|
+
and (decoded.count ~= nil or decoded.ageMs ~= nil)
|
|
103
|
+
then
|
|
104
|
+
keep = decoded
|
|
105
|
+
else
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
end
|
|
95
109
|
local tkey = terminalKey(name, state)
|
|
96
110
|
if keep.ageMs ~= nil then
|
|
97
111
|
local old = redis.call("ZRANGEBYSCORE", tkey, "-inf", now - keep.ageMs)
|
|
@@ -117,6 +131,20 @@ local function applyKeep(name, state, keepJson, now)
|
|
|
117
131
|
for i = count + 1, #arr do deleteJob(arr[i].id) end
|
|
118
132
|
end
|
|
119
133
|
end
|
|
134
|
+
local function dedupeStoreKey(name, key) return prefix .. ":dedupe:" .. name .. "\0" .. key end
|
|
135
|
+
-- A job leaving the pending states frees its pending-mode dedup entry; live
|
|
136
|
+
-- throttle windows deliberately outlast the job.
|
|
137
|
+
local function releaseDedupe(name, dkey, jobId, now)
|
|
138
|
+
if dkey == nil or dkey == false or dkey == "" then return end
|
|
139
|
+
local sk = dedupeStoreKey(name, dkey)
|
|
140
|
+
if redis.call("HGET", sk, "jobId") == jobId then
|
|
141
|
+
local exp = redis.call("HGET", sk, "expiresAt")
|
|
142
|
+
if exp == "" or tonumber(exp) <= now then
|
|
143
|
+
redis.call("DEL", sk)
|
|
144
|
+
redis.call("ZREM", prefix .. ":dedupes", name .. "\0" .. dkey)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
120
148
|
-- Move an active job (whose lock bookkeeping was already cleared by the
|
|
121
149
|
-- caller) to the terminal cancelled state.
|
|
122
150
|
local function finishCancelled(id, queue, name, startedAt, now, nowStr)
|
|
@@ -125,9 +153,10 @@ local function finishCancelled(id, queue, name, startedAt, now, nowStr)
|
|
|
125
153
|
"lockToken", "", "lockExpiresAt", "")
|
|
126
154
|
countsAdd(queue, "active", -1)
|
|
127
155
|
countsAdd(queue, "cancelled", 1)
|
|
128
|
-
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
156
|
+
redis.call("ZADD", prefix .. ":finished:cancelled", now, id)
|
|
129
157
|
redis.call("ZADD", terminalKey(name, "cancelled"), now, id)
|
|
130
158
|
appendAttempt(id, "cancelled", startedAt, nowStr, "")
|
|
159
|
+
releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
|
|
131
160
|
applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
|
|
132
161
|
end
|
|
133
162
|
`
|
|
@@ -153,7 +182,11 @@ export const enqueue = Redis.script(
|
|
|
153
182
|
keepJson: string,
|
|
154
183
|
timeoutMs: string,
|
|
155
184
|
delayMs: number,
|
|
156
|
-
now: number
|
|
185
|
+
now: number,
|
|
186
|
+
dedupeKey: string,
|
|
187
|
+
dedupeTtlMs: string,
|
|
188
|
+
dedupeExtend: string,
|
|
189
|
+
dedupeReplace: string
|
|
157
190
|
) => [
|
|
158
191
|
prefix,
|
|
159
192
|
idMode,
|
|
@@ -168,7 +201,11 @@ export const enqueue = Redis.script(
|
|
|
168
201
|
keepJson,
|
|
169
202
|
timeoutMs,
|
|
170
203
|
delayMs,
|
|
171
|
-
now
|
|
204
|
+
now,
|
|
205
|
+
dedupeKey,
|
|
206
|
+
dedupeTtlMs,
|
|
207
|
+
dedupeExtend,
|
|
208
|
+
dedupeReplace
|
|
172
209
|
],
|
|
173
210
|
{
|
|
174
211
|
numberOfKeys: 0,
|
|
@@ -183,7 +220,54 @@ if idMode == "user" or idMode == "generated" then
|
|
|
183
220
|
if idMode == "user" then return '{"duplicate":true,"id":' .. cjson.encode(id) .. '}' end
|
|
184
221
|
return '{"collision":true}'
|
|
185
222
|
end
|
|
186
|
-
|
|
223
|
+
end
|
|
224
|
+
-- Dedup decision tree: replace-while-delayed, throttle window, pending dedup.
|
|
225
|
+
local dKey = ARGV[15]
|
|
226
|
+
local name = ARGV[4]
|
|
227
|
+
if dKey ~= "" then
|
|
228
|
+
local sk = dedupeStoreKey(name, dKey)
|
|
229
|
+
local entryJob = redis.call("HGET", sk, "jobId")
|
|
230
|
+
if entryJob then
|
|
231
|
+
local expStr = redis.call("HGET", sk, "expiresAt")
|
|
232
|
+
local windowLive = expStr ~= "" and tonumber(expStr) > now
|
|
233
|
+
local keyedState = redis.call("HGET", jobKey(entryJob), "state")
|
|
234
|
+
local function bumpWindow()
|
|
235
|
+
if ARGV[17] == "1" and ARGV[16] ~= "" then
|
|
236
|
+
local windowEnd = now + tonumber(ARGV[16])
|
|
237
|
+
redis.call("HSET", sk, "expiresAt", fmt(windowEnd))
|
|
238
|
+
redis.call("ZADD", prefix .. ":dedupes", windowEnd, name .. "\0" .. dKey)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
-- Latest-wins while the keyed job is still delayed.
|
|
242
|
+
if ARGV[18] == "1" and keyedState == "delayed" then
|
|
243
|
+
local kjk = jobKey(entryJob)
|
|
244
|
+
local newRunAt = now + delayMs
|
|
245
|
+
redis.call("HSET", kjk, "payload", ARGV[6], "metadata", ARGV[7], "priority", ARGV[8],
|
|
246
|
+
"attemptsMax", ARGV[9], "backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
|
|
247
|
+
"runAt", fmt(newRunAt))
|
|
248
|
+
local keyedQueue = redis.call("HGET", kjk, "queue")
|
|
249
|
+
redis.call("ZADD", delayedKey(keyedQueue), newRunAt, entryJob)
|
|
250
|
+
-- A landed replace re-arms the ttl window.
|
|
251
|
+
if ARGV[16] ~= "" then
|
|
252
|
+
local windowEnd = now + tonumber(ARGV[16])
|
|
253
|
+
redis.call("HSET", sk, "expiresAt", fmt(windowEnd))
|
|
254
|
+
redis.call("ZADD", prefix .. ":dedupes", windowEnd, name .. "\0" .. dKey)
|
|
255
|
+
end
|
|
256
|
+
return '{"id":' .. cjson.encode(entryJob) .. ',"duplicate":true,"wake":true,"queue":' .. cjson.encode(keyedQueue) .. '}'
|
|
257
|
+
end
|
|
258
|
+
if windowLive then
|
|
259
|
+
bumpWindow()
|
|
260
|
+
return '{"id":' .. cjson.encode(entryJob) .. ',"duplicate":true}'
|
|
261
|
+
end
|
|
262
|
+
local pending = keyedState ~= false and keyedState ~= "completed"
|
|
263
|
+
and keyedState ~= "failed" and keyedState ~= "cancelled"
|
|
264
|
+
if expStr == "" and pending then
|
|
265
|
+
return '{"id":' .. cjson.encode(entryJob) .. ',"duplicate":true}'
|
|
266
|
+
end
|
|
267
|
+
-- Dead entry: the new job takes over the key below.
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
if idMode == "auto" then
|
|
187
271
|
id = ""
|
|
188
272
|
for i = 1, 5 do
|
|
189
273
|
local candidate = "j-" .. fmt(redis.call("INCR", prefix .. ":seq"))
|
|
@@ -202,7 +286,7 @@ redis.call("HSET", jobKey(id),
|
|
|
202
286
|
"payload", ARGV[6], "metadata", ARGV[7], "state", state,
|
|
203
287
|
"priority", ARGV[8], "attemptsMax", ARGV[9], "attemptsMade", "0", "stalledCount", "0",
|
|
204
288
|
"backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
|
|
205
|
-
"cancelRequested", "0", "runAt", fmt(runAt), "enqueuedAt", nowStr,
|
|
289
|
+
"cancelRequested", "0", "dedupeKey", dKey, "runAt", fmt(runAt), "enqueuedAt", nowStr,
|
|
206
290
|
"processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
|
|
207
291
|
"lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
|
|
208
292
|
redis.call("ZADD", prefix .. ":all", now, id)
|
|
@@ -212,6 +296,14 @@ else
|
|
|
212
296
|
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
213
297
|
end
|
|
214
298
|
countsAdd(queue, state, 1)
|
|
299
|
+
if dKey ~= "" then
|
|
300
|
+
local sk = dedupeStoreKey(name, dKey)
|
|
301
|
+
redis.call("DEL", sk)
|
|
302
|
+
redis.call("HSET", sk, "jobId", id,
|
|
303
|
+
"expiresAt", ARGV[16] == "" and "" or fmt(now + tonumber(ARGV[16])))
|
|
304
|
+
redis.call("ZADD", prefix .. ":dedupes",
|
|
305
|
+
ARGV[16] == "" and "inf" or tostring(now + tonumber(ARGV[16])), name .. "\0" .. dKey)
|
|
306
|
+
end
|
|
215
307
|
-- Wake on EVERY insert (delayed too): idle workers must re-claim to learn
|
|
216
308
|
-- the new nextRunAt, exactly like the memory and Postgres drivers.
|
|
217
309
|
return '{"id":' .. cjson.encode(id) .. ',"duplicate":false,"wake":true}'
|
|
@@ -340,9 +432,10 @@ local function finish(newState, storeExit, outcome, ledgerExit)
|
|
|
340
432
|
if storeExit ~= nil then redis.call("HSET", jk, "exit", storeExit) end
|
|
341
433
|
countsAdd(queue, "active", -1)
|
|
342
434
|
countsAdd(queue, newState, 1)
|
|
343
|
-
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
435
|
+
redis.call("ZADD", prefix .. ":finished:" .. newState, now, id)
|
|
344
436
|
redis.call("ZADD", terminalKey(name, newState), now, id)
|
|
345
437
|
appendAttempt(id, outcome, startedAt, nowStr, ledgerExit)
|
|
438
|
+
releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
|
|
346
439
|
applyKeep(name, newState, redis.call("HGET", jk, "keep"), now)
|
|
347
440
|
end
|
|
348
441
|
|
|
@@ -369,7 +462,7 @@ else
|
|
|
369
462
|
else
|
|
370
463
|
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
371
464
|
end
|
|
372
|
-
return '{"ok":true,"wake":true}'
|
|
465
|
+
return '{"ok":true,"wake":true,"queue":' .. cjson.encode(queue) .. '}'
|
|
373
466
|
end
|
|
374
467
|
return '{"ok":true}'
|
|
375
468
|
`
|
|
@@ -405,7 +498,7 @@ redis.call("HSET", jk, "state", "waiting", "lockToken", "", "lockExpiresAt", "")
|
|
|
405
498
|
addWaiting(queue, priority, seq, id)
|
|
406
499
|
countsAdd(queue, "active", -1)
|
|
407
500
|
countsAdd(queue, "waiting", 1)
|
|
408
|
-
return '{"ok":true,"wake":true}'
|
|
501
|
+
return '{"ok":true,"wake":true,"queue":' .. cjson.encode(queue) .. '}'
|
|
409
502
|
`
|
|
410
503
|
}
|
|
411
504
|
).withReturnType<string>()
|
|
@@ -471,8 +564,9 @@ for _, id in ipairs(expired) do
|
|
|
471
564
|
"failedReason", "job stalled more than allowable limit")
|
|
472
565
|
countsAdd(queue, "active", -1)
|
|
473
566
|
countsAdd(queue, "failed", 1)
|
|
474
|
-
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
567
|
+
redis.call("ZADD", prefix .. ":finished:failed", now, id)
|
|
475
568
|
redis.call("ZADD", terminalKey(name, "failed"), now, id)
|
|
569
|
+
releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
|
|
476
570
|
recovered[#recovered + 1] = { id = id, failed = true }
|
|
477
571
|
else
|
|
478
572
|
local seq = tonumber(redis.call("HGET", jk, "seq")) or 0
|
|
@@ -630,13 +724,13 @@ local seq = redis.call("INCR", prefix .. ":seq")
|
|
|
630
724
|
redis.call("HSET", jk, "state", "waiting", "attemptsMade", "0", "stalledCount", "0",
|
|
631
725
|
"cancelRequested", "0", "exit", "", "failedReason", "", "finishedAt", "",
|
|
632
726
|
"processedAt", "", "runAt", nowStr, "seq", fmt(seq))
|
|
633
|
-
redis.call("ZREM", prefix .. ":finished", id)
|
|
727
|
+
redis.call("ZREM", prefix .. ":finished:failed", id)
|
|
634
728
|
redis.call("ZREM", terminalKey(name, "failed"), id)
|
|
635
729
|
local priority = tonumber(redis.call("HGET", jk, "priority")) or 0
|
|
636
730
|
addWaiting(queue, priority, seq, id)
|
|
637
731
|
countsAdd(queue, "failed", -1)
|
|
638
732
|
countsAdd(queue, "waiting", 1)
|
|
639
|
-
return '{"ok":true}'
|
|
733
|
+
return '{"ok":true,"queue":' .. cjson.encode(queue) .. '}'
|
|
640
734
|
`
|
|
641
735
|
}
|
|
642
736
|
).withReturnType<string>()
|
|
@@ -670,9 +764,10 @@ redis.call("ZREM", delayedKey(queue), id)
|
|
|
670
764
|
redis.call("HSET", jk, "state", "cancelled", "finishedAt", nowStr, "cancelRequested", "0")
|
|
671
765
|
countsAdd(queue, state, -1)
|
|
672
766
|
countsAdd(queue, "cancelled", 1)
|
|
673
|
-
redis.call("ZADD", prefix .. ":finished", now, id)
|
|
767
|
+
redis.call("ZADD", prefix .. ":finished:cancelled", now, id)
|
|
674
768
|
redis.call("ZADD", terminalKey(name, "cancelled"), now, id)
|
|
675
769
|
appendAttempt(id, "cancelled", redis.call("HGET", jk, "processedAt"), nowStr, "")
|
|
770
|
+
releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
|
|
676
771
|
applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
|
|
677
772
|
return '{"ok":true}'
|
|
678
773
|
`
|
|
@@ -698,7 +793,7 @@ redis.call("HSET", jk, "state", "waiting", "runAt", ARGV[3])
|
|
|
698
793
|
addWaiting(queue, priority, seq, id)
|
|
699
794
|
countsAdd(queue, "delayed", -1)
|
|
700
795
|
countsAdd(queue, "waiting", 1)
|
|
701
|
-
return '{"ok":true}'
|
|
796
|
+
return '{"ok":true,"queue":' .. cjson.encode(queue) .. '}'
|
|
702
797
|
`
|
|
703
798
|
}
|
|
704
799
|
).withReturnType<string>()
|
|
@@ -846,17 +941,115 @@ return "1"
|
|
|
846
941
|
).withReturnType<string>()
|
|
847
942
|
|
|
848
943
|
/**
|
|
849
|
-
*
|
|
850
|
-
*
|
|
944
|
+
* sweepState(prefix, state, ttlMs, limit, offset, now) -> {scanned, deleted}
|
|
945
|
+
* One bounded page over a terminal state's finished zset, deleting rows past
|
|
946
|
+
* min(store ceiling, per-row keep.age). The caller advances the offset by
|
|
947
|
+
* (scanned - deleted) and stops when a page comes back short.
|
|
948
|
+
*/
|
|
949
|
+
export const sweepState = Redis.script(
|
|
950
|
+
(prefix: string, state: string, ttlMs: string, limit: number, offset: number, now: number) => [
|
|
951
|
+
prefix,
|
|
952
|
+
state,
|
|
953
|
+
ttlMs,
|
|
954
|
+
limit,
|
|
955
|
+
offset,
|
|
956
|
+
now
|
|
957
|
+
],
|
|
958
|
+
{
|
|
959
|
+
numberOfKeys: 0,
|
|
960
|
+
lua: `${HELPERS}
|
|
961
|
+
local state = ARGV[2]
|
|
962
|
+
local ttl = ARGV[3] ~= "" and tonumber(ARGV[3]) or nil
|
|
963
|
+
local limit = tonumber(ARGV[4])
|
|
964
|
+
local offset = tonumber(ARGV[5])
|
|
965
|
+
local now = tonumber(ARGV[6])
|
|
966
|
+
local batch = redis.call("ZRANGEBYSCORE", prefix .. ":finished:" .. state, "-inf", now,
|
|
967
|
+
"WITHSCORES", "LIMIT", offset, limit)
|
|
968
|
+
local scanned = 0
|
|
969
|
+
local deleted = 0
|
|
970
|
+
for i = 1, #batch, 2 do
|
|
971
|
+
scanned = scanned + 1
|
|
972
|
+
local id = batch[i]
|
|
973
|
+
local finishedAt = tonumber(batch[i + 1])
|
|
974
|
+
if redis.call("EXISTS", jobKey(id)) == 0 then
|
|
975
|
+
-- Orphaned member (hash evicted/removed out of band): self-heal so the
|
|
976
|
+
-- cursor math stays honest and the member never loops the sweep.
|
|
977
|
+
redis.call("ZREM", prefix .. ":finished:" .. state, id)
|
|
978
|
+
deleted = deleted + 1
|
|
979
|
+
else
|
|
980
|
+
local cutoffAge = ttl
|
|
981
|
+
local keepJson = redis.call("HGET", jobKey(id), "keep")
|
|
982
|
+
if keepJson and keepJson ~= "" then
|
|
983
|
+
local ok, keep = pcall(cjson.decode, keepJson)
|
|
984
|
+
if ok and type(keep) == "table" then
|
|
985
|
+
local policy = keep[state]
|
|
986
|
+
if type(policy) ~= "table"
|
|
987
|
+
and keep.completed == nil and keep.failed == nil and keep.cancelled == nil
|
|
988
|
+
and (keep.count ~= nil or keep.ageMs ~= nil)
|
|
989
|
+
then
|
|
990
|
+
policy = keep
|
|
991
|
+
end
|
|
992
|
+
if type(policy) == "table" and policy.ageMs ~= nil then
|
|
993
|
+
local age = tonumber(policy.ageMs)
|
|
994
|
+
if age ~= nil and (cutoffAge == nil or age < cutoffAge) then cutoffAge = age end
|
|
995
|
+
end
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
if cutoffAge ~= nil and finishedAt <= now - cutoffAge then
|
|
999
|
+
deleteJob(id)
|
|
1000
|
+
deleted = deleted + 1
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
return cjson.encode({ scanned = scanned, deleted = deleted })
|
|
1005
|
+
`
|
|
1006
|
+
}
|
|
1007
|
+
).withReturnType<string>()
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* sweepDedupes(prefix, limit, now) -> number pruned (bounded batch; the
|
|
1011
|
+
* caller loops until 0): expired windows, then pending pointers (+inf)
|
|
1012
|
+
* whose job is gone or terminal.
|
|
851
1013
|
*/
|
|
852
|
-
export const
|
|
853
|
-
(prefix: string,
|
|
1014
|
+
export const sweepDedupes = Redis.script(
|
|
1015
|
+
(prefix: string, limit: number, now: number) => [prefix, limit, now],
|
|
854
1016
|
{
|
|
855
1017
|
numberOfKeys: 0,
|
|
856
1018
|
lua: `${HELPERS}
|
|
857
|
-
local
|
|
858
|
-
|
|
859
|
-
|
|
1019
|
+
local limit = tonumber(ARGV[2])
|
|
1020
|
+
local now = tonumber(ARGV[3])
|
|
1021
|
+
-- Lazy migration: drain the pre-0.3 unsplit finished zset into the per-state
|
|
1022
|
+
-- keys (or drop orphans) so old history keeps getting swept.
|
|
1023
|
+
local migrated = 0
|
|
1024
|
+
local legacy = redis.call("ZRANGE", prefix .. ":finished", 0, limit - 1, "WITHSCORES")
|
|
1025
|
+
for i = 1, #legacy, 2 do
|
|
1026
|
+
local id = legacy[i]
|
|
1027
|
+
local state = redis.call("HGET", jobKey(id), "state")
|
|
1028
|
+
if state == "completed" or state == "failed" or state == "cancelled" then
|
|
1029
|
+
redis.call("ZADD", prefix .. ":finished:" .. state, tonumber(legacy[i + 1]), id)
|
|
1030
|
+
end
|
|
1031
|
+
redis.call("ZREM", prefix .. ":finished", id)
|
|
1032
|
+
migrated = migrated + 1
|
|
1033
|
+
end
|
|
1034
|
+
local index = prefix .. ":dedupes"
|
|
1035
|
+
local expired = redis.call("ZRANGEBYSCORE", index, "-inf", now, "LIMIT", 0, limit)
|
|
1036
|
+
for _, member in ipairs(expired) do
|
|
1037
|
+
redis.call("DEL", prefix .. ":dedupe:" .. member)
|
|
1038
|
+
redis.call("ZREM", index, member)
|
|
1039
|
+
end
|
|
1040
|
+
local removedPending = 0
|
|
1041
|
+
local pendings = redis.call("ZRANGEBYSCORE", index, "inf", "inf", "LIMIT", 0, limit)
|
|
1042
|
+
for _, member in ipairs(pendings) do
|
|
1043
|
+
local sk = prefix .. ":dedupe:" .. member
|
|
1044
|
+
local jobId = redis.call("HGET", sk, "jobId")
|
|
1045
|
+
local state = jobId and redis.call("HGET", jobKey(jobId), "state")
|
|
1046
|
+
if not state or state == "completed" or state == "failed" or state == "cancelled" then
|
|
1047
|
+
redis.call("DEL", sk)
|
|
1048
|
+
redis.call("ZREM", index, member)
|
|
1049
|
+
removedPending = removedPending + 1
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
return tostring(migrated + #expired + removedPending)
|
|
860
1053
|
`
|
|
861
1054
|
}
|
|
862
1055
|
).withReturnType<string>()
|