@chidchanun/bcp 0.2.9 → 0.2.11
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 +196 -129
- package/docs/README.md +49 -43
- package/docs/api-manifest.json +15 -2
- package/docs/api-reference.md +113 -8
- package/docs/docs-web-manifest.json +7 -3
- package/docs/durable-jobs.md +359 -0
- package/docs/platform-manifest.json +26 -4
- package/docs/releases/0.2.10.md +148 -0
- package/docs/releases/0.2.11.md +180 -0
- package/docs/workflow-orchestration.md +374 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/jobs.mjs +1310 -61
- package/packages/client/src/jobs.ts +17 -0
- package/packages/client/src/workflow.mjs +601 -0
- package/packages/client/src/workflow.ts +23 -0
- package/packages/server/src/jobs-redis.ts +1226 -0
- package/packages/server/src/jobs.ts +768 -128
- package/packages/server/src/workflow.ts +887 -0
|
@@ -4,6 +4,33 @@ import {
|
|
|
4
4
|
} from "node:crypto";
|
|
5
5
|
function createMemoryJobQueueAdapter() {
|
|
6
6
|
const jobs = /* @__PURE__ */ new Map();
|
|
7
|
+
const deadLetters = /* @__PURE__ */ new Map();
|
|
8
|
+
const recoverStale = (now, options = {}) => {
|
|
9
|
+
const limit = normalizePositiveInteger(
|
|
10
|
+
options.limit ?? 100,
|
|
11
|
+
"recovery limit"
|
|
12
|
+
);
|
|
13
|
+
const stale = Array.from(
|
|
14
|
+
jobs.values()
|
|
15
|
+
).filter(
|
|
16
|
+
(job) => job.state === "running" && job.leaseUntil !== void 0 && job.leaseUntil <= now
|
|
17
|
+
).sort(
|
|
18
|
+
(left, right) => (left.leaseUntil ?? 0) - (right.leaseUntil ?? 0) || left.id.localeCompare(
|
|
19
|
+
right.id
|
|
20
|
+
)
|
|
21
|
+
).slice(
|
|
22
|
+
0,
|
|
23
|
+
limit
|
|
24
|
+
);
|
|
25
|
+
for (const job of stale) {
|
|
26
|
+
job.state = "queued";
|
|
27
|
+
job.availableAt = now;
|
|
28
|
+
job.startedAt = void 0;
|
|
29
|
+
clearLease(job);
|
|
30
|
+
job.recoveredAt = now;
|
|
31
|
+
}
|
|
32
|
+
return stale.length;
|
|
33
|
+
};
|
|
7
34
|
return {
|
|
8
35
|
async enqueue(job) {
|
|
9
36
|
if (jobs.has(job.id)) {
|
|
@@ -16,7 +43,13 @@ function createMemoryJobQueueAdapter() {
|
|
|
16
43
|
cloneJob(job)
|
|
17
44
|
);
|
|
18
45
|
},
|
|
19
|
-
async reserve(now) {
|
|
46
|
+
async reserve(now, options) {
|
|
47
|
+
recoverStale(
|
|
48
|
+
now,
|
|
49
|
+
{
|
|
50
|
+
limit: 100
|
|
51
|
+
}
|
|
52
|
+
);
|
|
20
53
|
const candidate = Array.from(
|
|
21
54
|
jobs.values()
|
|
22
55
|
).filter(
|
|
@@ -31,25 +64,43 @@ function createMemoryJobQueueAdapter() {
|
|
|
31
64
|
candidate.attempts += 1;
|
|
32
65
|
candidate.startedAt = now;
|
|
33
66
|
candidate.error = void 0;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
candidate.recoveredAt = void 0;
|
|
68
|
+
if (options) {
|
|
69
|
+
candidate.leaseOwner = normalizeWorkerId(
|
|
70
|
+
options.ownerId
|
|
71
|
+
);
|
|
72
|
+
candidate.leaseUntil = now + normalizePositiveInteger(
|
|
73
|
+
options.visibilityTimeoutMs,
|
|
74
|
+
"visibilityTimeoutMs"
|
|
75
|
+
);
|
|
76
|
+
candidate.heartbeatAt = now;
|
|
77
|
+
}
|
|
78
|
+
return cloneJob(candidate);
|
|
37
79
|
},
|
|
38
|
-
async complete(id, completedAt) {
|
|
80
|
+
async complete(id, completedAt, ownerId) {
|
|
39
81
|
const job = jobs.get(id);
|
|
40
|
-
if (!job || job.state === "cancelled"
|
|
82
|
+
if (!job || job.state === "cancelled" || !leaseOwnerMatches(
|
|
83
|
+
job,
|
|
84
|
+
ownerId
|
|
85
|
+
)) {
|
|
41
86
|
return;
|
|
42
87
|
}
|
|
43
88
|
job.state = "succeeded";
|
|
44
89
|
job.completedAt = completedAt;
|
|
45
90
|
job.error = void 0;
|
|
91
|
+
clearLease(job);
|
|
92
|
+
deadLetters.delete(id);
|
|
46
93
|
},
|
|
47
94
|
async fail(id, options) {
|
|
48
95
|
const job = jobs.get(id);
|
|
49
|
-
if (!job || job.state === "cancelled"
|
|
96
|
+
if (!job || job.state === "cancelled" || !leaseOwnerMatches(
|
|
97
|
+
job,
|
|
98
|
+
options.ownerId
|
|
99
|
+
)) {
|
|
50
100
|
return;
|
|
51
101
|
}
|
|
52
102
|
job.error = options.error;
|
|
103
|
+
clearLease(job);
|
|
53
104
|
if (options.retryAt !== void 0 && job.attempts < job.maxAttempts) {
|
|
54
105
|
job.state = "queued";
|
|
55
106
|
job.availableAt = options.retryAt;
|
|
@@ -58,6 +109,14 @@ function createMemoryJobQueueAdapter() {
|
|
|
58
109
|
}
|
|
59
110
|
job.state = "failed";
|
|
60
111
|
job.completedAt = options.failedAt;
|
|
112
|
+
deadLetters.set(
|
|
113
|
+
id,
|
|
114
|
+
{
|
|
115
|
+
...cloneJob(job),
|
|
116
|
+
state: "failed",
|
|
117
|
+
deadLetteredAt: options.failedAt
|
|
118
|
+
}
|
|
119
|
+
);
|
|
61
120
|
},
|
|
62
121
|
async cancel(id, cancelledAt) {
|
|
63
122
|
const job = jobs.get(id);
|
|
@@ -66,6 +125,7 @@ function createMemoryJobQueueAdapter() {
|
|
|
66
125
|
}
|
|
67
126
|
job.state = "cancelled";
|
|
68
127
|
job.completedAt = cancelledAt;
|
|
128
|
+
clearLease(job);
|
|
69
129
|
return true;
|
|
70
130
|
},
|
|
71
131
|
async get(id) {
|
|
@@ -79,8 +139,91 @@ function createMemoryJobQueueAdapter() {
|
|
|
79
139
|
(left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)
|
|
80
140
|
);
|
|
81
141
|
},
|
|
142
|
+
async heartbeat(id, options) {
|
|
143
|
+
const job = jobs.get(id);
|
|
144
|
+
if (!job || job.state !== "running" || job.leaseOwner !== options.ownerId) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const visibilityTimeoutMs = normalizePositiveInteger(
|
|
148
|
+
options.visibilityTimeoutMs,
|
|
149
|
+
"visibilityTimeoutMs"
|
|
150
|
+
);
|
|
151
|
+
job.heartbeatAt = options.heartbeatAt;
|
|
152
|
+
job.leaseUntil = options.heartbeatAt + visibilityTimeoutMs;
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
155
|
+
async recoverStale(now, options) {
|
|
156
|
+
return recoverStale(
|
|
157
|
+
now,
|
|
158
|
+
options
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
async listDeadLetters() {
|
|
162
|
+
return Array.from(
|
|
163
|
+
deadLetters.values()
|
|
164
|
+
).map(cloneDeadLetter).sort(
|
|
165
|
+
(left, right) => left.deadLetteredAt - right.deadLetteredAt || left.id.localeCompare(
|
|
166
|
+
right.id
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
async requeueDeadLetter(id, now, options = {}) {
|
|
171
|
+
const job = jobs.get(id);
|
|
172
|
+
if (!job || job.state !== "failed" || !deadLetters.has(id)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const delayMs = normalizeNonNegativeNumber(
|
|
176
|
+
options.delayMs ?? 0,
|
|
177
|
+
"delayMs"
|
|
178
|
+
);
|
|
179
|
+
job.state = "queued";
|
|
180
|
+
job.availableAt = now + delayMs;
|
|
181
|
+
job.startedAt = void 0;
|
|
182
|
+
job.completedAt = void 0;
|
|
183
|
+
job.error = void 0;
|
|
184
|
+
job.recoveredAt = void 0;
|
|
185
|
+
clearLease(job);
|
|
186
|
+
if (options.resetAttempts !== false) {
|
|
187
|
+
job.attempts = 0;
|
|
188
|
+
}
|
|
189
|
+
deadLetters.delete(id);
|
|
190
|
+
return true;
|
|
191
|
+
},
|
|
192
|
+
async cleanup(options) {
|
|
193
|
+
const states = new Set(
|
|
194
|
+
options.states ?? [
|
|
195
|
+
"succeeded",
|
|
196
|
+
"failed",
|
|
197
|
+
"cancelled"
|
|
198
|
+
]
|
|
199
|
+
);
|
|
200
|
+
let removed = 0;
|
|
201
|
+
for (const [
|
|
202
|
+
id,
|
|
203
|
+
job
|
|
204
|
+
] of jobs) {
|
|
205
|
+
if (!states.has(
|
|
206
|
+
job.state
|
|
207
|
+
) || job.completedAt === void 0 || job.completedAt >= options.before) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
jobs.delete(id);
|
|
211
|
+
deadLetters.delete(id);
|
|
212
|
+
removed += 1;
|
|
213
|
+
}
|
|
214
|
+
return removed;
|
|
215
|
+
},
|
|
216
|
+
async stats() {
|
|
217
|
+
return calculateJobStats(
|
|
218
|
+
Array.from(
|
|
219
|
+
jobs.values()
|
|
220
|
+
),
|
|
221
|
+
deadLetters.size
|
|
222
|
+
);
|
|
223
|
+
},
|
|
82
224
|
clear() {
|
|
83
225
|
jobs.clear();
|
|
226
|
+
deadLetters.clear();
|
|
84
227
|
}
|
|
85
228
|
};
|
|
86
229
|
}
|
|
@@ -147,9 +290,7 @@ function createJobQueue(options = {}) {
|
|
|
147
290
|
);
|
|
148
291
|
const job = {
|
|
149
292
|
id,
|
|
150
|
-
name: normalizeJobName(
|
|
151
|
-
name
|
|
152
|
-
),
|
|
293
|
+
name: normalizeJobName(name),
|
|
153
294
|
payload,
|
|
154
295
|
state: "queued",
|
|
155
296
|
attempts: 0,
|
|
@@ -157,9 +298,7 @@ function createJobQueue(options = {}) {
|
|
|
157
298
|
createdAt,
|
|
158
299
|
availableAt: createdAt + delayMs
|
|
159
300
|
};
|
|
160
|
-
await adapter.enqueue(
|
|
161
|
-
job
|
|
162
|
-
);
|
|
301
|
+
await adapter.enqueue(job);
|
|
163
302
|
return cloneJob(job);
|
|
164
303
|
},
|
|
165
304
|
get(id) {
|
|
@@ -176,58 +315,103 @@ function createJobQueue(options = {}) {
|
|
|
176
315
|
now()
|
|
177
316
|
);
|
|
178
317
|
},
|
|
179
|
-
async processNext(signal = new AbortController().signal) {
|
|
318
|
+
async processNext(signal = new AbortController().signal, processOptions = {}) {
|
|
180
319
|
if (signal.aborted) {
|
|
181
320
|
return false;
|
|
182
321
|
}
|
|
183
|
-
const
|
|
184
|
-
|
|
322
|
+
const visibilityTimeoutMs = normalizePositiveInteger(
|
|
323
|
+
processOptions.visibilityTimeoutMs ?? 3e4,
|
|
324
|
+
"visibilityTimeoutMs"
|
|
185
325
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
326
|
+
const heartbeatIntervalMs = normalizePositiveInteger(
|
|
327
|
+
processOptions.heartbeatIntervalMs ?? Math.max(
|
|
328
|
+
1,
|
|
329
|
+
Math.floor(
|
|
330
|
+
visibilityTimeoutMs / 3
|
|
331
|
+
)
|
|
332
|
+
),
|
|
333
|
+
"heartbeatIntervalMs"
|
|
191
334
|
);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
335
|
+
const ownerId = normalizeWorkerId(
|
|
336
|
+
processOptions.ownerId ?? `process-${randomUUID()}`
|
|
337
|
+
);
|
|
338
|
+
if (adapter.recoverStale) {
|
|
339
|
+
await adapter.recoverStale(
|
|
340
|
+
now(),
|
|
195
341
|
{
|
|
196
|
-
|
|
197
|
-
|
|
342
|
+
limit: normalizePositiveInteger(
|
|
343
|
+
processOptions.recoveryLimit ?? 100,
|
|
344
|
+
"recoveryLimit"
|
|
345
|
+
)
|
|
198
346
|
}
|
|
199
347
|
);
|
|
200
|
-
return true;
|
|
201
348
|
}
|
|
349
|
+
const job = await adapter.reserve(
|
|
350
|
+
now(),
|
|
351
|
+
{
|
|
352
|
+
ownerId,
|
|
353
|
+
visibilityTimeoutMs
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
if (!job) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const stopHeartbeat = startJobHeartbeat(
|
|
360
|
+
adapter,
|
|
361
|
+
job.id,
|
|
362
|
+
ownerId,
|
|
363
|
+
visibilityTimeoutMs,
|
|
364
|
+
heartbeatIntervalMs,
|
|
365
|
+
now,
|
|
366
|
+
signal
|
|
367
|
+
);
|
|
368
|
+
const handler = handlers.get(job.name);
|
|
202
369
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
370
|
+
if (!handler) {
|
|
371
|
+
await adapter.fail(
|
|
372
|
+
job.id,
|
|
373
|
+
{
|
|
374
|
+
error: `No handler registered for job "${job.name}".`,
|
|
375
|
+
failedAt: now(),
|
|
376
|
+
ownerId
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
await handler({
|
|
383
|
+
job: cloneJob(job),
|
|
384
|
+
payload: job.payload,
|
|
385
|
+
signal
|
|
386
|
+
});
|
|
387
|
+
await adapter.complete(
|
|
388
|
+
job.id,
|
|
389
|
+
now(),
|
|
390
|
+
ownerId
|
|
391
|
+
);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
const failedAt = now();
|
|
394
|
+
const shouldRetry = job.attempts < job.maxAttempts;
|
|
395
|
+
const retryAt = shouldRetry ? failedAt + resolveRetryDelay(
|
|
396
|
+
retryDelay,
|
|
397
|
+
job.attempts
|
|
398
|
+
) : void 0;
|
|
399
|
+
await adapter.fail(
|
|
400
|
+
job.id,
|
|
401
|
+
{
|
|
402
|
+
error: formatJobError(
|
|
403
|
+
error
|
|
404
|
+
),
|
|
405
|
+
failedAt,
|
|
406
|
+
retryAt,
|
|
407
|
+
ownerId
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
} finally {
|
|
413
|
+
stopHeartbeat();
|
|
229
414
|
}
|
|
230
|
-
return true;
|
|
231
415
|
},
|
|
232
416
|
startWorker(workerOptions = {}) {
|
|
233
417
|
const worker = createWorker(
|
|
@@ -240,6 +424,51 @@ function createJobQueue(options = {}) {
|
|
|
240
424
|
workers.add(worker);
|
|
241
425
|
return worker;
|
|
242
426
|
},
|
|
427
|
+
async recoverStale(recoverOptions = {}) {
|
|
428
|
+
if (!adapter.recoverStale) {
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
return adapter.recoverStale(
|
|
432
|
+
now(),
|
|
433
|
+
recoverOptions
|
|
434
|
+
);
|
|
435
|
+
},
|
|
436
|
+
async deadLetters() {
|
|
437
|
+
return adapter.listDeadLetters ? adapter.listDeadLetters() : [];
|
|
438
|
+
},
|
|
439
|
+
async requeueDeadLetter(id, requeueOptions = {}) {
|
|
440
|
+
if (!adapter.requeueDeadLetter) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return adapter.requeueDeadLetter(
|
|
444
|
+
normalizeJobId(id),
|
|
445
|
+
now(),
|
|
446
|
+
requeueOptions
|
|
447
|
+
);
|
|
448
|
+
},
|
|
449
|
+
async cleanup(cleanupOptions) {
|
|
450
|
+
if (!adapter.cleanup) {
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
const before = normalizeNonNegativeNumber(
|
|
454
|
+
cleanupOptions.before,
|
|
455
|
+
"cleanup before"
|
|
456
|
+
);
|
|
457
|
+
return adapter.cleanup({
|
|
458
|
+
...cleanupOptions,
|
|
459
|
+
before
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
async stats() {
|
|
463
|
+
if (adapter.stats) {
|
|
464
|
+
return adapter.stats();
|
|
465
|
+
}
|
|
466
|
+
const jobs = await adapter.list();
|
|
467
|
+
return calculateJobStats(
|
|
468
|
+
jobs,
|
|
469
|
+
adapter.listDeadLetters ? (await adapter.listDeadLetters()).length : 0
|
|
470
|
+
);
|
|
471
|
+
},
|
|
243
472
|
async close() {
|
|
244
473
|
await Promise.all(
|
|
245
474
|
Array.from(
|
|
@@ -263,6 +492,26 @@ function createWorker(queue, options, onStop) {
|
|
|
263
492
|
options.pollIntervalMs ?? 250,
|
|
264
493
|
"pollIntervalMs"
|
|
265
494
|
);
|
|
495
|
+
const visibilityTimeoutMs = normalizePositiveInteger(
|
|
496
|
+
options.visibilityTimeoutMs ?? 3e4,
|
|
497
|
+
"visibilityTimeoutMs"
|
|
498
|
+
);
|
|
499
|
+
const heartbeatIntervalMs = normalizePositiveInteger(
|
|
500
|
+
options.heartbeatIntervalMs ?? Math.max(
|
|
501
|
+
1,
|
|
502
|
+
Math.floor(
|
|
503
|
+
visibilityTimeoutMs / 3
|
|
504
|
+
)
|
|
505
|
+
),
|
|
506
|
+
"heartbeatIntervalMs"
|
|
507
|
+
);
|
|
508
|
+
const recoveryLimit = normalizePositiveInteger(
|
|
509
|
+
options.recoveryLimit ?? 100,
|
|
510
|
+
"recoveryLimit"
|
|
511
|
+
);
|
|
512
|
+
const workerId = normalizeWorkerId(
|
|
513
|
+
options.workerId ?? `worker-${randomUUID()}`
|
|
514
|
+
);
|
|
266
515
|
const controller = new AbortController();
|
|
267
516
|
let running = true;
|
|
268
517
|
let stopPromise = null;
|
|
@@ -270,16 +519,24 @@ function createWorker(queue, options, onStop) {
|
|
|
270
519
|
{
|
|
271
520
|
length: concurrency
|
|
272
521
|
},
|
|
273
|
-
() => runWorkerLoop(
|
|
522
|
+
(_, index) => runWorkerLoop(
|
|
274
523
|
queue,
|
|
275
524
|
controller.signal,
|
|
276
|
-
pollIntervalMs
|
|
525
|
+
pollIntervalMs,
|
|
526
|
+
{
|
|
527
|
+
ownerId: `${workerId}:${index + 1}`,
|
|
528
|
+
visibilityTimeoutMs,
|
|
529
|
+
heartbeatIntervalMs,
|
|
530
|
+
recoveryLimit
|
|
531
|
+
},
|
|
532
|
+
options.onError
|
|
277
533
|
)
|
|
278
534
|
);
|
|
279
535
|
return {
|
|
280
536
|
get running() {
|
|
281
537
|
return running;
|
|
282
538
|
},
|
|
539
|
+
workerId,
|
|
283
540
|
stop() {
|
|
284
541
|
if (stopPromise) {
|
|
285
542
|
return stopPromise;
|
|
@@ -297,11 +554,24 @@ function createWorker(queue, options, onStop) {
|
|
|
297
554
|
}
|
|
298
555
|
};
|
|
299
556
|
}
|
|
300
|
-
async function runWorkerLoop(queue, signal, pollIntervalMs) {
|
|
557
|
+
async function runWorkerLoop(queue, signal, pollIntervalMs, options, onError) {
|
|
301
558
|
while (!signal.aborted) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
559
|
+
let processed = false;
|
|
560
|
+
try {
|
|
561
|
+
processed = await queue.processNext(
|
|
562
|
+
signal,
|
|
563
|
+
options
|
|
564
|
+
);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (signal.aborted) {
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (onError) {
|
|
570
|
+
await onError(error);
|
|
571
|
+
} else {
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
305
575
|
if (!processed && !signal.aborted) {
|
|
306
576
|
await sleep(
|
|
307
577
|
pollIntervalMs,
|
|
@@ -310,8 +580,50 @@ async function runWorkerLoop(queue, signal, pollIntervalMs) {
|
|
|
310
580
|
}
|
|
311
581
|
}
|
|
312
582
|
}
|
|
583
|
+
function startJobHeartbeat(adapter, jobId, ownerId, visibilityTimeoutMs, heartbeatIntervalMs, now, signal) {
|
|
584
|
+
if (!adapter.heartbeat) {
|
|
585
|
+
return () => void 0;
|
|
586
|
+
}
|
|
587
|
+
let stopped = false;
|
|
588
|
+
let timeout;
|
|
589
|
+
const schedule = () => {
|
|
590
|
+
if (stopped || signal.aborted) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
timeout = setTimeout(
|
|
594
|
+
async () => {
|
|
595
|
+
if (stopped || signal.aborted) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const active = await adapter.heartbeat?.(
|
|
600
|
+
jobId,
|
|
601
|
+
{
|
|
602
|
+
ownerId,
|
|
603
|
+
heartbeatAt: now(),
|
|
604
|
+
visibilityTimeoutMs
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
if (active !== false) {
|
|
608
|
+
schedule();
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
schedule();
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
heartbeatIntervalMs
|
|
615
|
+
);
|
|
616
|
+
};
|
|
617
|
+
schedule();
|
|
618
|
+
return () => {
|
|
619
|
+
stopped = true;
|
|
620
|
+
if (timeout) {
|
|
621
|
+
clearTimeout(timeout);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
313
625
|
function sleep(durationMs, signal) {
|
|
314
|
-
if (
|
|
626
|
+
if (signal.aborted) {
|
|
315
627
|
return Promise.resolve();
|
|
316
628
|
}
|
|
317
629
|
return new Promise(
|
|
@@ -339,6 +651,21 @@ function sleep(durationMs, signal) {
|
|
|
339
651
|
}
|
|
340
652
|
);
|
|
341
653
|
}
|
|
654
|
+
function calculateJobStats(jobs, deadLetters) {
|
|
655
|
+
const stats = {
|
|
656
|
+
total: jobs.length,
|
|
657
|
+
queued: 0,
|
|
658
|
+
running: 0,
|
|
659
|
+
succeeded: 0,
|
|
660
|
+
failed: 0,
|
|
661
|
+
cancelled: 0,
|
|
662
|
+
deadLetters
|
|
663
|
+
};
|
|
664
|
+
for (const job of jobs) {
|
|
665
|
+
stats[job.state] += 1;
|
|
666
|
+
}
|
|
667
|
+
return stats;
|
|
668
|
+
}
|
|
342
669
|
function resolveRetryDelay(value, attempt) {
|
|
343
670
|
const delay = typeof value === "function" ? value(attempt) : value;
|
|
344
671
|
return normalizeNonNegativeNumber(
|
|
@@ -374,6 +701,15 @@ function normalizeJobId(value) {
|
|
|
374
701
|
}
|
|
375
702
|
return id;
|
|
376
703
|
}
|
|
704
|
+
function normalizeWorkerId(value) {
|
|
705
|
+
const id = String(value).trim();
|
|
706
|
+
if (!id) {
|
|
707
|
+
throw new TypeError(
|
|
708
|
+
"BCP Jobs: worker id must be a non-empty string."
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return id;
|
|
712
|
+
}
|
|
377
713
|
function normalizePositiveInteger(value, field) {
|
|
378
714
|
if (!Number.isInteger(value) || value <= 0) {
|
|
379
715
|
throw new TypeError(
|
|
@@ -404,11 +740,27 @@ function formatJobError(error) {
|
|
|
404
740
|
return String(error);
|
|
405
741
|
}
|
|
406
742
|
}
|
|
743
|
+
function leaseOwnerMatches(job, ownerId) {
|
|
744
|
+
if (ownerId === void 0) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
return job.leaseOwner === ownerId;
|
|
748
|
+
}
|
|
749
|
+
function clearLease(job) {
|
|
750
|
+
job.leaseOwner = void 0;
|
|
751
|
+
job.leaseUntil = void 0;
|
|
752
|
+
job.heartbeatAt = void 0;
|
|
753
|
+
}
|
|
407
754
|
function cloneJob(job) {
|
|
408
755
|
return {
|
|
409
756
|
...job
|
|
410
757
|
};
|
|
411
758
|
}
|
|
759
|
+
function cloneDeadLetter(job) {
|
|
760
|
+
return {
|
|
761
|
+
...job
|
|
762
|
+
};
|
|
763
|
+
}
|
|
412
764
|
|
|
413
765
|
// packages/server/src/job-scheduler.ts
|
|
414
766
|
import {
|
|
@@ -1030,11 +1382,908 @@ function cloneSchedule(schedule) {
|
|
|
1030
1382
|
}
|
|
1031
1383
|
};
|
|
1032
1384
|
}
|
|
1385
|
+
|
|
1386
|
+
// packages/server/src/jobs-redis.ts
|
|
1387
|
+
function createRedisJobQueueAdapter(options) {
|
|
1388
|
+
const client = requireRedisClient(
|
|
1389
|
+
options.client
|
|
1390
|
+
);
|
|
1391
|
+
const namespace = normalizeNamespace(
|
|
1392
|
+
options.namespace
|
|
1393
|
+
);
|
|
1394
|
+
const keys = createQueueKeys(
|
|
1395
|
+
namespace
|
|
1396
|
+
);
|
|
1397
|
+
return {
|
|
1398
|
+
namespace,
|
|
1399
|
+
async enqueue(job) {
|
|
1400
|
+
const result = await evalRedis(
|
|
1401
|
+
client,
|
|
1402
|
+
ENQUEUE_SCRIPT,
|
|
1403
|
+
[
|
|
1404
|
+
jobKey(
|
|
1405
|
+
keys.jobPrefix,
|
|
1406
|
+
job.id
|
|
1407
|
+
),
|
|
1408
|
+
keys.all,
|
|
1409
|
+
keys.available
|
|
1410
|
+
],
|
|
1411
|
+
[
|
|
1412
|
+
job.id,
|
|
1413
|
+
JSON.stringify(job),
|
|
1414
|
+
String(
|
|
1415
|
+
job.availableAt
|
|
1416
|
+
)
|
|
1417
|
+
]
|
|
1418
|
+
);
|
|
1419
|
+
if (Number(result) !== 1) {
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
`BCP Jobs Redis: job id "${job.id}" already exists.`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
async reserve(now, reserveOptions) {
|
|
1426
|
+
const options2 = reserveOptions ?? {
|
|
1427
|
+
ownerId: "legacy-worker",
|
|
1428
|
+
visibilityTimeoutMs: 3e4
|
|
1429
|
+
};
|
|
1430
|
+
const raw = await evalRedis(
|
|
1431
|
+
client,
|
|
1432
|
+
RESERVE_SCRIPT,
|
|
1433
|
+
[
|
|
1434
|
+
keys.available,
|
|
1435
|
+
keys.running
|
|
1436
|
+
],
|
|
1437
|
+
[
|
|
1438
|
+
keys.jobPrefix,
|
|
1439
|
+
String(now),
|
|
1440
|
+
options2.ownerId,
|
|
1441
|
+
String(
|
|
1442
|
+
normalizePositiveInteger3(
|
|
1443
|
+
options2.visibilityTimeoutMs,
|
|
1444
|
+
"visibilityTimeoutMs"
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
]
|
|
1448
|
+
);
|
|
1449
|
+
return parseJsonReply(raw);
|
|
1450
|
+
},
|
|
1451
|
+
async complete(id, completedAt, ownerId) {
|
|
1452
|
+
await evalRedis(
|
|
1453
|
+
client,
|
|
1454
|
+
COMPLETE_SCRIPT,
|
|
1455
|
+
[
|
|
1456
|
+
jobKey(
|
|
1457
|
+
keys.jobPrefix,
|
|
1458
|
+
id
|
|
1459
|
+
),
|
|
1460
|
+
keys.available,
|
|
1461
|
+
keys.running,
|
|
1462
|
+
keys.terminal,
|
|
1463
|
+
keys.deadLetters
|
|
1464
|
+
],
|
|
1465
|
+
[
|
|
1466
|
+
id,
|
|
1467
|
+
String(completedAt),
|
|
1468
|
+
ownerId ?? ""
|
|
1469
|
+
]
|
|
1470
|
+
);
|
|
1471
|
+
},
|
|
1472
|
+
async fail(id, failOptions) {
|
|
1473
|
+
await evalRedis(
|
|
1474
|
+
client,
|
|
1475
|
+
FAIL_SCRIPT,
|
|
1476
|
+
[
|
|
1477
|
+
jobKey(
|
|
1478
|
+
keys.jobPrefix,
|
|
1479
|
+
id
|
|
1480
|
+
),
|
|
1481
|
+
keys.available,
|
|
1482
|
+
keys.running,
|
|
1483
|
+
keys.terminal,
|
|
1484
|
+
keys.deadLetters
|
|
1485
|
+
],
|
|
1486
|
+
[
|
|
1487
|
+
id,
|
|
1488
|
+
failOptions.error,
|
|
1489
|
+
String(
|
|
1490
|
+
failOptions.failedAt
|
|
1491
|
+
),
|
|
1492
|
+
failOptions.retryAt === void 0 ? "" : String(
|
|
1493
|
+
failOptions.retryAt
|
|
1494
|
+
),
|
|
1495
|
+
failOptions.ownerId ?? ""
|
|
1496
|
+
]
|
|
1497
|
+
);
|
|
1498
|
+
},
|
|
1499
|
+
async cancel(id, cancelledAt) {
|
|
1500
|
+
const result = await evalRedis(
|
|
1501
|
+
client,
|
|
1502
|
+
CANCEL_SCRIPT,
|
|
1503
|
+
[
|
|
1504
|
+
jobKey(
|
|
1505
|
+
keys.jobPrefix,
|
|
1506
|
+
id
|
|
1507
|
+
),
|
|
1508
|
+
keys.available,
|
|
1509
|
+
keys.running,
|
|
1510
|
+
keys.terminal
|
|
1511
|
+
],
|
|
1512
|
+
[
|
|
1513
|
+
id,
|
|
1514
|
+
String(cancelledAt)
|
|
1515
|
+
]
|
|
1516
|
+
);
|
|
1517
|
+
return Number(result) === 1;
|
|
1518
|
+
},
|
|
1519
|
+
async get(id) {
|
|
1520
|
+
const raw = await client.sendCommand([
|
|
1521
|
+
"GET",
|
|
1522
|
+
jobKey(
|
|
1523
|
+
keys.jobPrefix,
|
|
1524
|
+
id
|
|
1525
|
+
)
|
|
1526
|
+
]);
|
|
1527
|
+
return parseJsonReply(raw);
|
|
1528
|
+
},
|
|
1529
|
+
async list() {
|
|
1530
|
+
return readJobsByIds(
|
|
1531
|
+
client,
|
|
1532
|
+
keys.jobPrefix,
|
|
1533
|
+
await readStringList(
|
|
1534
|
+
client,
|
|
1535
|
+
[
|
|
1536
|
+
"SMEMBERS",
|
|
1537
|
+
keys.all
|
|
1538
|
+
]
|
|
1539
|
+
)
|
|
1540
|
+
);
|
|
1541
|
+
},
|
|
1542
|
+
async heartbeat(id, heartbeatOptions) {
|
|
1543
|
+
const result = await evalRedis(
|
|
1544
|
+
client,
|
|
1545
|
+
HEARTBEAT_SCRIPT,
|
|
1546
|
+
[
|
|
1547
|
+
jobKey(
|
|
1548
|
+
keys.jobPrefix,
|
|
1549
|
+
id
|
|
1550
|
+
),
|
|
1551
|
+
keys.running
|
|
1552
|
+
],
|
|
1553
|
+
[
|
|
1554
|
+
id,
|
|
1555
|
+
heartbeatOptions.ownerId,
|
|
1556
|
+
String(
|
|
1557
|
+
heartbeatOptions.heartbeatAt
|
|
1558
|
+
),
|
|
1559
|
+
String(
|
|
1560
|
+
normalizePositiveInteger3(
|
|
1561
|
+
heartbeatOptions.visibilityTimeoutMs,
|
|
1562
|
+
"visibilityTimeoutMs"
|
|
1563
|
+
)
|
|
1564
|
+
)
|
|
1565
|
+
]
|
|
1566
|
+
);
|
|
1567
|
+
return Number(result) === 1;
|
|
1568
|
+
},
|
|
1569
|
+
async recoverStale(now, recoverOptions = {}) {
|
|
1570
|
+
const result = await evalRedis(
|
|
1571
|
+
client,
|
|
1572
|
+
RECOVER_STALE_SCRIPT,
|
|
1573
|
+
[
|
|
1574
|
+
keys.running,
|
|
1575
|
+
keys.available
|
|
1576
|
+
],
|
|
1577
|
+
[
|
|
1578
|
+
keys.jobPrefix,
|
|
1579
|
+
String(now),
|
|
1580
|
+
String(
|
|
1581
|
+
normalizePositiveInteger3(
|
|
1582
|
+
recoverOptions.limit ?? 100,
|
|
1583
|
+
"recovery limit"
|
|
1584
|
+
)
|
|
1585
|
+
)
|
|
1586
|
+
]
|
|
1587
|
+
);
|
|
1588
|
+
return Number(result) || 0;
|
|
1589
|
+
},
|
|
1590
|
+
async listDeadLetters() {
|
|
1591
|
+
const ids = await readStringList(
|
|
1592
|
+
client,
|
|
1593
|
+
[
|
|
1594
|
+
"ZRANGE",
|
|
1595
|
+
keys.deadLetters,
|
|
1596
|
+
"0",
|
|
1597
|
+
"-1"
|
|
1598
|
+
]
|
|
1599
|
+
);
|
|
1600
|
+
const jobs = await readJobsByIds(
|
|
1601
|
+
client,
|
|
1602
|
+
keys.jobPrefix,
|
|
1603
|
+
ids
|
|
1604
|
+
);
|
|
1605
|
+
return jobs.filter(
|
|
1606
|
+
(job) => job.state === "failed" && job.completedAt !== void 0
|
|
1607
|
+
).map(
|
|
1608
|
+
(job) => ({
|
|
1609
|
+
...job,
|
|
1610
|
+
state: "failed",
|
|
1611
|
+
deadLetteredAt: job.completedAt
|
|
1612
|
+
})
|
|
1613
|
+
);
|
|
1614
|
+
},
|
|
1615
|
+
async requeueDeadLetter(id, now, requeueOptions = {}) {
|
|
1616
|
+
const result = await evalRedis(
|
|
1617
|
+
client,
|
|
1618
|
+
REQUEUE_DEAD_LETTER_SCRIPT,
|
|
1619
|
+
[
|
|
1620
|
+
jobKey(
|
|
1621
|
+
keys.jobPrefix,
|
|
1622
|
+
id
|
|
1623
|
+
),
|
|
1624
|
+
keys.available,
|
|
1625
|
+
keys.running,
|
|
1626
|
+
keys.terminal,
|
|
1627
|
+
keys.deadLetters
|
|
1628
|
+
],
|
|
1629
|
+
[
|
|
1630
|
+
id,
|
|
1631
|
+
String(now),
|
|
1632
|
+
String(
|
|
1633
|
+
normalizeNonNegativeNumber2(
|
|
1634
|
+
requeueOptions.delayMs ?? 0,
|
|
1635
|
+
"delayMs"
|
|
1636
|
+
)
|
|
1637
|
+
),
|
|
1638
|
+
requeueOptions.resetAttempts === false ? "0" : "1"
|
|
1639
|
+
]
|
|
1640
|
+
);
|
|
1641
|
+
return Number(result) === 1;
|
|
1642
|
+
},
|
|
1643
|
+
async cleanup(cleanupOptions) {
|
|
1644
|
+
const states = cleanupOptions.states ?? [
|
|
1645
|
+
"succeeded",
|
|
1646
|
+
"failed",
|
|
1647
|
+
"cancelled"
|
|
1648
|
+
];
|
|
1649
|
+
const result = await evalRedis(
|
|
1650
|
+
client,
|
|
1651
|
+
CLEANUP_SCRIPT,
|
|
1652
|
+
[
|
|
1653
|
+
keys.terminal,
|
|
1654
|
+
keys.all,
|
|
1655
|
+
keys.available,
|
|
1656
|
+
keys.running,
|
|
1657
|
+
keys.deadLetters
|
|
1658
|
+
],
|
|
1659
|
+
[
|
|
1660
|
+
keys.jobPrefix,
|
|
1661
|
+
String(
|
|
1662
|
+
cleanupOptions.before
|
|
1663
|
+
),
|
|
1664
|
+
JSON.stringify(states)
|
|
1665
|
+
]
|
|
1666
|
+
);
|
|
1667
|
+
return Number(result) || 0;
|
|
1668
|
+
},
|
|
1669
|
+
async stats() {
|
|
1670
|
+
const jobs = await readJobsByIds(
|
|
1671
|
+
client,
|
|
1672
|
+
keys.jobPrefix,
|
|
1673
|
+
await readStringList(
|
|
1674
|
+
client,
|
|
1675
|
+
[
|
|
1676
|
+
"SMEMBERS",
|
|
1677
|
+
keys.all
|
|
1678
|
+
]
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
const deadLetters = Number(
|
|
1682
|
+
await client.sendCommand([
|
|
1683
|
+
"ZCARD",
|
|
1684
|
+
keys.deadLetters
|
|
1685
|
+
])
|
|
1686
|
+
) || 0;
|
|
1687
|
+
return calculateStats(
|
|
1688
|
+
jobs,
|
|
1689
|
+
deadLetters
|
|
1690
|
+
);
|
|
1691
|
+
},
|
|
1692
|
+
async close() {
|
|
1693
|
+
if (options.close) {
|
|
1694
|
+
await options.close();
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function createRedisJobScheduleStore(options) {
|
|
1700
|
+
const client = requireRedisClient(
|
|
1701
|
+
options.client
|
|
1702
|
+
);
|
|
1703
|
+
const namespace = normalizeNamespace(
|
|
1704
|
+
options.namespace
|
|
1705
|
+
);
|
|
1706
|
+
const keys = createScheduleKeys(
|
|
1707
|
+
namespace
|
|
1708
|
+
);
|
|
1709
|
+
return {
|
|
1710
|
+
namespace,
|
|
1711
|
+
async upsert(schedule) {
|
|
1712
|
+
await evalRedis(
|
|
1713
|
+
client,
|
|
1714
|
+
UPSERT_SCHEDULE_SCRIPT,
|
|
1715
|
+
[
|
|
1716
|
+
scheduleKey(
|
|
1717
|
+
keys.schedulePrefix,
|
|
1718
|
+
schedule.id
|
|
1719
|
+
),
|
|
1720
|
+
keys.all,
|
|
1721
|
+
keys.due
|
|
1722
|
+
],
|
|
1723
|
+
[
|
|
1724
|
+
schedule.id,
|
|
1725
|
+
JSON.stringify(schedule),
|
|
1726
|
+
String(
|
|
1727
|
+
schedule.nextRunAt
|
|
1728
|
+
)
|
|
1729
|
+
]
|
|
1730
|
+
);
|
|
1731
|
+
},
|
|
1732
|
+
async get(id) {
|
|
1733
|
+
return parseJsonReply(
|
|
1734
|
+
await client.sendCommand([
|
|
1735
|
+
"GET",
|
|
1736
|
+
scheduleKey(
|
|
1737
|
+
keys.schedulePrefix,
|
|
1738
|
+
id
|
|
1739
|
+
)
|
|
1740
|
+
])
|
|
1741
|
+
);
|
|
1742
|
+
},
|
|
1743
|
+
async list() {
|
|
1744
|
+
const ids = await readStringList(
|
|
1745
|
+
client,
|
|
1746
|
+
[
|
|
1747
|
+
"SMEMBERS",
|
|
1748
|
+
keys.all
|
|
1749
|
+
]
|
|
1750
|
+
);
|
|
1751
|
+
const schedules = await readSchedulesByIds(
|
|
1752
|
+
client,
|
|
1753
|
+
keys.schedulePrefix,
|
|
1754
|
+
ids
|
|
1755
|
+
);
|
|
1756
|
+
return schedules.sort(
|
|
1757
|
+
(left, right) => left.nextRunAt - right.nextRunAt || left.id.localeCompare(
|
|
1758
|
+
right.id
|
|
1759
|
+
)
|
|
1760
|
+
);
|
|
1761
|
+
},
|
|
1762
|
+
async remove(id) {
|
|
1763
|
+
const result = await evalRedis(
|
|
1764
|
+
client,
|
|
1765
|
+
REMOVE_SCHEDULE_SCRIPT,
|
|
1766
|
+
[
|
|
1767
|
+
scheduleKey(
|
|
1768
|
+
keys.schedulePrefix,
|
|
1769
|
+
id
|
|
1770
|
+
),
|
|
1771
|
+
keys.all,
|
|
1772
|
+
keys.due
|
|
1773
|
+
],
|
|
1774
|
+
[
|
|
1775
|
+
id
|
|
1776
|
+
]
|
|
1777
|
+
);
|
|
1778
|
+
return Number(result) === 1;
|
|
1779
|
+
},
|
|
1780
|
+
async acquireDue(now, acquireOptions) {
|
|
1781
|
+
const raw = await evalRedis(
|
|
1782
|
+
client,
|
|
1783
|
+
ACQUIRE_DUE_SCHEDULES_SCRIPT,
|
|
1784
|
+
[
|
|
1785
|
+
keys.due
|
|
1786
|
+
],
|
|
1787
|
+
[
|
|
1788
|
+
keys.schedulePrefix,
|
|
1789
|
+
String(now),
|
|
1790
|
+
acquireOptions.ownerId,
|
|
1791
|
+
String(
|
|
1792
|
+
normalizePositiveInteger3(
|
|
1793
|
+
acquireOptions.leaseMs,
|
|
1794
|
+
"schedule leaseMs"
|
|
1795
|
+
)
|
|
1796
|
+
),
|
|
1797
|
+
String(
|
|
1798
|
+
normalizePositiveInteger3(
|
|
1799
|
+
acquireOptions.limit,
|
|
1800
|
+
"schedule limit"
|
|
1801
|
+
)
|
|
1802
|
+
)
|
|
1803
|
+
]
|
|
1804
|
+
);
|
|
1805
|
+
if (!Array.isArray(raw)) {
|
|
1806
|
+
return [];
|
|
1807
|
+
}
|
|
1808
|
+
return raw.map(
|
|
1809
|
+
(value) => parseJsonReply(value)
|
|
1810
|
+
).filter(
|
|
1811
|
+
(value) => value !== null
|
|
1812
|
+
);
|
|
1813
|
+
},
|
|
1814
|
+
async complete(id, completeOptions) {
|
|
1815
|
+
await evalRedis(
|
|
1816
|
+
client,
|
|
1817
|
+
COMPLETE_SCHEDULE_SCRIPT,
|
|
1818
|
+
[
|
|
1819
|
+
scheduleKey(
|
|
1820
|
+
keys.schedulePrefix,
|
|
1821
|
+
id
|
|
1822
|
+
),
|
|
1823
|
+
keys.due
|
|
1824
|
+
],
|
|
1825
|
+
[
|
|
1826
|
+
id,
|
|
1827
|
+
completeOptions.ownerId,
|
|
1828
|
+
String(
|
|
1829
|
+
completeOptions.lastRunAt
|
|
1830
|
+
),
|
|
1831
|
+
String(
|
|
1832
|
+
completeOptions.nextRunAt
|
|
1833
|
+
),
|
|
1834
|
+
String(
|
|
1835
|
+
completeOptions.updatedAt
|
|
1836
|
+
)
|
|
1837
|
+
]
|
|
1838
|
+
);
|
|
1839
|
+
},
|
|
1840
|
+
async release(id, ownerId) {
|
|
1841
|
+
await evalRedis(
|
|
1842
|
+
client,
|
|
1843
|
+
RELEASE_SCHEDULE_SCRIPT,
|
|
1844
|
+
[
|
|
1845
|
+
scheduleKey(
|
|
1846
|
+
keys.schedulePrefix,
|
|
1847
|
+
id
|
|
1848
|
+
)
|
|
1849
|
+
],
|
|
1850
|
+
[
|
|
1851
|
+
ownerId
|
|
1852
|
+
]
|
|
1853
|
+
);
|
|
1854
|
+
},
|
|
1855
|
+
async close() {
|
|
1856
|
+
if (options.close) {
|
|
1857
|
+
await options.close();
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function createQueueKeys(namespace) {
|
|
1863
|
+
return {
|
|
1864
|
+
all: `${namespace}:jobs:all`,
|
|
1865
|
+
available: `${namespace}:jobs:available`,
|
|
1866
|
+
running: `${namespace}:jobs:running`,
|
|
1867
|
+
terminal: `${namespace}:jobs:terminal`,
|
|
1868
|
+
deadLetters: `${namespace}:jobs:dlq`,
|
|
1869
|
+
jobPrefix: `${namespace}:job:`
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function createScheduleKeys(namespace) {
|
|
1873
|
+
return {
|
|
1874
|
+
all: `${namespace}:schedules:all`,
|
|
1875
|
+
due: `${namespace}:schedules:due`,
|
|
1876
|
+
schedulePrefix: `${namespace}:schedule:`
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function jobKey(prefix, id) {
|
|
1880
|
+
return `${prefix}${id}`;
|
|
1881
|
+
}
|
|
1882
|
+
function scheduleKey(prefix, id) {
|
|
1883
|
+
return `${prefix}${id}`;
|
|
1884
|
+
}
|
|
1885
|
+
async function evalRedis(client, script, keys, args) {
|
|
1886
|
+
return client.sendCommand([
|
|
1887
|
+
"EVAL",
|
|
1888
|
+
script,
|
|
1889
|
+
String(keys.length),
|
|
1890
|
+
...keys,
|
|
1891
|
+
...args
|
|
1892
|
+
]);
|
|
1893
|
+
}
|
|
1894
|
+
async function readStringList(client, command) {
|
|
1895
|
+
const result = await client.sendCommand(
|
|
1896
|
+
command
|
|
1897
|
+
);
|
|
1898
|
+
if (!Array.isArray(result)) {
|
|
1899
|
+
return [];
|
|
1900
|
+
}
|
|
1901
|
+
return result.map(
|
|
1902
|
+
(value) => redisString(value)
|
|
1903
|
+
).filter(
|
|
1904
|
+
(value) => value !== null
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
async function readJobsByIds(client, prefix, ids) {
|
|
1908
|
+
if (ids.length === 0) {
|
|
1909
|
+
return [];
|
|
1910
|
+
}
|
|
1911
|
+
const raw = await client.sendCommand([
|
|
1912
|
+
"MGET",
|
|
1913
|
+
...ids.map(
|
|
1914
|
+
(id) => jobKey(
|
|
1915
|
+
prefix,
|
|
1916
|
+
id
|
|
1917
|
+
)
|
|
1918
|
+
)
|
|
1919
|
+
]);
|
|
1920
|
+
if (!Array.isArray(raw)) {
|
|
1921
|
+
return [];
|
|
1922
|
+
}
|
|
1923
|
+
return raw.map(
|
|
1924
|
+
(value) => parseJsonReply(value)
|
|
1925
|
+
).filter(
|
|
1926
|
+
(value) => value !== null
|
|
1927
|
+
).sort(
|
|
1928
|
+
(left, right) => left.createdAt - right.createdAt || left.id.localeCompare(
|
|
1929
|
+
right.id
|
|
1930
|
+
)
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
async function readSchedulesByIds(client, prefix, ids) {
|
|
1934
|
+
if (ids.length === 0) {
|
|
1935
|
+
return [];
|
|
1936
|
+
}
|
|
1937
|
+
const raw = await client.sendCommand([
|
|
1938
|
+
"MGET",
|
|
1939
|
+
...ids.map(
|
|
1940
|
+
(id) => scheduleKey(
|
|
1941
|
+
prefix,
|
|
1942
|
+
id
|
|
1943
|
+
)
|
|
1944
|
+
)
|
|
1945
|
+
]);
|
|
1946
|
+
if (!Array.isArray(raw)) {
|
|
1947
|
+
return [];
|
|
1948
|
+
}
|
|
1949
|
+
return raw.map(
|
|
1950
|
+
(value) => parseJsonReply(value)
|
|
1951
|
+
).filter(
|
|
1952
|
+
(value) => value !== null
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
function parseJsonReply(value) {
|
|
1956
|
+
const text = redisString(value);
|
|
1957
|
+
if (text === null) {
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
return JSON.parse(text);
|
|
1961
|
+
}
|
|
1962
|
+
function redisString(value) {
|
|
1963
|
+
if (typeof value === "string") {
|
|
1964
|
+
return value;
|
|
1965
|
+
}
|
|
1966
|
+
if (value instanceof Uint8Array) {
|
|
1967
|
+
return Buffer.from(
|
|
1968
|
+
value
|
|
1969
|
+
).toString("utf8");
|
|
1970
|
+
}
|
|
1971
|
+
if (typeof value === "number") {
|
|
1972
|
+
return String(value);
|
|
1973
|
+
}
|
|
1974
|
+
return null;
|
|
1975
|
+
}
|
|
1976
|
+
function calculateStats(jobs, deadLetters) {
|
|
1977
|
+
const stats = {
|
|
1978
|
+
total: jobs.length,
|
|
1979
|
+
queued: 0,
|
|
1980
|
+
running: 0,
|
|
1981
|
+
succeeded: 0,
|
|
1982
|
+
failed: 0,
|
|
1983
|
+
cancelled: 0,
|
|
1984
|
+
deadLetters
|
|
1985
|
+
};
|
|
1986
|
+
for (const job of jobs) {
|
|
1987
|
+
stats[job.state] += 1;
|
|
1988
|
+
}
|
|
1989
|
+
return stats;
|
|
1990
|
+
}
|
|
1991
|
+
function requireRedisClient(client) {
|
|
1992
|
+
if (!client || typeof client.sendCommand !== "function") {
|
|
1993
|
+
throw new TypeError(
|
|
1994
|
+
"BCP Jobs Redis: client.sendCommand(command) is required."
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
return client;
|
|
1998
|
+
}
|
|
1999
|
+
function normalizeNamespace(value = "bcp:{jobs}") {
|
|
2000
|
+
const namespace = String(value).trim();
|
|
2001
|
+
if (!namespace) {
|
|
2002
|
+
throw new TypeError(
|
|
2003
|
+
"BCP Jobs Redis: namespace must be non-empty."
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
return namespace.replace(
|
|
2007
|
+
/:+$/,
|
|
2008
|
+
""
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
function normalizePositiveInteger3(value, field) {
|
|
2012
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2013
|
+
throw new TypeError(
|
|
2014
|
+
`BCP Jobs Redis: ${field} must be a positive integer.`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
return value;
|
|
2018
|
+
}
|
|
2019
|
+
function normalizeNonNegativeNumber2(value, field) {
|
|
2020
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2021
|
+
throw new TypeError(
|
|
2022
|
+
`BCP Jobs Redis: ${field} must be a non-negative finite number.`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
return Math.floor(value);
|
|
2026
|
+
}
|
|
2027
|
+
var ENQUEUE_SCRIPT = `
|
|
2028
|
+
if redis.call('EXISTS', KEYS[1]) == 1 then
|
|
2029
|
+
return 0
|
|
2030
|
+
end
|
|
2031
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
2032
|
+
redis.call('SADD', KEYS[2], ARGV[1])
|
|
2033
|
+
redis.call('ZADD', KEYS[3], ARGV[3], ARGV[1])
|
|
2034
|
+
return 1
|
|
2035
|
+
`;
|
|
2036
|
+
var RESERVE_SCRIPT = `
|
|
2037
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, 20)
|
|
2038
|
+
for _, id in ipairs(ids) do
|
|
2039
|
+
local key = ARGV[1] .. id
|
|
2040
|
+
local raw = redis.call('GET', key)
|
|
2041
|
+
if not raw then
|
|
2042
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2043
|
+
else
|
|
2044
|
+
local job = cjson.decode(raw)
|
|
2045
|
+
if job.state == 'queued' then
|
|
2046
|
+
job.state = 'running'
|
|
2047
|
+
job.attempts = (job.attempts or 0) + 1
|
|
2048
|
+
job.startedAt = tonumber(ARGV[2])
|
|
2049
|
+
job.error = nil
|
|
2050
|
+
job.recoveredAt = nil
|
|
2051
|
+
job.leaseOwner = ARGV[3]
|
|
2052
|
+
job.leaseUntil = tonumber(ARGV[2]) + tonumber(ARGV[4])
|
|
2053
|
+
job.heartbeatAt = tonumber(ARGV[2])
|
|
2054
|
+
local encoded = cjson.encode(job)
|
|
2055
|
+
redis.call('SET', key, encoded)
|
|
2056
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2057
|
+
redis.call('ZADD', KEYS[2], job.leaseUntil, id)
|
|
2058
|
+
return encoded
|
|
2059
|
+
end
|
|
2060
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2061
|
+
end
|
|
2062
|
+
end
|
|
2063
|
+
return nil
|
|
2064
|
+
`;
|
|
2065
|
+
var COMPLETE_SCRIPT = `
|
|
2066
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2067
|
+
if not raw then return 0 end
|
|
2068
|
+
local job = cjson.decode(raw)
|
|
2069
|
+
if job.state == 'cancelled' then return 0 end
|
|
2070
|
+
if ARGV[3] ~= '' and job.leaseOwner ~= ARGV[3] then return 0 end
|
|
2071
|
+
job.state = 'succeeded'
|
|
2072
|
+
job.completedAt = tonumber(ARGV[2])
|
|
2073
|
+
job.error = nil
|
|
2074
|
+
job.leaseOwner = nil
|
|
2075
|
+
job.leaseUntil = nil
|
|
2076
|
+
job.heartbeatAt = nil
|
|
2077
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2078
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
2079
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2080
|
+
redis.call('ZADD', KEYS[4], ARGV[2], ARGV[1])
|
|
2081
|
+
redis.call('ZREM', KEYS[5], ARGV[1])
|
|
2082
|
+
return 1
|
|
2083
|
+
`;
|
|
2084
|
+
var FAIL_SCRIPT = `
|
|
2085
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2086
|
+
if not raw then return 0 end
|
|
2087
|
+
local job = cjson.decode(raw)
|
|
2088
|
+
if job.state == 'cancelled' then return 0 end
|
|
2089
|
+
if ARGV[5] ~= '' and job.leaseOwner ~= ARGV[5] then return 0 end
|
|
2090
|
+
job.error = ARGV[2]
|
|
2091
|
+
job.leaseOwner = nil
|
|
2092
|
+
job.leaseUntil = nil
|
|
2093
|
+
job.heartbeatAt = nil
|
|
2094
|
+
local retryAt = nil
|
|
2095
|
+
if ARGV[4] ~= '' then retryAt = tonumber(ARGV[4]) end
|
|
2096
|
+
if retryAt and (job.attempts or 0) < (job.maxAttempts or 1) then
|
|
2097
|
+
job.state = 'queued'
|
|
2098
|
+
job.availableAt = retryAt
|
|
2099
|
+
job.startedAt = nil
|
|
2100
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2101
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2102
|
+
redis.call('ZADD', KEYS[2], retryAt, ARGV[1])
|
|
2103
|
+
return 2
|
|
2104
|
+
end
|
|
2105
|
+
job.state = 'failed'
|
|
2106
|
+
job.completedAt = tonumber(ARGV[3])
|
|
2107
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2108
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
2109
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2110
|
+
redis.call('ZADD', KEYS[4], ARGV[3], ARGV[1])
|
|
2111
|
+
redis.call('ZADD', KEYS[5], ARGV[3], ARGV[1])
|
|
2112
|
+
return 1
|
|
2113
|
+
`;
|
|
2114
|
+
var CANCEL_SCRIPT = `
|
|
2115
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2116
|
+
if not raw then return 0 end
|
|
2117
|
+
local job = cjson.decode(raw)
|
|
2118
|
+
if job.state == 'succeeded' or job.state == 'failed' or job.state == 'cancelled' then
|
|
2119
|
+
return 0
|
|
2120
|
+
end
|
|
2121
|
+
job.state = 'cancelled'
|
|
2122
|
+
job.completedAt = tonumber(ARGV[2])
|
|
2123
|
+
job.leaseOwner = nil
|
|
2124
|
+
job.leaseUntil = nil
|
|
2125
|
+
job.heartbeatAt = nil
|
|
2126
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2127
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
2128
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2129
|
+
redis.call('ZADD', KEYS[4], ARGV[2], ARGV[1])
|
|
2130
|
+
return 1
|
|
2131
|
+
`;
|
|
2132
|
+
var HEARTBEAT_SCRIPT = `
|
|
2133
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2134
|
+
if not raw then return 0 end
|
|
2135
|
+
local job = cjson.decode(raw)
|
|
2136
|
+
if job.state ~= 'running' or job.leaseOwner ~= ARGV[2] then
|
|
2137
|
+
return 0
|
|
2138
|
+
end
|
|
2139
|
+
job.heartbeatAt = tonumber(ARGV[3])
|
|
2140
|
+
job.leaseUntil = tonumber(ARGV[3]) + tonumber(ARGV[4])
|
|
2141
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2142
|
+
redis.call('ZADD', KEYS[2], job.leaseUntil, ARGV[1])
|
|
2143
|
+
return 1
|
|
2144
|
+
`;
|
|
2145
|
+
var RECOVER_STALE_SCRIPT = `
|
|
2146
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, tonumber(ARGV[3]))
|
|
2147
|
+
local recovered = 0
|
|
2148
|
+
for _, id in ipairs(ids) do
|
|
2149
|
+
local key = ARGV[1] .. id
|
|
2150
|
+
local raw = redis.call('GET', key)
|
|
2151
|
+
if not raw then
|
|
2152
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2153
|
+
else
|
|
2154
|
+
local job = cjson.decode(raw)
|
|
2155
|
+
if job.state == 'running' and job.leaseUntil and tonumber(job.leaseUntil) <= tonumber(ARGV[2]) then
|
|
2156
|
+
job.state = 'queued'
|
|
2157
|
+
job.availableAt = tonumber(ARGV[2])
|
|
2158
|
+
job.startedAt = nil
|
|
2159
|
+
job.leaseOwner = nil
|
|
2160
|
+
job.leaseUntil = nil
|
|
2161
|
+
job.heartbeatAt = nil
|
|
2162
|
+
job.recoveredAt = tonumber(ARGV[2])
|
|
2163
|
+
redis.call('SET', key, cjson.encode(job))
|
|
2164
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2165
|
+
redis.call('ZADD', KEYS[2], ARGV[2], id)
|
|
2166
|
+
recovered = recovered + 1
|
|
2167
|
+
end
|
|
2168
|
+
end
|
|
2169
|
+
end
|
|
2170
|
+
return recovered
|
|
2171
|
+
`;
|
|
2172
|
+
var REQUEUE_DEAD_LETTER_SCRIPT = `
|
|
2173
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2174
|
+
if not raw then return 0 end
|
|
2175
|
+
if not redis.call('ZSCORE', KEYS[5], ARGV[1]) then return 0 end
|
|
2176
|
+
local job = cjson.decode(raw)
|
|
2177
|
+
if job.state ~= 'failed' then return 0 end
|
|
2178
|
+
job.state = 'queued'
|
|
2179
|
+
job.availableAt = tonumber(ARGV[2]) + tonumber(ARGV[3])
|
|
2180
|
+
job.startedAt = nil
|
|
2181
|
+
job.completedAt = nil
|
|
2182
|
+
job.error = nil
|
|
2183
|
+
job.leaseOwner = nil
|
|
2184
|
+
job.leaseUntil = nil
|
|
2185
|
+
job.heartbeatAt = nil
|
|
2186
|
+
job.recoveredAt = nil
|
|
2187
|
+
if ARGV[4] == '1' then job.attempts = 0 end
|
|
2188
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
2189
|
+
redis.call('ZADD', KEYS[2], job.availableAt, ARGV[1])
|
|
2190
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2191
|
+
redis.call('ZREM', KEYS[4], ARGV[1])
|
|
2192
|
+
redis.call('ZREM', KEYS[5], ARGV[1])
|
|
2193
|
+
return 1
|
|
2194
|
+
`;
|
|
2195
|
+
var CLEANUP_SCRIPT = `
|
|
2196
|
+
local allowed = {}
|
|
2197
|
+
for _, state in ipairs(cjson.decode(ARGV[3])) do allowed[state] = true end
|
|
2198
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, 1000)
|
|
2199
|
+
local removed = 0
|
|
2200
|
+
for _, id in ipairs(ids) do
|
|
2201
|
+
local key = ARGV[1] .. id
|
|
2202
|
+
local raw = redis.call('GET', key)
|
|
2203
|
+
if not raw then
|
|
2204
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2205
|
+
redis.call('SREM', KEYS[2], id)
|
|
2206
|
+
else
|
|
2207
|
+
local job = cjson.decode(raw)
|
|
2208
|
+
if allowed[job.state] then
|
|
2209
|
+
redis.call('DEL', key)
|
|
2210
|
+
redis.call('SREM', KEYS[2], id)
|
|
2211
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2212
|
+
redis.call('ZREM', KEYS[3], id)
|
|
2213
|
+
redis.call('ZREM', KEYS[4], id)
|
|
2214
|
+
redis.call('ZREM', KEYS[5], id)
|
|
2215
|
+
removed = removed + 1
|
|
2216
|
+
end
|
|
2217
|
+
end
|
|
2218
|
+
end
|
|
2219
|
+
return removed
|
|
2220
|
+
`;
|
|
2221
|
+
var UPSERT_SCHEDULE_SCRIPT = `
|
|
2222
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
2223
|
+
redis.call('SADD', KEYS[2], ARGV[1])
|
|
2224
|
+
redis.call('ZADD', KEYS[3], ARGV[3], ARGV[1])
|
|
2225
|
+
return 1
|
|
2226
|
+
`;
|
|
2227
|
+
var REMOVE_SCHEDULE_SCRIPT = `
|
|
2228
|
+
local existed = redis.call('DEL', KEYS[1])
|
|
2229
|
+
redis.call('SREM', KEYS[2], ARGV[1])
|
|
2230
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
2231
|
+
return existed
|
|
2232
|
+
`;
|
|
2233
|
+
var ACQUIRE_DUE_SCHEDULES_SCRIPT = `
|
|
2234
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, tonumber(ARGV[5]) * 4)
|
|
2235
|
+
local result = {}
|
|
2236
|
+
for _, id in ipairs(ids) do
|
|
2237
|
+
if #result >= tonumber(ARGV[5]) then break end
|
|
2238
|
+
local key = ARGV[1] .. id
|
|
2239
|
+
local raw = redis.call('GET', key)
|
|
2240
|
+
if not raw then
|
|
2241
|
+
redis.call('ZREM', KEYS[1], id)
|
|
2242
|
+
else
|
|
2243
|
+
local schedule = cjson.decode(raw)
|
|
2244
|
+
local leaseUntil = schedule.leaseUntil
|
|
2245
|
+
if schedule.nextRunAt <= tonumber(ARGV[2]) and (not leaseUntil or tonumber(leaseUntil) <= tonumber(ARGV[2])) then
|
|
2246
|
+
schedule.leaseOwner = ARGV[3]
|
|
2247
|
+
schedule.leaseUntil = tonumber(ARGV[2]) + tonumber(ARGV[4])
|
|
2248
|
+
local encoded = cjson.encode(schedule)
|
|
2249
|
+
redis.call('SET', key, encoded)
|
|
2250
|
+
table.insert(result, encoded)
|
|
2251
|
+
end
|
|
2252
|
+
end
|
|
2253
|
+
end
|
|
2254
|
+
return result
|
|
2255
|
+
`;
|
|
2256
|
+
var COMPLETE_SCHEDULE_SCRIPT = `
|
|
2257
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2258
|
+
if not raw then return 0 end
|
|
2259
|
+
local schedule = cjson.decode(raw)
|
|
2260
|
+
if schedule.leaseOwner ~= ARGV[2] then return 0 end
|
|
2261
|
+
schedule.lastRunAt = tonumber(ARGV[3])
|
|
2262
|
+
schedule.nextRunAt = tonumber(ARGV[4])
|
|
2263
|
+
schedule.updatedAt = tonumber(ARGV[5])
|
|
2264
|
+
schedule.leaseOwner = nil
|
|
2265
|
+
schedule.leaseUntil = nil
|
|
2266
|
+
redis.call('SET', KEYS[1], cjson.encode(schedule))
|
|
2267
|
+
redis.call('ZADD', KEYS[2], ARGV[4], ARGV[1])
|
|
2268
|
+
return 1
|
|
2269
|
+
`;
|
|
2270
|
+
var RELEASE_SCHEDULE_SCRIPT = `
|
|
2271
|
+
local raw = redis.call('GET', KEYS[1])
|
|
2272
|
+
if not raw then return 0 end
|
|
2273
|
+
local schedule = cjson.decode(raw)
|
|
2274
|
+
if schedule.leaseOwner ~= ARGV[1] then return 0 end
|
|
2275
|
+
schedule.leaseOwner = nil
|
|
2276
|
+
schedule.leaseUntil = nil
|
|
2277
|
+
redis.call('SET', KEYS[1], cjson.encode(schedule))
|
|
2278
|
+
return 1
|
|
2279
|
+
`;
|
|
1033
2280
|
export {
|
|
1034
2281
|
createJobQueue,
|
|
1035
2282
|
createJobScheduler,
|
|
1036
2283
|
createMemoryJobQueueAdapter,
|
|
1037
2284
|
createMemoryJobScheduleStore,
|
|
2285
|
+
createRedisJobQueueAdapter,
|
|
2286
|
+
createRedisJobScheduleStore,
|
|
1038
2287
|
nextCronTime,
|
|
1039
2288
|
nextScheduleTime
|
|
1040
2289
|
};
|