@chidchanun/bcp 0.2.9 → 0.2.10
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 +122 -128
- package/docs/README.md +45 -41
- package/docs/api-manifest.json +3 -2
- package/docs/api-reference.md +69 -8
- package/docs/docs-web-manifest.json +5 -3
- package/docs/durable-jobs.md +359 -0
- package/docs/platform-manifest.json +15 -4
- package/docs/releases/0.2.10.md +148 -0
- package/package.json +1 -1
- package/packages/client/src/jobs.mjs +1310 -61
- package/packages/client/src/jobs.ts +17 -0
- package/packages/server/src/jobs-redis.ts +1226 -0
- package/packages/server/src/jobs.ts +768 -128
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CleanupJobsOptions,
|
|
3
|
+
HeartbeatJobOptions,
|
|
4
|
+
JobQueueAdapter,
|
|
5
|
+
JobQueueStats,
|
|
6
|
+
JobRecord,
|
|
7
|
+
RecoverStaleJobsOptions,
|
|
8
|
+
RequeueDeadLetterOptions,
|
|
9
|
+
} from "./jobs.js";
|
|
10
|
+
import type {
|
|
11
|
+
JobScheduleRecord,
|
|
12
|
+
JobScheduleStore,
|
|
13
|
+
} from "./job-scheduler.js";
|
|
14
|
+
|
|
15
|
+
export interface RedisCommandClient {
|
|
16
|
+
sendCommand(
|
|
17
|
+
command: string[]
|
|
18
|
+
): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RedisJobsAdapterOptions {
|
|
22
|
+
client: RedisCommandClient;
|
|
23
|
+
namespace?: string;
|
|
24
|
+
close?: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RedisJobQueueAdapter
|
|
28
|
+
extends JobQueueAdapter {
|
|
29
|
+
readonly namespace: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RedisJobScheduleStore
|
|
33
|
+
extends JobScheduleStore {
|
|
34
|
+
readonly namespace: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createRedisJobQueueAdapter(
|
|
38
|
+
options: RedisJobsAdapterOptions
|
|
39
|
+
): RedisJobQueueAdapter {
|
|
40
|
+
const client =
|
|
41
|
+
requireRedisClient(
|
|
42
|
+
options.client
|
|
43
|
+
);
|
|
44
|
+
const namespace =
|
|
45
|
+
normalizeNamespace(
|
|
46
|
+
options.namespace
|
|
47
|
+
);
|
|
48
|
+
const keys =
|
|
49
|
+
createQueueKeys(
|
|
50
|
+
namespace
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
namespace,
|
|
55
|
+
|
|
56
|
+
async enqueue(job) {
|
|
57
|
+
const result =
|
|
58
|
+
await evalRedis(
|
|
59
|
+
client,
|
|
60
|
+
ENQUEUE_SCRIPT,
|
|
61
|
+
[
|
|
62
|
+
jobKey(
|
|
63
|
+
keys.jobPrefix,
|
|
64
|
+
job.id
|
|
65
|
+
),
|
|
66
|
+
keys.all,
|
|
67
|
+
keys.available,
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
job.id,
|
|
71
|
+
JSON.stringify(job),
|
|
72
|
+
String(
|
|
73
|
+
job.availableAt
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (Number(result) !== 1) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`BCP Jobs Redis: job id "${job.id}" already exists.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async reserve(
|
|
86
|
+
now,
|
|
87
|
+
reserveOptions
|
|
88
|
+
) {
|
|
89
|
+
const options =
|
|
90
|
+
reserveOptions ?? {
|
|
91
|
+
ownerId:
|
|
92
|
+
"legacy-worker",
|
|
93
|
+
visibilityTimeoutMs:
|
|
94
|
+
30_000,
|
|
95
|
+
};
|
|
96
|
+
const raw =
|
|
97
|
+
await evalRedis(
|
|
98
|
+
client,
|
|
99
|
+
RESERVE_SCRIPT,
|
|
100
|
+
[
|
|
101
|
+
keys.available,
|
|
102
|
+
keys.running,
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
keys.jobPrefix,
|
|
106
|
+
String(now),
|
|
107
|
+
options.ownerId,
|
|
108
|
+
String(
|
|
109
|
+
normalizePositiveInteger(
|
|
110
|
+
options.visibilityTimeoutMs,
|
|
111
|
+
"visibilityTimeoutMs"
|
|
112
|
+
)
|
|
113
|
+
),
|
|
114
|
+
]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return parseJsonReply<
|
|
118
|
+
JobRecord
|
|
119
|
+
>(raw);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async complete(
|
|
123
|
+
id,
|
|
124
|
+
completedAt,
|
|
125
|
+
ownerId
|
|
126
|
+
) {
|
|
127
|
+
await evalRedis(
|
|
128
|
+
client,
|
|
129
|
+
COMPLETE_SCRIPT,
|
|
130
|
+
[
|
|
131
|
+
jobKey(
|
|
132
|
+
keys.jobPrefix,
|
|
133
|
+
id
|
|
134
|
+
),
|
|
135
|
+
keys.available,
|
|
136
|
+
keys.running,
|
|
137
|
+
keys.terminal,
|
|
138
|
+
keys.deadLetters,
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
id,
|
|
142
|
+
String(completedAt),
|
|
143
|
+
ownerId ?? "",
|
|
144
|
+
]
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async fail(
|
|
149
|
+
id,
|
|
150
|
+
failOptions
|
|
151
|
+
) {
|
|
152
|
+
await evalRedis(
|
|
153
|
+
client,
|
|
154
|
+
FAIL_SCRIPT,
|
|
155
|
+
[
|
|
156
|
+
jobKey(
|
|
157
|
+
keys.jobPrefix,
|
|
158
|
+
id
|
|
159
|
+
),
|
|
160
|
+
keys.available,
|
|
161
|
+
keys.running,
|
|
162
|
+
keys.terminal,
|
|
163
|
+
keys.deadLetters,
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
id,
|
|
167
|
+
failOptions.error,
|
|
168
|
+
String(
|
|
169
|
+
failOptions.failedAt
|
|
170
|
+
),
|
|
171
|
+
failOptions.retryAt ===
|
|
172
|
+
undefined
|
|
173
|
+
? ""
|
|
174
|
+
: String(
|
|
175
|
+
failOptions.retryAt
|
|
176
|
+
),
|
|
177
|
+
failOptions.ownerId ??
|
|
178
|
+
"",
|
|
179
|
+
]
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async cancel(
|
|
184
|
+
id,
|
|
185
|
+
cancelledAt
|
|
186
|
+
) {
|
|
187
|
+
const result =
|
|
188
|
+
await evalRedis(
|
|
189
|
+
client,
|
|
190
|
+
CANCEL_SCRIPT,
|
|
191
|
+
[
|
|
192
|
+
jobKey(
|
|
193
|
+
keys.jobPrefix,
|
|
194
|
+
id
|
|
195
|
+
),
|
|
196
|
+
keys.available,
|
|
197
|
+
keys.running,
|
|
198
|
+
keys.terminal,
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
id,
|
|
202
|
+
String(cancelledAt),
|
|
203
|
+
]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return Number(result) === 1;
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async get(id) {
|
|
210
|
+
const raw =
|
|
211
|
+
await client.sendCommand([
|
|
212
|
+
"GET",
|
|
213
|
+
jobKey(
|
|
214
|
+
keys.jobPrefix,
|
|
215
|
+
id
|
|
216
|
+
),
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
return parseJsonReply<
|
|
220
|
+
JobRecord<any>
|
|
221
|
+
>(raw);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async list() {
|
|
225
|
+
return readJobsByIds(
|
|
226
|
+
client,
|
|
227
|
+
keys.jobPrefix,
|
|
228
|
+
await readStringList(
|
|
229
|
+
client,
|
|
230
|
+
[
|
|
231
|
+
"SMEMBERS",
|
|
232
|
+
keys.all,
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async heartbeat(
|
|
239
|
+
id,
|
|
240
|
+
heartbeatOptions
|
|
241
|
+
) {
|
|
242
|
+
const result =
|
|
243
|
+
await evalRedis(
|
|
244
|
+
client,
|
|
245
|
+
HEARTBEAT_SCRIPT,
|
|
246
|
+
[
|
|
247
|
+
jobKey(
|
|
248
|
+
keys.jobPrefix,
|
|
249
|
+
id
|
|
250
|
+
),
|
|
251
|
+
keys.running,
|
|
252
|
+
],
|
|
253
|
+
[
|
|
254
|
+
id,
|
|
255
|
+
heartbeatOptions.ownerId,
|
|
256
|
+
String(
|
|
257
|
+
heartbeatOptions
|
|
258
|
+
.heartbeatAt
|
|
259
|
+
),
|
|
260
|
+
String(
|
|
261
|
+
normalizePositiveInteger(
|
|
262
|
+
heartbeatOptions
|
|
263
|
+
.visibilityTimeoutMs,
|
|
264
|
+
"visibilityTimeoutMs"
|
|
265
|
+
)
|
|
266
|
+
),
|
|
267
|
+
]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return Number(result) === 1;
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async recoverStale(
|
|
274
|
+
now,
|
|
275
|
+
recoverOptions = {}
|
|
276
|
+
) {
|
|
277
|
+
const result =
|
|
278
|
+
await evalRedis(
|
|
279
|
+
client,
|
|
280
|
+
RECOVER_STALE_SCRIPT,
|
|
281
|
+
[
|
|
282
|
+
keys.running,
|
|
283
|
+
keys.available,
|
|
284
|
+
],
|
|
285
|
+
[
|
|
286
|
+
keys.jobPrefix,
|
|
287
|
+
String(now),
|
|
288
|
+
String(
|
|
289
|
+
normalizePositiveInteger(
|
|
290
|
+
recoverOptions.limit ??
|
|
291
|
+
100,
|
|
292
|
+
"recovery limit"
|
|
293
|
+
)
|
|
294
|
+
),
|
|
295
|
+
]
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return Number(result) || 0;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async listDeadLetters() {
|
|
302
|
+
const ids =
|
|
303
|
+
await readStringList(
|
|
304
|
+
client,
|
|
305
|
+
[
|
|
306
|
+
"ZRANGE",
|
|
307
|
+
keys.deadLetters,
|
|
308
|
+
"0",
|
|
309
|
+
"-1",
|
|
310
|
+
]
|
|
311
|
+
);
|
|
312
|
+
const jobs =
|
|
313
|
+
await readJobsByIds(
|
|
314
|
+
client,
|
|
315
|
+
keys.jobPrefix,
|
|
316
|
+
ids
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return jobs
|
|
320
|
+
.filter(
|
|
321
|
+
(
|
|
322
|
+
job
|
|
323
|
+
): job is JobRecord & {
|
|
324
|
+
state: "failed";
|
|
325
|
+
completedAt: number;
|
|
326
|
+
} =>
|
|
327
|
+
job.state ===
|
|
328
|
+
"failed" &&
|
|
329
|
+
job.completedAt !==
|
|
330
|
+
undefined
|
|
331
|
+
)
|
|
332
|
+
.map(
|
|
333
|
+
job => ({
|
|
334
|
+
...job,
|
|
335
|
+
state:
|
|
336
|
+
"failed" as const,
|
|
337
|
+
deadLetteredAt:
|
|
338
|
+
job.completedAt,
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
async requeueDeadLetter(
|
|
344
|
+
id,
|
|
345
|
+
now,
|
|
346
|
+
requeueOptions = {}
|
|
347
|
+
) {
|
|
348
|
+
const result =
|
|
349
|
+
await evalRedis(
|
|
350
|
+
client,
|
|
351
|
+
REQUEUE_DEAD_LETTER_SCRIPT,
|
|
352
|
+
[
|
|
353
|
+
jobKey(
|
|
354
|
+
keys.jobPrefix,
|
|
355
|
+
id
|
|
356
|
+
),
|
|
357
|
+
keys.available,
|
|
358
|
+
keys.running,
|
|
359
|
+
keys.terminal,
|
|
360
|
+
keys.deadLetters,
|
|
361
|
+
],
|
|
362
|
+
[
|
|
363
|
+
id,
|
|
364
|
+
String(now),
|
|
365
|
+
String(
|
|
366
|
+
normalizeNonNegativeNumber(
|
|
367
|
+
requeueOptions.delayMs ??
|
|
368
|
+
0,
|
|
369
|
+
"delayMs"
|
|
370
|
+
)
|
|
371
|
+
),
|
|
372
|
+
requeueOptions
|
|
373
|
+
.resetAttempts ===
|
|
374
|
+
false
|
|
375
|
+
? "0"
|
|
376
|
+
: "1",
|
|
377
|
+
]
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return Number(result) === 1;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
async cleanup(cleanupOptions) {
|
|
384
|
+
const states =
|
|
385
|
+
cleanupOptions.states ?? [
|
|
386
|
+
"succeeded",
|
|
387
|
+
"failed",
|
|
388
|
+
"cancelled",
|
|
389
|
+
];
|
|
390
|
+
const result =
|
|
391
|
+
await evalRedis(
|
|
392
|
+
client,
|
|
393
|
+
CLEANUP_SCRIPT,
|
|
394
|
+
[
|
|
395
|
+
keys.terminal,
|
|
396
|
+
keys.all,
|
|
397
|
+
keys.available,
|
|
398
|
+
keys.running,
|
|
399
|
+
keys.deadLetters,
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
keys.jobPrefix,
|
|
403
|
+
String(
|
|
404
|
+
cleanupOptions.before
|
|
405
|
+
),
|
|
406
|
+
JSON.stringify(states),
|
|
407
|
+
]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return Number(result) || 0;
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
async stats() {
|
|
414
|
+
const jobs =
|
|
415
|
+
await readJobsByIds(
|
|
416
|
+
client,
|
|
417
|
+
keys.jobPrefix,
|
|
418
|
+
await readStringList(
|
|
419
|
+
client,
|
|
420
|
+
[
|
|
421
|
+
"SMEMBERS",
|
|
422
|
+
keys.all,
|
|
423
|
+
]
|
|
424
|
+
)
|
|
425
|
+
);
|
|
426
|
+
const deadLetters =
|
|
427
|
+
Number(
|
|
428
|
+
await client
|
|
429
|
+
.sendCommand([
|
|
430
|
+
"ZCARD",
|
|
431
|
+
keys.deadLetters,
|
|
432
|
+
])
|
|
433
|
+
) || 0;
|
|
434
|
+
|
|
435
|
+
return calculateStats(
|
|
436
|
+
jobs,
|
|
437
|
+
deadLetters
|
|
438
|
+
);
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
async close() {
|
|
442
|
+
if (options.close) {
|
|
443
|
+
await options.close();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function createRedisJobScheduleStore(
|
|
450
|
+
options: RedisJobsAdapterOptions
|
|
451
|
+
): RedisJobScheduleStore {
|
|
452
|
+
const client =
|
|
453
|
+
requireRedisClient(
|
|
454
|
+
options.client
|
|
455
|
+
);
|
|
456
|
+
const namespace =
|
|
457
|
+
normalizeNamespace(
|
|
458
|
+
options.namespace
|
|
459
|
+
);
|
|
460
|
+
const keys =
|
|
461
|
+
createScheduleKeys(
|
|
462
|
+
namespace
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
namespace,
|
|
467
|
+
|
|
468
|
+
async upsert(schedule) {
|
|
469
|
+
await evalRedis(
|
|
470
|
+
client,
|
|
471
|
+
UPSERT_SCHEDULE_SCRIPT,
|
|
472
|
+
[
|
|
473
|
+
scheduleKey(
|
|
474
|
+
keys.schedulePrefix,
|
|
475
|
+
schedule.id
|
|
476
|
+
),
|
|
477
|
+
keys.all,
|
|
478
|
+
keys.due,
|
|
479
|
+
],
|
|
480
|
+
[
|
|
481
|
+
schedule.id,
|
|
482
|
+
JSON.stringify(schedule),
|
|
483
|
+
String(
|
|
484
|
+
schedule.nextRunAt
|
|
485
|
+
),
|
|
486
|
+
]
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
async get(id) {
|
|
491
|
+
return parseJsonReply<
|
|
492
|
+
JobScheduleRecord<any>
|
|
493
|
+
>(
|
|
494
|
+
await client
|
|
495
|
+
.sendCommand([
|
|
496
|
+
"GET",
|
|
497
|
+
scheduleKey(
|
|
498
|
+
keys.schedulePrefix,
|
|
499
|
+
id
|
|
500
|
+
),
|
|
501
|
+
])
|
|
502
|
+
);
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
async list() {
|
|
506
|
+
const ids =
|
|
507
|
+
await readStringList(
|
|
508
|
+
client,
|
|
509
|
+
[
|
|
510
|
+
"SMEMBERS",
|
|
511
|
+
keys.all,
|
|
512
|
+
]
|
|
513
|
+
);
|
|
514
|
+
const schedules =
|
|
515
|
+
await readSchedulesByIds(
|
|
516
|
+
client,
|
|
517
|
+
keys.schedulePrefix,
|
|
518
|
+
ids
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
return schedules.sort(
|
|
522
|
+
(left, right) =>
|
|
523
|
+
left.nextRunAt -
|
|
524
|
+
right.nextRunAt ||
|
|
525
|
+
left.id.localeCompare(
|
|
526
|
+
right.id
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
async remove(id) {
|
|
532
|
+
const result =
|
|
533
|
+
await evalRedis(
|
|
534
|
+
client,
|
|
535
|
+
REMOVE_SCHEDULE_SCRIPT,
|
|
536
|
+
[
|
|
537
|
+
scheduleKey(
|
|
538
|
+
keys.schedulePrefix,
|
|
539
|
+
id
|
|
540
|
+
),
|
|
541
|
+
keys.all,
|
|
542
|
+
keys.due,
|
|
543
|
+
],
|
|
544
|
+
[
|
|
545
|
+
id,
|
|
546
|
+
]
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
return Number(result) === 1;
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
async acquireDue(
|
|
553
|
+
now,
|
|
554
|
+
acquireOptions
|
|
555
|
+
) {
|
|
556
|
+
const raw =
|
|
557
|
+
await evalRedis(
|
|
558
|
+
client,
|
|
559
|
+
ACQUIRE_DUE_SCHEDULES_SCRIPT,
|
|
560
|
+
[
|
|
561
|
+
keys.due,
|
|
562
|
+
],
|
|
563
|
+
[
|
|
564
|
+
keys.schedulePrefix,
|
|
565
|
+
String(now),
|
|
566
|
+
acquireOptions.ownerId,
|
|
567
|
+
String(
|
|
568
|
+
normalizePositiveInteger(
|
|
569
|
+
acquireOptions.leaseMs,
|
|
570
|
+
"schedule leaseMs"
|
|
571
|
+
)
|
|
572
|
+
),
|
|
573
|
+
String(
|
|
574
|
+
normalizePositiveInteger(
|
|
575
|
+
acquireOptions.limit,
|
|
576
|
+
"schedule limit"
|
|
577
|
+
)
|
|
578
|
+
),
|
|
579
|
+
]
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (!Array.isArray(raw)) {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return raw
|
|
587
|
+
.map(
|
|
588
|
+
value =>
|
|
589
|
+
parseJsonReply<
|
|
590
|
+
JobScheduleRecord
|
|
591
|
+
>(value)
|
|
592
|
+
)
|
|
593
|
+
.filter(
|
|
594
|
+
(
|
|
595
|
+
value
|
|
596
|
+
): value is JobScheduleRecord =>
|
|
597
|
+
value !== null
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
async complete(
|
|
602
|
+
id,
|
|
603
|
+
completeOptions
|
|
604
|
+
) {
|
|
605
|
+
await evalRedis(
|
|
606
|
+
client,
|
|
607
|
+
COMPLETE_SCHEDULE_SCRIPT,
|
|
608
|
+
[
|
|
609
|
+
scheduleKey(
|
|
610
|
+
keys.schedulePrefix,
|
|
611
|
+
id
|
|
612
|
+
),
|
|
613
|
+
keys.due,
|
|
614
|
+
],
|
|
615
|
+
[
|
|
616
|
+
id,
|
|
617
|
+
completeOptions.ownerId,
|
|
618
|
+
String(
|
|
619
|
+
completeOptions
|
|
620
|
+
.lastRunAt
|
|
621
|
+
),
|
|
622
|
+
String(
|
|
623
|
+
completeOptions
|
|
624
|
+
.nextRunAt
|
|
625
|
+
),
|
|
626
|
+
String(
|
|
627
|
+
completeOptions
|
|
628
|
+
.updatedAt
|
|
629
|
+
),
|
|
630
|
+
]
|
|
631
|
+
);
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
async release(
|
|
635
|
+
id,
|
|
636
|
+
ownerId
|
|
637
|
+
) {
|
|
638
|
+
await evalRedis(
|
|
639
|
+
client,
|
|
640
|
+
RELEASE_SCHEDULE_SCRIPT,
|
|
641
|
+
[
|
|
642
|
+
scheduleKey(
|
|
643
|
+
keys.schedulePrefix,
|
|
644
|
+
id
|
|
645
|
+
),
|
|
646
|
+
],
|
|
647
|
+
[
|
|
648
|
+
ownerId,
|
|
649
|
+
]
|
|
650
|
+
);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
async close() {
|
|
654
|
+
if (options.close) {
|
|
655
|
+
await options.close();
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function createQueueKeys(
|
|
662
|
+
namespace: string
|
|
663
|
+
) {
|
|
664
|
+
return {
|
|
665
|
+
all:
|
|
666
|
+
`${namespace}:jobs:all`,
|
|
667
|
+
available:
|
|
668
|
+
`${namespace}:jobs:available`,
|
|
669
|
+
running:
|
|
670
|
+
`${namespace}:jobs:running`,
|
|
671
|
+
terminal:
|
|
672
|
+
`${namespace}:jobs:terminal`,
|
|
673
|
+
deadLetters:
|
|
674
|
+
`${namespace}:jobs:dlq`,
|
|
675
|
+
jobPrefix:
|
|
676
|
+
`${namespace}:job:`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function createScheduleKeys(
|
|
681
|
+
namespace: string
|
|
682
|
+
) {
|
|
683
|
+
return {
|
|
684
|
+
all:
|
|
685
|
+
`${namespace}:schedules:all`,
|
|
686
|
+
due:
|
|
687
|
+
`${namespace}:schedules:due`,
|
|
688
|
+
schedulePrefix:
|
|
689
|
+
`${namespace}:schedule:`,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function jobKey(
|
|
694
|
+
prefix: string,
|
|
695
|
+
id: string
|
|
696
|
+
): string {
|
|
697
|
+
return `${prefix}${id}`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function scheduleKey(
|
|
701
|
+
prefix: string,
|
|
702
|
+
id: string
|
|
703
|
+
): string {
|
|
704
|
+
return `${prefix}${id}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function evalRedis(
|
|
708
|
+
client: RedisCommandClient,
|
|
709
|
+
script: string,
|
|
710
|
+
keys: string[],
|
|
711
|
+
args: string[]
|
|
712
|
+
): Promise<unknown> {
|
|
713
|
+
return client.sendCommand([
|
|
714
|
+
"EVAL",
|
|
715
|
+
script,
|
|
716
|
+
String(keys.length),
|
|
717
|
+
...keys,
|
|
718
|
+
...args,
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function readStringList(
|
|
723
|
+
client: RedisCommandClient,
|
|
724
|
+
command: string[]
|
|
725
|
+
): Promise<string[]> {
|
|
726
|
+
const result =
|
|
727
|
+
await client.sendCommand(
|
|
728
|
+
command
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
if (!Array.isArray(result)) {
|
|
732
|
+
return [];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return result
|
|
736
|
+
.map(
|
|
737
|
+
value =>
|
|
738
|
+
redisString(value)
|
|
739
|
+
)
|
|
740
|
+
.filter(
|
|
741
|
+
(
|
|
742
|
+
value
|
|
743
|
+
): value is string =>
|
|
744
|
+
value !== null
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function readJobsByIds(
|
|
749
|
+
client: RedisCommandClient,
|
|
750
|
+
prefix: string,
|
|
751
|
+
ids: string[]
|
|
752
|
+
): Promise<JobRecord[]> {
|
|
753
|
+
if (ids.length === 0) {
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const raw =
|
|
758
|
+
await client.sendCommand([
|
|
759
|
+
"MGET",
|
|
760
|
+
...ids.map(
|
|
761
|
+
id =>
|
|
762
|
+
jobKey(
|
|
763
|
+
prefix,
|
|
764
|
+
id
|
|
765
|
+
)
|
|
766
|
+
),
|
|
767
|
+
]);
|
|
768
|
+
|
|
769
|
+
if (!Array.isArray(raw)) {
|
|
770
|
+
return [];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return raw
|
|
774
|
+
.map(
|
|
775
|
+
value =>
|
|
776
|
+
parseJsonReply<
|
|
777
|
+
JobRecord
|
|
778
|
+
>(value)
|
|
779
|
+
)
|
|
780
|
+
.filter(
|
|
781
|
+
(
|
|
782
|
+
value
|
|
783
|
+
): value is JobRecord =>
|
|
784
|
+
value !== null
|
|
785
|
+
)
|
|
786
|
+
.sort(
|
|
787
|
+
(left, right) =>
|
|
788
|
+
left.createdAt -
|
|
789
|
+
right.createdAt ||
|
|
790
|
+
left.id.localeCompare(
|
|
791
|
+
right.id
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function readSchedulesByIds(
|
|
797
|
+
client: RedisCommandClient,
|
|
798
|
+
prefix: string,
|
|
799
|
+
ids: string[]
|
|
800
|
+
): Promise<JobScheduleRecord[]> {
|
|
801
|
+
if (ids.length === 0) {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const raw =
|
|
806
|
+
await client.sendCommand([
|
|
807
|
+
"MGET",
|
|
808
|
+
...ids.map(
|
|
809
|
+
id =>
|
|
810
|
+
scheduleKey(
|
|
811
|
+
prefix,
|
|
812
|
+
id
|
|
813
|
+
)
|
|
814
|
+
),
|
|
815
|
+
]);
|
|
816
|
+
|
|
817
|
+
if (!Array.isArray(raw)) {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return raw
|
|
822
|
+
.map(
|
|
823
|
+
value =>
|
|
824
|
+
parseJsonReply<
|
|
825
|
+
JobScheduleRecord
|
|
826
|
+
>(value)
|
|
827
|
+
)
|
|
828
|
+
.filter(
|
|
829
|
+
(
|
|
830
|
+
value
|
|
831
|
+
): value is JobScheduleRecord =>
|
|
832
|
+
value !== null
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function parseJsonReply<T>(
|
|
837
|
+
value: unknown
|
|
838
|
+
): T | null {
|
|
839
|
+
const text =
|
|
840
|
+
redisString(value);
|
|
841
|
+
|
|
842
|
+
if (text === null) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return JSON.parse(text) as T;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function redisString(
|
|
850
|
+
value: unknown
|
|
851
|
+
): string | null {
|
|
852
|
+
if (typeof value === "string") {
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (
|
|
857
|
+
value instanceof Uint8Array
|
|
858
|
+
) {
|
|
859
|
+
return Buffer.from(
|
|
860
|
+
value
|
|
861
|
+
).toString("utf8");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (
|
|
865
|
+
typeof value === "number"
|
|
866
|
+
) {
|
|
867
|
+
return String(value);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function calculateStats(
|
|
874
|
+
jobs: JobRecord[],
|
|
875
|
+
deadLetters: number
|
|
876
|
+
): JobQueueStats {
|
|
877
|
+
const stats:
|
|
878
|
+
JobQueueStats = {
|
|
879
|
+
total: jobs.length,
|
|
880
|
+
queued: 0,
|
|
881
|
+
running: 0,
|
|
882
|
+
succeeded: 0,
|
|
883
|
+
failed: 0,
|
|
884
|
+
cancelled: 0,
|
|
885
|
+
deadLetters,
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
for (const job of jobs) {
|
|
889
|
+
stats[job.state] += 1;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return stats;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function requireRedisClient(
|
|
896
|
+
client: RedisCommandClient
|
|
897
|
+
): RedisCommandClient {
|
|
898
|
+
if (
|
|
899
|
+
!client ||
|
|
900
|
+
typeof client.sendCommand !==
|
|
901
|
+
"function"
|
|
902
|
+
) {
|
|
903
|
+
throw new TypeError(
|
|
904
|
+
"BCP Jobs Redis: client.sendCommand(command) is required."
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return client;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function normalizeNamespace(
|
|
912
|
+
value = "bcp:{jobs}"
|
|
913
|
+
): string {
|
|
914
|
+
const namespace =
|
|
915
|
+
String(value).trim();
|
|
916
|
+
|
|
917
|
+
if (!namespace) {
|
|
918
|
+
throw new TypeError(
|
|
919
|
+
"BCP Jobs Redis: namespace must be non-empty."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return namespace.replace(
|
|
924
|
+
/:+$/,
|
|
925
|
+
""
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function normalizePositiveInteger(
|
|
930
|
+
value: number,
|
|
931
|
+
field: string
|
|
932
|
+
): number {
|
|
933
|
+
if (
|
|
934
|
+
!Number.isInteger(value) ||
|
|
935
|
+
value <= 0
|
|
936
|
+
) {
|
|
937
|
+
throw new TypeError(
|
|
938
|
+
`BCP Jobs Redis: ${field} must be a positive integer.`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return value;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function normalizeNonNegativeNumber(
|
|
946
|
+
value: number,
|
|
947
|
+
field: string
|
|
948
|
+
): number {
|
|
949
|
+
if (
|
|
950
|
+
!Number.isFinite(value) ||
|
|
951
|
+
value < 0
|
|
952
|
+
) {
|
|
953
|
+
throw new TypeError(
|
|
954
|
+
`BCP Jobs Redis: ${field} must be a non-negative finite number.`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return Math.floor(value);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const ENQUEUE_SCRIPT = `
|
|
962
|
+
if redis.call('EXISTS', KEYS[1]) == 1 then
|
|
963
|
+
return 0
|
|
964
|
+
end
|
|
965
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
966
|
+
redis.call('SADD', KEYS[2], ARGV[1])
|
|
967
|
+
redis.call('ZADD', KEYS[3], ARGV[3], ARGV[1])
|
|
968
|
+
return 1
|
|
969
|
+
`;
|
|
970
|
+
|
|
971
|
+
const RESERVE_SCRIPT = `
|
|
972
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, 20)
|
|
973
|
+
for _, id in ipairs(ids) do
|
|
974
|
+
local key = ARGV[1] .. id
|
|
975
|
+
local raw = redis.call('GET', key)
|
|
976
|
+
if not raw then
|
|
977
|
+
redis.call('ZREM', KEYS[1], id)
|
|
978
|
+
else
|
|
979
|
+
local job = cjson.decode(raw)
|
|
980
|
+
if job.state == 'queued' then
|
|
981
|
+
job.state = 'running'
|
|
982
|
+
job.attempts = (job.attempts or 0) + 1
|
|
983
|
+
job.startedAt = tonumber(ARGV[2])
|
|
984
|
+
job.error = nil
|
|
985
|
+
job.recoveredAt = nil
|
|
986
|
+
job.leaseOwner = ARGV[3]
|
|
987
|
+
job.leaseUntil = tonumber(ARGV[2]) + tonumber(ARGV[4])
|
|
988
|
+
job.heartbeatAt = tonumber(ARGV[2])
|
|
989
|
+
local encoded = cjson.encode(job)
|
|
990
|
+
redis.call('SET', key, encoded)
|
|
991
|
+
redis.call('ZREM', KEYS[1], id)
|
|
992
|
+
redis.call('ZADD', KEYS[2], job.leaseUntil, id)
|
|
993
|
+
return encoded
|
|
994
|
+
end
|
|
995
|
+
redis.call('ZREM', KEYS[1], id)
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
return nil
|
|
999
|
+
`;
|
|
1000
|
+
|
|
1001
|
+
const COMPLETE_SCRIPT = `
|
|
1002
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1003
|
+
if not raw then return 0 end
|
|
1004
|
+
local job = cjson.decode(raw)
|
|
1005
|
+
if job.state == 'cancelled' then return 0 end
|
|
1006
|
+
if ARGV[3] ~= '' and job.leaseOwner ~= ARGV[3] then return 0 end
|
|
1007
|
+
job.state = 'succeeded'
|
|
1008
|
+
job.completedAt = tonumber(ARGV[2])
|
|
1009
|
+
job.error = nil
|
|
1010
|
+
job.leaseOwner = nil
|
|
1011
|
+
job.leaseUntil = nil
|
|
1012
|
+
job.heartbeatAt = nil
|
|
1013
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1014
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
1015
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1016
|
+
redis.call('ZADD', KEYS[4], ARGV[2], ARGV[1])
|
|
1017
|
+
redis.call('ZREM', KEYS[5], ARGV[1])
|
|
1018
|
+
return 1
|
|
1019
|
+
`;
|
|
1020
|
+
|
|
1021
|
+
const FAIL_SCRIPT = `
|
|
1022
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1023
|
+
if not raw then return 0 end
|
|
1024
|
+
local job = cjson.decode(raw)
|
|
1025
|
+
if job.state == 'cancelled' then return 0 end
|
|
1026
|
+
if ARGV[5] ~= '' and job.leaseOwner ~= ARGV[5] then return 0 end
|
|
1027
|
+
job.error = ARGV[2]
|
|
1028
|
+
job.leaseOwner = nil
|
|
1029
|
+
job.leaseUntil = nil
|
|
1030
|
+
job.heartbeatAt = nil
|
|
1031
|
+
local retryAt = nil
|
|
1032
|
+
if ARGV[4] ~= '' then retryAt = tonumber(ARGV[4]) end
|
|
1033
|
+
if retryAt and (job.attempts or 0) < (job.maxAttempts or 1) then
|
|
1034
|
+
job.state = 'queued'
|
|
1035
|
+
job.availableAt = retryAt
|
|
1036
|
+
job.startedAt = nil
|
|
1037
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1038
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1039
|
+
redis.call('ZADD', KEYS[2], retryAt, ARGV[1])
|
|
1040
|
+
return 2
|
|
1041
|
+
end
|
|
1042
|
+
job.state = 'failed'
|
|
1043
|
+
job.completedAt = tonumber(ARGV[3])
|
|
1044
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1045
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
1046
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1047
|
+
redis.call('ZADD', KEYS[4], ARGV[3], ARGV[1])
|
|
1048
|
+
redis.call('ZADD', KEYS[5], ARGV[3], ARGV[1])
|
|
1049
|
+
return 1
|
|
1050
|
+
`;
|
|
1051
|
+
|
|
1052
|
+
const CANCEL_SCRIPT = `
|
|
1053
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1054
|
+
if not raw then return 0 end
|
|
1055
|
+
local job = cjson.decode(raw)
|
|
1056
|
+
if job.state == 'succeeded' or job.state == 'failed' or job.state == 'cancelled' then
|
|
1057
|
+
return 0
|
|
1058
|
+
end
|
|
1059
|
+
job.state = 'cancelled'
|
|
1060
|
+
job.completedAt = tonumber(ARGV[2])
|
|
1061
|
+
job.leaseOwner = nil
|
|
1062
|
+
job.leaseUntil = nil
|
|
1063
|
+
job.heartbeatAt = nil
|
|
1064
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1065
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
1066
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1067
|
+
redis.call('ZADD', KEYS[4], ARGV[2], ARGV[1])
|
|
1068
|
+
return 1
|
|
1069
|
+
`;
|
|
1070
|
+
|
|
1071
|
+
const HEARTBEAT_SCRIPT = `
|
|
1072
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1073
|
+
if not raw then return 0 end
|
|
1074
|
+
local job = cjson.decode(raw)
|
|
1075
|
+
if job.state ~= 'running' or job.leaseOwner ~= ARGV[2] then
|
|
1076
|
+
return 0
|
|
1077
|
+
end
|
|
1078
|
+
job.heartbeatAt = tonumber(ARGV[3])
|
|
1079
|
+
job.leaseUntil = tonumber(ARGV[3]) + tonumber(ARGV[4])
|
|
1080
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1081
|
+
redis.call('ZADD', KEYS[2], job.leaseUntil, ARGV[1])
|
|
1082
|
+
return 1
|
|
1083
|
+
`;
|
|
1084
|
+
|
|
1085
|
+
const RECOVER_STALE_SCRIPT = `
|
|
1086
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, tonumber(ARGV[3]))
|
|
1087
|
+
local recovered = 0
|
|
1088
|
+
for _, id in ipairs(ids) do
|
|
1089
|
+
local key = ARGV[1] .. id
|
|
1090
|
+
local raw = redis.call('GET', key)
|
|
1091
|
+
if not raw then
|
|
1092
|
+
redis.call('ZREM', KEYS[1], id)
|
|
1093
|
+
else
|
|
1094
|
+
local job = cjson.decode(raw)
|
|
1095
|
+
if job.state == 'running' and job.leaseUntil and tonumber(job.leaseUntil) <= tonumber(ARGV[2]) then
|
|
1096
|
+
job.state = 'queued'
|
|
1097
|
+
job.availableAt = tonumber(ARGV[2])
|
|
1098
|
+
job.startedAt = nil
|
|
1099
|
+
job.leaseOwner = nil
|
|
1100
|
+
job.leaseUntil = nil
|
|
1101
|
+
job.heartbeatAt = nil
|
|
1102
|
+
job.recoveredAt = tonumber(ARGV[2])
|
|
1103
|
+
redis.call('SET', key, cjson.encode(job))
|
|
1104
|
+
redis.call('ZREM', KEYS[1], id)
|
|
1105
|
+
redis.call('ZADD', KEYS[2], ARGV[2], id)
|
|
1106
|
+
recovered = recovered + 1
|
|
1107
|
+
end
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
return recovered
|
|
1111
|
+
`;
|
|
1112
|
+
|
|
1113
|
+
const REQUEUE_DEAD_LETTER_SCRIPT = `
|
|
1114
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1115
|
+
if not raw then return 0 end
|
|
1116
|
+
if not redis.call('ZSCORE', KEYS[5], ARGV[1]) then return 0 end
|
|
1117
|
+
local job = cjson.decode(raw)
|
|
1118
|
+
if job.state ~= 'failed' then return 0 end
|
|
1119
|
+
job.state = 'queued'
|
|
1120
|
+
job.availableAt = tonumber(ARGV[2]) + tonumber(ARGV[3])
|
|
1121
|
+
job.startedAt = nil
|
|
1122
|
+
job.completedAt = nil
|
|
1123
|
+
job.error = nil
|
|
1124
|
+
job.leaseOwner = nil
|
|
1125
|
+
job.leaseUntil = nil
|
|
1126
|
+
job.heartbeatAt = nil
|
|
1127
|
+
job.recoveredAt = nil
|
|
1128
|
+
if ARGV[4] == '1' then job.attempts = 0 end
|
|
1129
|
+
redis.call('SET', KEYS[1], cjson.encode(job))
|
|
1130
|
+
redis.call('ZADD', KEYS[2], job.availableAt, ARGV[1])
|
|
1131
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1132
|
+
redis.call('ZREM', KEYS[4], ARGV[1])
|
|
1133
|
+
redis.call('ZREM', KEYS[5], ARGV[1])
|
|
1134
|
+
return 1
|
|
1135
|
+
`;
|
|
1136
|
+
|
|
1137
|
+
const CLEANUP_SCRIPT = `
|
|
1138
|
+
local allowed = {}
|
|
1139
|
+
for _, state in ipairs(cjson.decode(ARGV[3])) do allowed[state] = true end
|
|
1140
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, 1000)
|
|
1141
|
+
local removed = 0
|
|
1142
|
+
for _, id in ipairs(ids) do
|
|
1143
|
+
local key = ARGV[1] .. id
|
|
1144
|
+
local raw = redis.call('GET', key)
|
|
1145
|
+
if not raw then
|
|
1146
|
+
redis.call('ZREM', KEYS[1], id)
|
|
1147
|
+
redis.call('SREM', KEYS[2], id)
|
|
1148
|
+
else
|
|
1149
|
+
local job = cjson.decode(raw)
|
|
1150
|
+
if allowed[job.state] then
|
|
1151
|
+
redis.call('DEL', key)
|
|
1152
|
+
redis.call('SREM', KEYS[2], id)
|
|
1153
|
+
redis.call('ZREM', KEYS[1], id)
|
|
1154
|
+
redis.call('ZREM', KEYS[3], id)
|
|
1155
|
+
redis.call('ZREM', KEYS[4], id)
|
|
1156
|
+
redis.call('ZREM', KEYS[5], id)
|
|
1157
|
+
removed = removed + 1
|
|
1158
|
+
end
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
return removed
|
|
1162
|
+
`;
|
|
1163
|
+
|
|
1164
|
+
const UPSERT_SCHEDULE_SCRIPT = `
|
|
1165
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
1166
|
+
redis.call('SADD', KEYS[2], ARGV[1])
|
|
1167
|
+
redis.call('ZADD', KEYS[3], ARGV[3], ARGV[1])
|
|
1168
|
+
return 1
|
|
1169
|
+
`;
|
|
1170
|
+
|
|
1171
|
+
const REMOVE_SCHEDULE_SCRIPT = `
|
|
1172
|
+
local existed = redis.call('DEL', KEYS[1])
|
|
1173
|
+
redis.call('SREM', KEYS[2], ARGV[1])
|
|
1174
|
+
redis.call('ZREM', KEYS[3], ARGV[1])
|
|
1175
|
+
return existed
|
|
1176
|
+
`;
|
|
1177
|
+
|
|
1178
|
+
const ACQUIRE_DUE_SCHEDULES_SCRIPT = `
|
|
1179
|
+
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[2], 'LIMIT', 0, tonumber(ARGV[5]) * 4)
|
|
1180
|
+
local result = {}
|
|
1181
|
+
for _, id in ipairs(ids) do
|
|
1182
|
+
if #result >= tonumber(ARGV[5]) then break end
|
|
1183
|
+
local key = ARGV[1] .. id
|
|
1184
|
+
local raw = redis.call('GET', key)
|
|
1185
|
+
if not raw then
|
|
1186
|
+
redis.call('ZREM', KEYS[1], id)
|
|
1187
|
+
else
|
|
1188
|
+
local schedule = cjson.decode(raw)
|
|
1189
|
+
local leaseUntil = schedule.leaseUntil
|
|
1190
|
+
if schedule.nextRunAt <= tonumber(ARGV[2]) and (not leaseUntil or tonumber(leaseUntil) <= tonumber(ARGV[2])) then
|
|
1191
|
+
schedule.leaseOwner = ARGV[3]
|
|
1192
|
+
schedule.leaseUntil = tonumber(ARGV[2]) + tonumber(ARGV[4])
|
|
1193
|
+
local encoded = cjson.encode(schedule)
|
|
1194
|
+
redis.call('SET', key, encoded)
|
|
1195
|
+
table.insert(result, encoded)
|
|
1196
|
+
end
|
|
1197
|
+
end
|
|
1198
|
+
end
|
|
1199
|
+
return result
|
|
1200
|
+
`;
|
|
1201
|
+
|
|
1202
|
+
const COMPLETE_SCHEDULE_SCRIPT = `
|
|
1203
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1204
|
+
if not raw then return 0 end
|
|
1205
|
+
local schedule = cjson.decode(raw)
|
|
1206
|
+
if schedule.leaseOwner ~= ARGV[2] then return 0 end
|
|
1207
|
+
schedule.lastRunAt = tonumber(ARGV[3])
|
|
1208
|
+
schedule.nextRunAt = tonumber(ARGV[4])
|
|
1209
|
+
schedule.updatedAt = tonumber(ARGV[5])
|
|
1210
|
+
schedule.leaseOwner = nil
|
|
1211
|
+
schedule.leaseUntil = nil
|
|
1212
|
+
redis.call('SET', KEYS[1], cjson.encode(schedule))
|
|
1213
|
+
redis.call('ZADD', KEYS[2], ARGV[4], ARGV[1])
|
|
1214
|
+
return 1
|
|
1215
|
+
`;
|
|
1216
|
+
|
|
1217
|
+
const RELEASE_SCHEDULE_SCRIPT = `
|
|
1218
|
+
local raw = redis.call('GET', KEYS[1])
|
|
1219
|
+
if not raw then return 0 end
|
|
1220
|
+
local schedule = cjson.decode(raw)
|
|
1221
|
+
if schedule.leaseOwner ~= ARGV[1] then return 0 end
|
|
1222
|
+
schedule.leaseOwner = nil
|
|
1223
|
+
schedule.leaseUntil = nil
|
|
1224
|
+
redis.call('SET', KEYS[1], cjson.encode(schedule))
|
|
1225
|
+
return 1
|
|
1226
|
+
`;
|