effect-mq 0.3.2 → 0.4.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 +102 -14
- package/dist/Job.d.ts +131 -9
- package/dist/Job.d.ts.map +1 -1
- package/dist/Job.js +81 -5
- package/dist/Job.js.map +1 -1
- package/dist/JobStore.d.ts +63 -2
- package/dist/JobStore.d.ts.map +1 -1
- package/dist/JobStore.js.map +1 -1
- package/dist/MemoryJobStore.d.ts.map +1 -1
- package/dist/MemoryJobStore.js +159 -119
- package/dist/MemoryJobStore.js.map +1 -1
- package/dist/Worker.d.ts +18 -0
- package/dist/Worker.d.ts.map +1 -1
- package/dist/Worker.js +37 -9
- package/dist/Worker.js.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.d.ts.map +1 -1
- package/dist/drizzle-postgres/DrizzleJobStore.js +239 -49
- package/dist/drizzle-postgres/DrizzleJobStore.js.map +1 -1
- package/dist/drizzle-postgres/schema.d.ts +2 -0
- package/dist/drizzle-postgres/schema.d.ts.map +1 -1
- package/dist/drizzle-postgres/schema.js +1 -0
- package/dist/drizzle-postgres/schema.js.map +1 -1
- package/dist/redis/RedisJobStore.d.ts.map +1 -1
- package/dist/redis/RedisJobStore.js +173 -36
- package/dist/redis/RedisJobStore.js.map +1 -1
- package/dist/redis/scripts.d.ts +24 -1
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/scripts.js +126 -21
- package/dist/redis/scripts.js.map +1 -1
- package/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +266 -0
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +1 -1
- package/src/Job.ts +268 -13
- package/src/JobStore.ts +77 -2
- package/src/MemoryJobStore.ts +102 -53
- package/src/Worker.ts +61 -9
- package/src/drizzle-postgres/DrizzleJobStore.ts +273 -66
- package/src/drizzle-postgres/schema.ts +2 -0
- package/src/redis/RedisJobStore.ts +236 -47
- package/src/redis/scripts.ts +153 -21
- package/src/testing/conformance.ts +350 -0
package/dist/redis/scripts.js
CHANGED
|
@@ -158,6 +158,30 @@ local function finishCancelled(id, queue, name, startedAt, now, nowStr)
|
|
|
158
158
|
releaseDedupe(name, redis.call("HGET", jk, "dedupeKey"), id, now)
|
|
159
159
|
applyKeep(name, "cancelled", redis.call("HGET", jk, "keep"), now)
|
|
160
160
|
end
|
|
161
|
+
-- Insert one fresh job row plus every index entry. String params are stored
|
|
162
|
+
-- verbatim (payload/metadata/backoff/keep/trace are pre-encoded JSON, "" =
|
|
163
|
+
-- absent); priority/delayMs/now numeric-coercible.
|
|
164
|
+
local function insertJobRow(id, name, queue, payloadJson, metadataJson, priority,
|
|
165
|
+
attemptsMax, backoffJson, keepJson, timeoutMs, dedupeKey, traceJson, delayMs, now, nowStr)
|
|
166
|
+
local seq = redis.call("INCR", prefix .. ":seq")
|
|
167
|
+
local state = delayMs > 0 and "delayed" or "waiting"
|
|
168
|
+
local runAt = now + delayMs
|
|
169
|
+
redis.call("HSET", jobKey(id),
|
|
170
|
+
"id", id, "name", name, "queue", queue,
|
|
171
|
+
"payload", payloadJson, "metadata", metadataJson, "state", state,
|
|
172
|
+
"priority", priority, "attemptsMax", attemptsMax, "attemptsMade", "0", "stalledCount", "0",
|
|
173
|
+
"backoff", backoffJson, "keep", keepJson, "timeoutMs", timeoutMs,
|
|
174
|
+
"cancelRequested", "0", "dedupeKey", dedupeKey, "trace", traceJson, "runAt", fmt(runAt), "enqueuedAt", nowStr,
|
|
175
|
+
"processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
|
|
176
|
+
"lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
|
|
177
|
+
redis.call("ZADD", prefix .. ":all", now, id)
|
|
178
|
+
if state == "waiting" then
|
|
179
|
+
addWaiting(queue, tonumber(priority), seq, id)
|
|
180
|
+
else
|
|
181
|
+
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
182
|
+
end
|
|
183
|
+
countsAdd(queue, state, 1)
|
|
184
|
+
end
|
|
161
185
|
`;
|
|
162
186
|
/**
|
|
163
187
|
* enqueue(prefix, idMode, id, name, queue, payloadJson, metadataJson,
|
|
@@ -165,7 +189,7 @@ end
|
|
|
165
189
|
* idMode: "user" (dedup no-op), "generated" (collision -> retry sentinel),
|
|
166
190
|
* "auto" (j-<seq>, in-script collision loop).
|
|
167
191
|
*/
|
|
168
|
-
export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now, dedupeKey, dedupeTtlMs, dedupeExtend, dedupeReplace) => [
|
|
192
|
+
export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, delayMs, now, dedupeKey, dedupeTtlMs, dedupeExtend, dedupeReplace, traceJson) => [
|
|
169
193
|
prefix,
|
|
170
194
|
idMode,
|
|
171
195
|
id,
|
|
@@ -183,7 +207,8 @@ export const enqueue = Redis.script((prefix, idMode, id, name, queue, payloadJso
|
|
|
183
207
|
dedupeKey,
|
|
184
208
|
dedupeTtlMs,
|
|
185
209
|
dedupeExtend,
|
|
186
|
-
dedupeReplace
|
|
210
|
+
dedupeReplace,
|
|
211
|
+
traceJson
|
|
187
212
|
], {
|
|
188
213
|
numberOfKeys: 0,
|
|
189
214
|
lua: `${HELPERS}
|
|
@@ -221,7 +246,7 @@ if dKey ~= "" then
|
|
|
221
246
|
local newRunAt = now + delayMs
|
|
222
247
|
redis.call("HSET", kjk, "payload", ARGV[6], "metadata", ARGV[7], "priority", ARGV[8],
|
|
223
248
|
"attemptsMax", ARGV[9], "backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
|
|
224
|
-
"runAt", fmt(newRunAt))
|
|
249
|
+
"trace", ARGV[19], "runAt", fmt(newRunAt))
|
|
225
250
|
local keyedQueue = redis.call("HGET", kjk, "queue")
|
|
226
251
|
redis.call("ZADD", delayedKey(keyedQueue), newRunAt, entryJob)
|
|
227
252
|
-- A landed replace re-arms the ttl window.
|
|
@@ -255,24 +280,8 @@ if idMode == "auto" then
|
|
|
255
280
|
end
|
|
256
281
|
if id == "" then return '{"error":"id"}' end
|
|
257
282
|
end
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
local runAt = now + delayMs
|
|
261
|
-
redis.call("HSET", jobKey(id),
|
|
262
|
-
"id", id, "name", ARGV[4], "queue", queue,
|
|
263
|
-
"payload", ARGV[6], "metadata", ARGV[7], "state", state,
|
|
264
|
-
"priority", ARGV[8], "attemptsMax", ARGV[9], "attemptsMade", "0", "stalledCount", "0",
|
|
265
|
-
"backoff", ARGV[10], "keep", ARGV[11], "timeoutMs", ARGV[12],
|
|
266
|
-
"cancelRequested", "0", "dedupeKey", dKey, "runAt", fmt(runAt), "enqueuedAt", nowStr,
|
|
267
|
-
"processedAt", "", "finishedAt", "", "exit", "", "failedReason", "",
|
|
268
|
-
"lockToken", "", "lockExpiresAt", "", "seq", fmt(seq))
|
|
269
|
-
redis.call("ZADD", prefix .. ":all", now, id)
|
|
270
|
-
if state == "waiting" then
|
|
271
|
-
addWaiting(queue, tonumber(ARGV[8]), seq, id)
|
|
272
|
-
else
|
|
273
|
-
redis.call("ZADD", delayedKey(queue), runAt, id)
|
|
274
|
-
end
|
|
275
|
-
countsAdd(queue, state, 1)
|
|
283
|
+
insertJobRow(id, ARGV[4], queue, ARGV[6], ARGV[7], ARGV[8], ARGV[9], ARGV[10], ARGV[11],
|
|
284
|
+
ARGV[12], dKey, ARGV[19], delayMs, now, nowStr)
|
|
276
285
|
if dKey ~= "" then
|
|
277
286
|
local sk = dedupeStoreKey(name, dKey)
|
|
278
287
|
redis.call("DEL", sk)
|
|
@@ -831,6 +840,102 @@ redis.call("ZADD", prefix .. ":schedules", tonumber(ARGV[4]), key)
|
|
|
831
840
|
return "1"
|
|
832
841
|
`
|
|
833
842
|
}).withReturnType();
|
|
843
|
+
/**
|
|
844
|
+
* tickSchedule(prefix, key, expectedRunAt, nextRunAt, id, name, queue,
|
|
845
|
+
* payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson,
|
|
846
|
+
* timeoutMs, traceJson, delayMs, now) -> "1" fired | "0"
|
|
847
|
+
* Atomic occurrence claim: the nextRunAt CAS and the tick job's insert run
|
|
848
|
+
* in one script, so a stale sweeper can never re-fire a slot — even after
|
|
849
|
+
* retention pruned the previous slot's job row.
|
|
850
|
+
*/
|
|
851
|
+
export const tickSchedule = Redis.script((prefix, key, expectedRunAt, nextRunAt, id, name, queue, payloadJson, metadataJson, priority, attemptsMax, backoffJson, keepJson, timeoutMs, traceJson, delayMs, now) => [
|
|
852
|
+
prefix,
|
|
853
|
+
key,
|
|
854
|
+
expectedRunAt,
|
|
855
|
+
nextRunAt,
|
|
856
|
+
id,
|
|
857
|
+
name,
|
|
858
|
+
queue,
|
|
859
|
+
payloadJson,
|
|
860
|
+
metadataJson,
|
|
861
|
+
priority,
|
|
862
|
+
attemptsMax,
|
|
863
|
+
backoffJson,
|
|
864
|
+
keepJson,
|
|
865
|
+
timeoutMs,
|
|
866
|
+
traceJson,
|
|
867
|
+
delayMs,
|
|
868
|
+
now
|
|
869
|
+
], {
|
|
870
|
+
numberOfKeys: 0,
|
|
871
|
+
lua: `${HELPERS}
|
|
872
|
+
local key = ARGV[2]
|
|
873
|
+
local sk = prefix .. ":schedule:" .. key
|
|
874
|
+
local current = redis.call("HGET", sk, "nextRunAt")
|
|
875
|
+
if current == false or tonumber(current) ~= tonumber(ARGV[3]) then return "0" end
|
|
876
|
+
redis.call("HSET", sk, "nextRunAt", fmt(tonumber(ARGV[4])))
|
|
877
|
+
redis.call("ZADD", prefix .. ":schedules", tonumber(ARGV[4]), key)
|
|
878
|
+
local id = ARGV[5]
|
|
879
|
+
-- Pre-existing slot row (pre-0.4 crash between enqueue and advance): the
|
|
880
|
+
-- schedule still advances, but nothing new fires.
|
|
881
|
+
if redis.call("EXISTS", jobKey(id)) == 1 then return "0" end
|
|
882
|
+
insertJobRow(id, ARGV[6], ARGV[7], ARGV[8], ARGV[9], ARGV[10], ARGV[11], ARGV[12], ARGV[13],
|
|
883
|
+
ARGV[14], "", ARGV[15], tonumber(ARGV[16]), tonumber(ARGV[17]), ARGV[17])
|
|
884
|
+
return "1"
|
|
885
|
+
`
|
|
886
|
+
}).withReturnType();
|
|
887
|
+
/**
|
|
888
|
+
* enqueueMany(prefix, now, count, ...items) -> JSON array of per-item results
|
|
889
|
+
* ({id, duplicate} | {collision} | {error}). Items are 13-ARGV strides:
|
|
890
|
+
* idMode, id, name, queue, payloadJson, metadataJson, priority, attemptsMax,
|
|
891
|
+
* backoffJson, keepJson, timeoutMs, traceJson, delayMs. Plain (non-dedup)
|
|
892
|
+
* items only — the caller routes dedup items through \`enqueue\`.
|
|
893
|
+
*/
|
|
894
|
+
export const enqueueMany = Redis.script((prefix, now, count, items) => [prefix, now, count, ...items], {
|
|
895
|
+
numberOfKeys: 0,
|
|
896
|
+
lua: `${HELPERS}
|
|
897
|
+
local now = tonumber(ARGV[2])
|
|
898
|
+
local nowStr = ARGV[2]
|
|
899
|
+
local count = tonumber(ARGV[3])
|
|
900
|
+
local out = {}
|
|
901
|
+
for i = 0, count - 1 do
|
|
902
|
+
local base = 3 + i * 13
|
|
903
|
+
local idMode = ARGV[base + 1]
|
|
904
|
+
local id = ARGV[base + 2]
|
|
905
|
+
local result
|
|
906
|
+
if idMode ~= "auto" and redis.call("EXISTS", jobKey(id)) == 1 then
|
|
907
|
+
-- Sequential in-script processing makes intra-batch repeats of one user
|
|
908
|
+
-- id resolve exactly like separate enqueues: first inserts, rest dedup.
|
|
909
|
+
if idMode == "user" then
|
|
910
|
+
result = '{"id":' .. cjson.encode(id) .. ',"duplicate":true}'
|
|
911
|
+
else
|
|
912
|
+
result = '{"collision":true}'
|
|
913
|
+
end
|
|
914
|
+
else
|
|
915
|
+
if idMode == "auto" then
|
|
916
|
+
id = ""
|
|
917
|
+
for a = 1, 5 do
|
|
918
|
+
local candidate = "j-" .. fmt(redis.call("INCR", prefix .. ":seq"))
|
|
919
|
+
if redis.call("EXISTS", jobKey(candidate)) == 0 then
|
|
920
|
+
id = candidate
|
|
921
|
+
break
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
if id == "" then
|
|
926
|
+
result = '{"error":"id"}'
|
|
927
|
+
else
|
|
928
|
+
insertJobRow(id, ARGV[base + 3], ARGV[base + 4], ARGV[base + 5], ARGV[base + 6],
|
|
929
|
+
ARGV[base + 7], ARGV[base + 8], ARGV[base + 9], ARGV[base + 10], ARGV[base + 11],
|
|
930
|
+
"", ARGV[base + 12], tonumber(ARGV[base + 13]), now, nowStr)
|
|
931
|
+
result = '{"id":' .. cjson.encode(id) .. ',"duplicate":false}'
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
out[#out + 1] = result
|
|
935
|
+
end
|
|
936
|
+
return "[" .. table.concat(out, ",") .. "]"
|
|
937
|
+
`
|
|
938
|
+
}).withReturnType();
|
|
834
939
|
/**
|
|
835
940
|
* sweepState(prefix, state, ttlMs, limit, offset, now) -> {scanned, deleted}
|
|
836
941
|
* One bounded page over a terminal state's finished zset, deleting rows past
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAA;AAEnD;;;GAGG;AACH,MAAM,OAAO,GAAG
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAuB,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAE1C,OAAO,EAAuB,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAA;AAkChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,MAAM,cACA,MAAM,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAC/C,IA8jDF,CAAA"}
|
|
@@ -33,6 +33,7 @@ const baseRequest = (overrides) => ({
|
|
|
33
33
|
keep: undefined,
|
|
34
34
|
timeoutMs: undefined,
|
|
35
35
|
dedupe: undefined,
|
|
36
|
+
trace: undefined,
|
|
36
37
|
delayMs: 0,
|
|
37
38
|
...overrides
|
|
38
39
|
});
|
|
@@ -586,6 +587,238 @@ export const jobStoreConformance = (name, storeLayer) => {
|
|
|
586
587
|
expect(yield* store.removeSchedule(schedule.key)).toBe(false);
|
|
587
588
|
expect(yield* store.listSchedules()).toEqual([]);
|
|
588
589
|
})));
|
|
590
|
+
const minutelySchedule = () => ({
|
|
591
|
+
key: JobStore.ScheduleKey("TestJob/minutely"),
|
|
592
|
+
jobName: "TestJob",
|
|
593
|
+
queue: QueueName("default"),
|
|
594
|
+
cron: undefined,
|
|
595
|
+
tz: undefined,
|
|
596
|
+
everyMs: 60_000,
|
|
597
|
+
payload: { n: 7 },
|
|
598
|
+
metadata: {},
|
|
599
|
+
priority: 0,
|
|
600
|
+
attemptsMax: 1,
|
|
601
|
+
backoff: undefined,
|
|
602
|
+
keep: undefined,
|
|
603
|
+
timeoutMs: undefined,
|
|
604
|
+
nextRunAt: 60_000
|
|
605
|
+
});
|
|
606
|
+
it.effect("tickSchedule fires a slot exactly once and advances atomically", () => withStore((store) => Effect.gen(function* () {
|
|
607
|
+
const schedule = minutelySchedule();
|
|
608
|
+
yield* store.upsertSchedule(schedule);
|
|
609
|
+
yield* TestClock.adjust(60_000);
|
|
610
|
+
const request = baseRequest({
|
|
611
|
+
id: JobId("sched/TestJob/minutely/60000"),
|
|
612
|
+
payload: { n: 7 }
|
|
613
|
+
});
|
|
614
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
|
|
615
|
+
expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
|
|
616
|
+
expect((yield* store.counts()).waiting).toBe(1);
|
|
617
|
+
// A concurrent sweeper holding the same stale expectation loses the
|
|
618
|
+
// CAS: nothing fires, nothing advances further.
|
|
619
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 180_000, request)).toBe(false);
|
|
620
|
+
expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
|
|
621
|
+
expect((yield* store.counts()).waiting).toBe(1);
|
|
622
|
+
const claim = yield* store.claim(claimOptions());
|
|
623
|
+
assert(claim._tag === "Claimed");
|
|
624
|
+
expect(claim.job.id).toBe("sched/TestJob/minutely/60000");
|
|
625
|
+
expect(claim.job.payload).toEqual({ n: 7 });
|
|
626
|
+
})));
|
|
627
|
+
it.effect("a stale tick cannot re-fire a slot whose job was pruned", () => withStore((store) => Effect.gen(function* () {
|
|
628
|
+
const schedule = minutelySchedule();
|
|
629
|
+
yield* store.upsertSchedule(schedule);
|
|
630
|
+
yield* TestClock.adjust(60_000);
|
|
631
|
+
const slotId = JobId("sched/TestJob/minutely/60000");
|
|
632
|
+
const request = baseRequest({ id: slotId, payload: { n: 7 } });
|
|
633
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
|
|
634
|
+
// Retention prunes the slot job — the old non-atomic design would
|
|
635
|
+
// now let a stale sweeper re-insert it. The CAS must still refuse.
|
|
636
|
+
expect(yield* store.remove(slotId)).toBe(true);
|
|
637
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 180_000, request)).toBe(false);
|
|
638
|
+
expect((yield* store.counts()).waiting).toBe(0);
|
|
639
|
+
expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
|
|
640
|
+
})));
|
|
641
|
+
it.effect("tickSchedule advances without firing when the slot job already exists", () => withStore((store) => Effect.gen(function* () {
|
|
642
|
+
const schedule = minutelySchedule();
|
|
643
|
+
yield* store.upsertSchedule(schedule);
|
|
644
|
+
yield* TestClock.adjust(60_000);
|
|
645
|
+
const slotId = JobId("sched/TestJob/minutely/60000");
|
|
646
|
+
// A pre-0.4 worker crashed between its enqueue and advance: the
|
|
647
|
+
// slot row exists but nextRunAt is stale. The tick must advance the
|
|
648
|
+
// schedule (or it stays due forever) yet report nothing new fired.
|
|
649
|
+
yield* store.enqueue(baseRequest({ id: slotId, payload: { n: 7 } }));
|
|
650
|
+
const request = baseRequest({ id: slotId, payload: { n: 7 } });
|
|
651
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(false);
|
|
652
|
+
expect((yield* store.listSchedules())[0]?.nextRunAt).toBe(120_000);
|
|
653
|
+
expect((yield* store.counts()).waiting).toBe(1);
|
|
654
|
+
})));
|
|
655
|
+
it.effect("tickSchedule rejects requests without an id and unknown keys", () => withStore((store) => Effect.gen(function* () {
|
|
656
|
+
const missing = yield* Effect.flip(store.tickSchedule(JobStore.ScheduleKey("TestJob/minutely"), 60_000, 120_000, baseRequest()));
|
|
657
|
+
expect(missing._tag).toBe("JobStoreError");
|
|
658
|
+
expect(yield* store.tickSchedule(JobStore.ScheduleKey("ghost"), 0, 60_000, baseRequest({ id: JobId("sched/ghost/0") }))).toBe(false);
|
|
659
|
+
expect((yield* store.counts()).waiting).toBe(0);
|
|
660
|
+
})));
|
|
661
|
+
it.effect("tickSchedule wakes parked takers", () => withStore((store) => Effect.gen(function* () {
|
|
662
|
+
const schedule = minutelySchedule();
|
|
663
|
+
yield* store.upsertSchedule(schedule);
|
|
664
|
+
const empty = yield* store.claim(claimOptions());
|
|
665
|
+
assert(empty._tag === "Empty");
|
|
666
|
+
const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken));
|
|
667
|
+
yield* Effect.yieldNow;
|
|
668
|
+
yield* TestClock.adjust(60_000);
|
|
669
|
+
const request = baseRequest({
|
|
670
|
+
id: JobId("sched/TestJob/minutely/60000"),
|
|
671
|
+
payload: { n: 7 }
|
|
672
|
+
});
|
|
673
|
+
expect(yield* store.tickSchedule(schedule.key, 60_000, 120_000, request)).toBe(true);
|
|
674
|
+
yield* Fiber.join(waiter);
|
|
675
|
+
const claim = yield* store.claim(claimOptions({ token: "t-2" }));
|
|
676
|
+
assert(claim._tag === "Claimed");
|
|
677
|
+
})));
|
|
678
|
+
it.effect("enqueueMany inserts a batch with positional results", () => withStore((store) => Effect.gen(function* () {
|
|
679
|
+
const results = yield* store.enqueueMany([
|
|
680
|
+
baseRequest({ payload: { n: 1 } }),
|
|
681
|
+
baseRequest({ payload: { n: 2 }, delayMs: 60_000 }),
|
|
682
|
+
baseRequest({ id: JobId("batch-3"), payload: { n: 3 }, priority: 5 })
|
|
683
|
+
]);
|
|
684
|
+
expect(results).toHaveLength(3);
|
|
685
|
+
expect(results.map((result) => result.duplicate)).toEqual([false, false, false]);
|
|
686
|
+
expect(new Set(results.map((result) => result.id)).size).toBe(3);
|
|
687
|
+
expect(results[2]?.id).toBe("batch-3");
|
|
688
|
+
const counts = yield* store.counts();
|
|
689
|
+
expect(counts.waiting).toBe(2);
|
|
690
|
+
expect(counts.delayed).toBe(1);
|
|
691
|
+
const explicit = yield* store.getJob(JobId("batch-3"));
|
|
692
|
+
assert(Option.isSome(explicit));
|
|
693
|
+
expect(explicit.value.payload).toEqual({ n: 3 });
|
|
694
|
+
expect(explicit.value.priority).toBe(5);
|
|
695
|
+
const delayedId = results[1]?.id;
|
|
696
|
+
assert(delayedId !== undefined);
|
|
697
|
+
const delayed = yield* store.getJob(delayedId);
|
|
698
|
+
assert(Option.isSome(delayed));
|
|
699
|
+
expect(delayed.value.state).toBe("delayed");
|
|
700
|
+
expect(delayed.value.runAt).toBe(60_000);
|
|
701
|
+
// Priority order survives the batch path.
|
|
702
|
+
const claim = yield* store.claim(claimOptions());
|
|
703
|
+
assert(claim._tag === "Claimed");
|
|
704
|
+
expect(claim.job.id).toBe("batch-3");
|
|
705
|
+
})));
|
|
706
|
+
it.effect("enqueueMany reports duplicates positionally without clobbering", () => withStore((store) => Effect.gen(function* () {
|
|
707
|
+
yield* store.enqueue(baseRequest({ id: JobId("dup-1"), payload: { n: 0 } }));
|
|
708
|
+
const results = yield* store.enqueueMany([
|
|
709
|
+
baseRequest({ payload: { n: 1 } }),
|
|
710
|
+
baseRequest({ id: JobId("dup-1"), payload: { n: 99 } }),
|
|
711
|
+
baseRequest({ payload: { n: 2 } })
|
|
712
|
+
]);
|
|
713
|
+
expect(results.map((result) => result.duplicate)).toEqual([false, true, false]);
|
|
714
|
+
expect(results[1]?.id).toBe("dup-1");
|
|
715
|
+
const kept = yield* store.getJob(JobId("dup-1"));
|
|
716
|
+
assert(Option.isSome(kept));
|
|
717
|
+
expect(kept.value.payload).toEqual({ n: 0 });
|
|
718
|
+
expect((yield* store.counts()).waiting).toBe(3);
|
|
719
|
+
})));
|
|
720
|
+
it.effect("enqueueMany resolves intra-batch repeats of one id like separate enqueues", () => withStore((store) => Effect.gen(function* () {
|
|
721
|
+
const results = yield* store.enqueueMany([
|
|
722
|
+
baseRequest({ id: JobId("same"), payload: { n: 1 } }),
|
|
723
|
+
baseRequest({ id: JobId("same"), payload: { n: 2 } })
|
|
724
|
+
]);
|
|
725
|
+
expect(results.map((result) => result.duplicate)).toEqual([false, true]);
|
|
726
|
+
expect(results[1]?.id).toBe("same");
|
|
727
|
+
expect((yield* store.counts()).waiting).toBe(1);
|
|
728
|
+
const job = yield* store.getJob(JobId("same"));
|
|
729
|
+
assert(Option.isSome(job));
|
|
730
|
+
expect(job.value.payload).toEqual({ n: 1 });
|
|
731
|
+
})));
|
|
732
|
+
it.effect("enqueueMany applies per-item dedup policies in order", () => withStore((store) => Effect.gen(function* () {
|
|
733
|
+
const dedupe = { key: "k", ttlMs: undefined, extend: false, replace: false };
|
|
734
|
+
const results = yield* store.enqueueMany([
|
|
735
|
+
baseRequest({ payload: { n: 1 }, dedupe }),
|
|
736
|
+
baseRequest({ payload: { n: 2 } }),
|
|
737
|
+
baseRequest({ payload: { n: 3 }, dedupe })
|
|
738
|
+
]);
|
|
739
|
+
expect(results.map((result) => result.duplicate)).toEqual([false, false, true]);
|
|
740
|
+
expect(results[2]?.id).toBe(results[0]?.id);
|
|
741
|
+
expect((yield* store.counts()).waiting).toBe(2);
|
|
742
|
+
})));
|
|
743
|
+
it.effect("enqueueMany with an empty batch returns an empty array", () => withStore((store) => Effect.gen(function* () {
|
|
744
|
+
expect(yield* store.enqueueMany([])).toEqual([]);
|
|
745
|
+
})));
|
|
746
|
+
// Pins the hand-duplicated field plumbing in batch/tick insert paths
|
|
747
|
+
// (positional Lua ARGV strides, multi-row VALUES lists): every persisted
|
|
748
|
+
// field must come out identical to the single-enqueue path. A swapped
|
|
749
|
+
// pair of ARGVs or a dropped column fails this even though the simpler
|
|
750
|
+
// tests (payload/priority/state only) stay green.
|
|
751
|
+
it.effect("batch and tick inserts persist every field exactly like enqueue", () => withStore((store) => Effect.gen(function* () {
|
|
752
|
+
const richRequest = (id) => baseRequest({
|
|
753
|
+
id: JobId(id),
|
|
754
|
+
payload: { big: 1234567890123456, nested: { arr: [1, 2, 3] } },
|
|
755
|
+
metadata: { tenant: "acme", region: "us" },
|
|
756
|
+
priority: 3,
|
|
757
|
+
attemptsMax: 4,
|
|
758
|
+
backoff: { _tag: "fixed", delayMs: 2_000 },
|
|
759
|
+
keep: { completed: { count: 2, ageMs: undefined } },
|
|
760
|
+
timeoutMs: 9_000,
|
|
761
|
+
trace: { traceId: "trace-1", spanId: "span-1", sampled: true, delayed: false }
|
|
762
|
+
});
|
|
763
|
+
yield* store.enqueue(richRequest("rich-single"));
|
|
764
|
+
expect(yield* store.enqueueMany([richRequest("rich-batch")]))
|
|
765
|
+
.toEqual([{ id: "rich-batch", duplicate: false }]);
|
|
766
|
+
const schedule = {
|
|
767
|
+
...minutelySchedule(),
|
|
768
|
+
key: JobStore.ScheduleKey("TestJob/parity"),
|
|
769
|
+
nextRunAt: 0
|
|
770
|
+
};
|
|
771
|
+
yield* store.upsertSchedule(schedule);
|
|
772
|
+
expect(yield* store.tickSchedule(schedule.key, 0, 60_000, richRequest("rich-tick")))
|
|
773
|
+
.toBe(true);
|
|
774
|
+
const project = (job) => ({
|
|
775
|
+
name: job.name,
|
|
776
|
+
queue: job.queue,
|
|
777
|
+
state: job.state,
|
|
778
|
+
payload: job.payload,
|
|
779
|
+
metadata: job.metadata,
|
|
780
|
+
priority: job.priority,
|
|
781
|
+
attemptsMax: job.attemptsMax,
|
|
782
|
+
attemptsMade: job.attemptsMade,
|
|
783
|
+
backoff: job.backoff,
|
|
784
|
+
keep: job.keep,
|
|
785
|
+
timeoutMs: job.timeoutMs,
|
|
786
|
+
cancelRequested: job.cancelRequested,
|
|
787
|
+
trace: job.trace,
|
|
788
|
+
runAt: job.runAt,
|
|
789
|
+
enqueuedAt: job.enqueuedAt
|
|
790
|
+
});
|
|
791
|
+
const single = yield* store.getJob(JobId("rich-single"));
|
|
792
|
+
assert(Option.isSome(single));
|
|
793
|
+
const expected = project(single.value);
|
|
794
|
+
// The reference row itself must carry the rich fields — otherwise
|
|
795
|
+
// three empty rows would compare equal and prove nothing.
|
|
796
|
+
expect(expected.backoff).toEqual({ _tag: "fixed", delayMs: 2_000 });
|
|
797
|
+
expect(expected.keep).toEqual({ completed: { count: 2 } });
|
|
798
|
+
expect(expected.timeoutMs).toBe(9_000);
|
|
799
|
+
expect(expected.trace).toEqual({
|
|
800
|
+
traceId: "trace-1",
|
|
801
|
+
spanId: "span-1",
|
|
802
|
+
sampled: true,
|
|
803
|
+
delayed: false
|
|
804
|
+
});
|
|
805
|
+
expect(expected.payload).toEqual({ big: 1234567890123456, nested: { arr: [1, 2, 3] } });
|
|
806
|
+
for (const id of [JobId("rich-batch"), JobId("rich-tick")]) {
|
|
807
|
+
const job = yield* store.getJob(id);
|
|
808
|
+
assert(Option.isSome(job));
|
|
809
|
+
expect(project(job.value)).toEqual(expected);
|
|
810
|
+
}
|
|
811
|
+
})));
|
|
812
|
+
it.effect("enqueueMany wakes parked takers", () => withStore((store) => Effect.gen(function* () {
|
|
813
|
+
const empty = yield* store.claim(claimOptions());
|
|
814
|
+
assert(empty._tag === "Empty");
|
|
815
|
+
const waiter = yield* Effect.forkChild(store.awaitWake([QueueName("default")], empty.wakeToken));
|
|
816
|
+
yield* Effect.yieldNow;
|
|
817
|
+
yield* store.enqueueMany([baseRequest()]);
|
|
818
|
+
yield* Fiber.join(waiter);
|
|
819
|
+
const claim = yield* store.claim(claimOptions({ token: "t-2" }));
|
|
820
|
+
assert(claim._tag === "Claimed");
|
|
821
|
+
})));
|
|
589
822
|
it.effect("completion wins over a pending cancel request", () => withStore((store) => Effect.gen(function* () {
|
|
590
823
|
const { id } = yield* store.enqueue(baseRequest());
|
|
591
824
|
const claim = yield* store.claim(claimOptions());
|
|
@@ -930,6 +1163,39 @@ export const jobStoreConformance = (name, storeLayer) => {
|
|
|
930
1163
|
yield* Fiber.join(waiter);
|
|
931
1164
|
expect(woke).toBe(true);
|
|
932
1165
|
})));
|
|
1166
|
+
it.effect("cancelByDedupe cancels pending keyed jobs and is idempotent", () => withStore((store) => Effect.gen(function* () {
|
|
1167
|
+
const dedupe = { key: "emp-1", ttlMs: undefined, extend: false, replace: false };
|
|
1168
|
+
// Unknown key: nothing pending, no error.
|
|
1169
|
+
expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(false);
|
|
1170
|
+
// Delayed keyed job: cancelled terminally.
|
|
1171
|
+
const delayed = yield* store.enqueue(baseRequest({ dedupe, delayMs: 60_000 }));
|
|
1172
|
+
expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(true);
|
|
1173
|
+
const job = yield* store.getJob(delayed.id);
|
|
1174
|
+
assert(Option.isSome(job));
|
|
1175
|
+
expect(job.value.state).toBe("cancelled");
|
|
1176
|
+
expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(false);
|
|
1177
|
+
// Active keyed job: flagged for the heartbeat.
|
|
1178
|
+
const active = yield* store.enqueue(baseRequest({ dedupe, payload: { n: 2 } }));
|
|
1179
|
+
const claim = yield* store.claim(claimOptions());
|
|
1180
|
+
assert(claim._tag === "Claimed");
|
|
1181
|
+
expect(yield* store.cancelByDedupe("TestJob", "emp-1")).toBe(true);
|
|
1182
|
+
const flagged = yield* store.getJob(active.id);
|
|
1183
|
+
assert(Option.isSome(flagged));
|
|
1184
|
+
expect(flagged.value.cancelRequested).toBe(true);
|
|
1185
|
+
// Name scoping: same key under another name is untouched.
|
|
1186
|
+
expect(yield* store.cancelByDedupe("OtherJob", "emp-1")).toBe(false);
|
|
1187
|
+
})));
|
|
1188
|
+
it.effect("the producer's trace context round-trips on the record", () => withStore((store) => Effect.gen(function* () {
|
|
1189
|
+
const trace = { traceId: "trace-abc", spanId: "span-def", sampled: true, delayed: false };
|
|
1190
|
+
const { id } = yield* store.enqueue(baseRequest({ trace }));
|
|
1191
|
+
const job = yield* store.getJob(id);
|
|
1192
|
+
assert(Option.isSome(job));
|
|
1193
|
+
expect(job.value.trace).toEqual(trace);
|
|
1194
|
+
const bare = yield* store.enqueue(baseRequest({ payload: { n: 2 } }));
|
|
1195
|
+
const none = yield* store.getJob(bare.id);
|
|
1196
|
+
assert(Option.isSome(none));
|
|
1197
|
+
expect(none.value.trace).toBeUndefined();
|
|
1198
|
+
})));
|
|
933
1199
|
it.effect("delayed jobs still promote to waiting while their queue is paused", () => withStore((store) => Effect.gen(function* () {
|
|
934
1200
|
const { id } = yield* store.enqueue(baseRequest({ delayMs: 1_000 }));
|
|
935
1201
|
yield* store.pause(QueueName("default"));
|