@cedarjs/jobs 7.1.0 → 7.1.1-next.468
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/dist/adapters/BaseAdapter/BaseAdapter.d.ts +133 -0
- package/dist/adapters/BaseAdapter/BaseAdapter.d.ts.map +1 -1
- package/dist/adapters/BaseAdapter/BaseAdapter.js +6 -0
- package/dist/adapters/PrismaAdapter/PrismaAdapter.d.ts +67 -3
- package/dist/adapters/PrismaAdapter/PrismaAdapter.d.ts.map +1 -1
- package/dist/adapters/PrismaAdapter/PrismaAdapter.js +378 -59
- package/dist/adapters/PrismaAdapter/errors.d.ts +14 -0
- package/dist/adapters/PrismaAdapter/errors.d.ts.map +1 -1
- package/dist/adapters/PrismaAdapter/errors.js +17 -1
- package/dist/bins/cedar-jobs-worker.d.ts +11 -0
- package/dist/bins/cedar-jobs-worker.d.ts.map +1 -1
- package/dist/bins/cedar-jobs-worker.js +12 -1
- package/dist/bins/cedar-jobs.d.ts +25 -1
- package/dist/bins/cedar-jobs.d.ts.map +1 -1
- package/dist/bins/cedar-jobs.js +104 -3
- package/dist/consts.d.ts +9 -0
- package/dist/consts.d.ts.map +1 -1
- package/dist/consts.js +7 -1
- package/dist/core/Executor.d.ts.map +1 -1
- package/dist/core/Executor.js +11 -4
- package/dist/core/JobManager.d.ts +76 -0
- package/dist/core/JobManager.d.ts.map +1 -1
- package/dist/core/JobManager.js +210 -1
- package/dist/core/Scheduler.d.ts.map +1 -1
- package/dist/core/Scheduler.js +16 -3
- package/dist/core/Worker.d.ts +8 -0
- package/dist/core/Worker.d.ts.map +1 -1
- package/dist/core/Worker.js +46 -1
- package/dist/errors.d.ts +54 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +60 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/loaders.d.ts +29 -0
- package/dist/loaders.d.ts.map +1 -1
- package/dist/loaders.js +48 -1
- package/dist/types.d.ts +101 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -9
|
@@ -4,8 +4,19 @@ import {
|
|
|
4
4
|
DEFAULT_MODEL_NAME,
|
|
5
5
|
MAX_RUNTIME_GRACE_PERIOD
|
|
6
6
|
} from "../../consts.js";
|
|
7
|
+
import {
|
|
8
|
+
CedarJSJobsError,
|
|
9
|
+
JobIdNotFoundError,
|
|
10
|
+
JobNotActiveError
|
|
11
|
+
} from "../../errors.js";
|
|
12
|
+
import { nextCronRunAt } from "../../util.js";
|
|
7
13
|
import { BaseAdapter } from "../BaseAdapter/BaseAdapter.js";
|
|
8
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
KeyColumnMissingError,
|
|
16
|
+
ModelNameError,
|
|
17
|
+
PausedAtColumnMissingError
|
|
18
|
+
} from "./errors.js";
|
|
19
|
+
const MAX_KEY_RETRIES = 5;
|
|
9
20
|
function isPrismaAdapterDelegate(value) {
|
|
10
21
|
const candidate = value;
|
|
11
22
|
return candidate !== null && typeof candidate === "object" && typeof candidate.findFirst === "function" && typeof candidate.updateMany === "function" && typeof candidate.delete === "function" && typeof candidate.update === "function" && typeof candidate.create === "function" && typeof candidate.deleteMany === "function";
|
|
@@ -27,11 +38,34 @@ function getActiveProvider(db) {
|
|
|
27
38
|
}
|
|
28
39
|
return provider;
|
|
29
40
|
}
|
|
41
|
+
function isUniqueViolation(e) {
|
|
42
|
+
return typeof e === "object" && e !== null && "code" in e && e.code === "P2002";
|
|
43
|
+
}
|
|
44
|
+
function isMissingKeyColumn(e) {
|
|
45
|
+
return e instanceof Error && e.name === "PrismaClientValidationError" && /Unknown (argument|field) `key`/.test(e.message);
|
|
46
|
+
}
|
|
47
|
+
function releaseKey(job) {
|
|
48
|
+
return job.key != null ? { key: null } : {};
|
|
49
|
+
}
|
|
50
|
+
function rowToJob(row) {
|
|
51
|
+
const { name, path, args } = JSON.parse(row.handler);
|
|
52
|
+
return { ...row, name, path, args };
|
|
53
|
+
}
|
|
54
|
+
function isMissingPausedAtColumn(e) {
|
|
55
|
+
return e instanceof Error && e.name === "PrismaClientValidationError" && /Unknown (argument|field) `pausedAt`/.test(e.message);
|
|
56
|
+
}
|
|
30
57
|
class PrismaAdapter extends BaseAdapter {
|
|
58
|
+
supportsUniqueKeys = true;
|
|
31
59
|
db;
|
|
32
60
|
model;
|
|
33
61
|
accessor;
|
|
34
62
|
provider;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the model has a `pausedAt` column, so `find()` has to leave out
|
|
65
|
+
* paused jobs. Unknown until the first `find()`, which finds out by
|
|
66
|
+
* filtering on the column
|
|
67
|
+
*/
|
|
68
|
+
#hasPausedAtColumn;
|
|
35
69
|
constructor(options) {
|
|
36
70
|
super(options);
|
|
37
71
|
this.db = options.db;
|
|
@@ -54,59 +88,71 @@ class PrismaAdapter extends BaseAdapter {
|
|
|
54
88
|
const maxRuntimeExpire = new Date(
|
|
55
89
|
(/* @__PURE__ */ new Date()).getTime() - ((maxRuntime || DEFAULT_MAX_RUNTIME) + MAX_RUNTIME_GRACE_PERIOD) * 1e3
|
|
56
90
|
);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
lt: maxRuntimeExpire
|
|
70
|
-
}
|
|
91
|
+
const conditions = (withPausedAt) => [
|
|
92
|
+
{
|
|
93
|
+
OR: [
|
|
94
|
+
{
|
|
95
|
+
AND: [
|
|
96
|
+
{ runAt: { lte: /* @__PURE__ */ new Date() } },
|
|
97
|
+
{
|
|
98
|
+
OR: [
|
|
99
|
+
{ lockedAt: null },
|
|
100
|
+
{
|
|
101
|
+
lockedAt: {
|
|
102
|
+
lt: maxRuntimeExpire
|
|
71
103
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const job = await this.accessor.findFirst({
|
|
89
|
-
select: { id: true, attempts: true },
|
|
90
|
-
where: whereWithQueue,
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{ lockedBy: processName }
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{ failedAt: null },
|
|
113
|
+
...withPausedAt ? [{ pausedAt: null }] : [],
|
|
114
|
+
// If queues is ['*'] then skip, otherwise add a WHERE...IN for the
|
|
115
|
+
// array of queue names
|
|
116
|
+
...queues.length > 1 || queues[0] !== "*" ? [{ queue: { in: queues } }] : []
|
|
117
|
+
];
|
|
118
|
+
const findCandidate = (withPausedAt) => this.accessor.findFirst({
|
|
119
|
+
where: { AND: conditions(withPausedAt) },
|
|
91
120
|
orderBy: [{ priority: "asc" }, { runAt: "asc" }],
|
|
92
121
|
take: 1
|
|
93
122
|
});
|
|
123
|
+
let job;
|
|
124
|
+
if (this.#hasPausedAtColumn === false) {
|
|
125
|
+
job = await findCandidate(false);
|
|
126
|
+
} else {
|
|
127
|
+
try {
|
|
128
|
+
job = await findCandidate(true);
|
|
129
|
+
this.#hasPausedAtColumn = true;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (!isMissingPausedAtColumn(e)) {
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
this.#hasPausedAtColumn = false;
|
|
135
|
+
job = await findCandidate(false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
94
138
|
if (job) {
|
|
95
|
-
const
|
|
96
|
-
AND: [...
|
|
97
|
-
}
|
|
139
|
+
const whereWithId = {
|
|
140
|
+
AND: [...conditions(this.#hasPausedAtColumn === true), { id: job.id }]
|
|
141
|
+
};
|
|
98
142
|
const { count } = await this.accessor.updateMany({
|
|
99
|
-
where:
|
|
143
|
+
where: whereWithId,
|
|
100
144
|
data: {
|
|
101
145
|
lockedAt: /* @__PURE__ */ new Date(),
|
|
102
146
|
lockedBy: processName,
|
|
103
|
-
attempts: job.attempts + 1
|
|
147
|
+
attempts: job.attempts + 1,
|
|
148
|
+
...job.cron ? {} : releaseKey(job)
|
|
104
149
|
}
|
|
105
150
|
});
|
|
106
151
|
if (count) {
|
|
107
152
|
const data = await this.accessor.findFirst({ where: { id: job.id } });
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
if (data) {
|
|
154
|
+
return rowToJob(data);
|
|
155
|
+
}
|
|
110
156
|
}
|
|
111
157
|
}
|
|
112
158
|
return void 0;
|
|
@@ -121,8 +167,9 @@ class PrismaAdapter extends BaseAdapter {
|
|
|
121
167
|
* precision. Nothing else changes `lockedAt` while the job is locked, and
|
|
122
168
|
* a newer claim of the same job sets its own, different `lockedAt`. `attempts` alone
|
|
123
169
|
* is not unique per claim, because a successful run of a recurring job
|
|
124
|
-
* resets it to 0, and `lockedBy` is not unique either, because
|
|
125
|
-
* process
|
|
170
|
+
* resets it to 0, and `lockedBy` is not unique either, because it names
|
|
171
|
+
* the worker process, which claims many jobs and can claim the same job
|
|
172
|
+
* again
|
|
126
173
|
*/
|
|
127
174
|
#claimedBy(job) {
|
|
128
175
|
return {
|
|
@@ -194,7 +241,8 @@ ${error.stack}`,
|
|
|
194
241
|
} else {
|
|
195
242
|
const data = {
|
|
196
243
|
failedAt: /* @__PURE__ */ new Date(),
|
|
197
|
-
runAt: null
|
|
244
|
+
runAt: null,
|
|
245
|
+
...releaseKey(job)
|
|
198
246
|
};
|
|
199
247
|
if (error) {
|
|
200
248
|
data.lastError = `${error.message}
|
|
@@ -207,9 +255,16 @@ ${error.stack}`;
|
|
|
207
255
|
});
|
|
208
256
|
}
|
|
209
257
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Schedules a job by creating a new record in the background job table.
|
|
260
|
+
* Returns the created job so that callers get access to its `id` (to cancel
|
|
261
|
+
* it later, poll its status, etc.)
|
|
262
|
+
*
|
|
263
|
+
* With a `key`, the `key` column's unique constraint makes sure only one
|
|
264
|
+
* queued job holds the key, also when several processes schedule at once.
|
|
265
|
+
* If the insert hits the constraint, the job that holds the key is
|
|
266
|
+
* returned with `alreadyQueued: true` instead
|
|
267
|
+
*/
|
|
213
268
|
async schedule({
|
|
214
269
|
name,
|
|
215
270
|
path,
|
|
@@ -217,19 +272,151 @@ ${error.stack}`;
|
|
|
217
272
|
runAt,
|
|
218
273
|
cron,
|
|
219
274
|
queue,
|
|
220
|
-
priority
|
|
275
|
+
priority,
|
|
276
|
+
key
|
|
221
277
|
}) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
278
|
+
const job = { name, path, args };
|
|
279
|
+
const data = {
|
|
280
|
+
handler: JSON.stringify(job),
|
|
281
|
+
runAt,
|
|
282
|
+
cron,
|
|
283
|
+
queue,
|
|
284
|
+
priority,
|
|
285
|
+
...key !== void 0 ? { key } : {}
|
|
286
|
+
};
|
|
287
|
+
if (key === void 0) {
|
|
288
|
+
return { ...await this.#create(data, job), alreadyQueued: false };
|
|
289
|
+
}
|
|
290
|
+
for (let attempt = 0; attempt <= MAX_KEY_RETRIES; attempt++) {
|
|
291
|
+
try {
|
|
292
|
+
return { ...await this.#create(data, job), alreadyQueued: false };
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (!isUniqueViolation(e)) {
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
229
297
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
298
|
+
const queued = await this.accessor.findFirst({ where: { key } });
|
|
299
|
+
if (queued) {
|
|
300
|
+
return { ...rowToJob(queued), alreadyQueued: true };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw new CedarJSJobsError(
|
|
304
|
+
`Could not schedule ${name} with the key \`${key}\`: the key was taken and released again ${MAX_KEY_RETRIES + 1} times in a row`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
async #create(data, job) {
|
|
308
|
+
let row;
|
|
309
|
+
try {
|
|
310
|
+
row = await this.accessor.create({ data });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
if (isMissingKeyColumn(e)) {
|
|
313
|
+
throw new KeyColumnMissingError(String(this.model));
|
|
314
|
+
}
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
return { ...row, ...job };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Makes the stored declared schedules match `schedules`. Each schedule is
|
|
321
|
+
* one row, identified by its unique `key`, so concurrent syncs can't create
|
|
322
|
+
* duplicates: an insert that hits the unique constraint compares against
|
|
323
|
+
* (and if needed updates) the row the other sync created instead
|
|
324
|
+
*/
|
|
325
|
+
async syncSchedules({
|
|
326
|
+
schedules,
|
|
327
|
+
keyPrefix
|
|
328
|
+
}) {
|
|
329
|
+
const result = {
|
|
330
|
+
created: [],
|
|
331
|
+
updated: [],
|
|
332
|
+
removed: [],
|
|
333
|
+
unchanged: []
|
|
334
|
+
};
|
|
335
|
+
if (!this.accessor.findMany) {
|
|
336
|
+
throw new ModelNameError(String(this.model));
|
|
337
|
+
}
|
|
338
|
+
let stored;
|
|
339
|
+
try {
|
|
340
|
+
stored = await this.accessor.findMany({
|
|
341
|
+
where: { key: { startsWith: keyPrefix } }
|
|
342
|
+
});
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (!isMissingKeyColumn(e)) {
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
if (schedules.length === 0) {
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
throw new KeyColumnMissingError(String(this.model));
|
|
351
|
+
}
|
|
352
|
+
const storedByKey = new Map(stored.map((row) => [row.key, row]));
|
|
353
|
+
for (const schedule of schedules) {
|
|
354
|
+
const outcome = await this.#syncSchedule(
|
|
355
|
+
schedule,
|
|
356
|
+
storedByKey.get(schedule.key)
|
|
357
|
+
);
|
|
358
|
+
result[outcome].push(schedule.key);
|
|
359
|
+
}
|
|
360
|
+
const declared = new Set(schedules.map((schedule) => schedule.key));
|
|
361
|
+
const undeclared = stored.map((row) => row.key).filter((key) => !!key && !declared.has(key));
|
|
362
|
+
if (undeclared.length > 0) {
|
|
363
|
+
await this.accessor.deleteMany({ where: { key: { in: undeclared } } });
|
|
364
|
+
result.removed.push(...undeclared);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
async #syncSchedule(schedule, stored) {
|
|
369
|
+
const { name, path, args, cron, runAt, queue, priority, key } = schedule;
|
|
370
|
+
const handler = JSON.stringify({ name, path, args });
|
|
371
|
+
let row = stored;
|
|
372
|
+
for (let attempt = 0; attempt <= MAX_KEY_RETRIES; attempt++) {
|
|
373
|
+
if (!row) {
|
|
374
|
+
try {
|
|
375
|
+
await this.accessor.create({
|
|
376
|
+
data: { handler, runAt, cron, queue, priority, key }
|
|
377
|
+
});
|
|
378
|
+
return "created";
|
|
379
|
+
} catch (e) {
|
|
380
|
+
if (!isUniqueViolation(e)) {
|
|
381
|
+
throw e;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
row = await this.accessor.findFirst({ where: { key } });
|
|
385
|
+
if (!row) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const changes = {};
|
|
390
|
+
if (row.handler !== handler) {
|
|
391
|
+
changes.handler = handler;
|
|
392
|
+
}
|
|
393
|
+
if (row.queue !== queue) {
|
|
394
|
+
changes.queue = queue;
|
|
395
|
+
}
|
|
396
|
+
if (row.priority !== priority) {
|
|
397
|
+
changes.priority = priority;
|
|
398
|
+
}
|
|
399
|
+
if (row.cron !== cron) {
|
|
400
|
+
changes.cron = cron;
|
|
401
|
+
if (!row.lockedAt) {
|
|
402
|
+
changes.runAt = runAt;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (Object.keys(changes).length === 0) {
|
|
406
|
+
return "unchanged";
|
|
407
|
+
}
|
|
408
|
+
const { count } = await this.accessor.updateMany({
|
|
409
|
+
where: { id: row.id },
|
|
410
|
+
data: changes
|
|
411
|
+
});
|
|
412
|
+
if (count > 0) {
|
|
413
|
+
return "updated";
|
|
414
|
+
}
|
|
415
|
+
row = void 0;
|
|
416
|
+
}
|
|
417
|
+
throw new CedarJSJobsError(
|
|
418
|
+
`Could not sync the schedule \`${key}\`: it was created and removed again ${MAX_KEY_RETRIES + 1} times in a row`
|
|
419
|
+
);
|
|
233
420
|
}
|
|
234
421
|
/**
|
|
235
422
|
* Cancels a job by marking it as permanently failed so no worker will pick
|
|
@@ -242,16 +429,148 @@ ${error.stack}`;
|
|
|
242
429
|
*/
|
|
243
430
|
async cancel({ jobId }) {
|
|
244
431
|
this.logger.debug(`[CedarJS Jobs] Cancelling job ${jobId}`);
|
|
432
|
+
const job = await this.accessor.findFirst({
|
|
433
|
+
where: { id: jobId }
|
|
434
|
+
});
|
|
245
435
|
const { count } = await this.accessor.updateMany({
|
|
246
436
|
where: { id: jobId, failedAt: null, runAt: { not: null } },
|
|
247
437
|
data: {
|
|
248
438
|
failedAt: /* @__PURE__ */ new Date(),
|
|
249
439
|
lastError: "Job cancelled by user",
|
|
250
|
-
runAt: null
|
|
440
|
+
runAt: null,
|
|
441
|
+
...releaseKey(job ?? {})
|
|
251
442
|
}
|
|
252
443
|
});
|
|
253
444
|
return count > 0;
|
|
254
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Pauses a job by setting its `pausedAt`, which `find()` leaves out. A run
|
|
448
|
+
* in progress finishes, and `success()` and `error()` keep `pausedAt` when
|
|
449
|
+
* they reschedule the job
|
|
450
|
+
*/
|
|
451
|
+
async pause({ jobId }) {
|
|
452
|
+
const row = await this.#findActive(jobId);
|
|
453
|
+
if (row.pausedAt) {
|
|
454
|
+
return { alreadyPaused: true, pausedAt: row.pausedAt };
|
|
455
|
+
}
|
|
456
|
+
const pausedAt = /* @__PURE__ */ new Date();
|
|
457
|
+
const { count } = await this.accessor.updateMany({
|
|
458
|
+
where: {
|
|
459
|
+
id: row.id,
|
|
460
|
+
pausedAt: null,
|
|
461
|
+
failedAt: null,
|
|
462
|
+
runAt: { not: null }
|
|
463
|
+
},
|
|
464
|
+
data: { pausedAt }
|
|
465
|
+
});
|
|
466
|
+
if (count === 0) {
|
|
467
|
+
const current = await this.#findActive(jobId);
|
|
468
|
+
if (!current.pausedAt) {
|
|
469
|
+
return this.pause({ jobId });
|
|
470
|
+
}
|
|
471
|
+
return { alreadyPaused: true, pausedAt: current.pausedAt };
|
|
472
|
+
}
|
|
473
|
+
return { alreadyPaused: false, pausedAt };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Resumes a paused job. A recurring job runs next at the next match of its
|
|
477
|
+
* cron from now, and, unless it's running, starts that run with a clean
|
|
478
|
+
* slate (`attempts` reset to 0, like after a successful run). A running
|
|
479
|
+
* job keeps its `attempts` and `runAt`: `attempts` identifies the run's
|
|
480
|
+
* claim (see `#claimedBy()`), and the worker sets the next run time when
|
|
481
|
+
* the run finishes, so a run that finishes while this write is under way
|
|
482
|
+
* keeps the run time it set, and the result's `runAt` is `null`. A one-off
|
|
483
|
+
* job keeps its `runAt`
|
|
484
|
+
*/
|
|
485
|
+
async resume({ jobId }) {
|
|
486
|
+
const row = await this.#findActive(jobId);
|
|
487
|
+
if (!row.pausedAt) {
|
|
488
|
+
return { wasPaused: false, runAt: row.runAt };
|
|
489
|
+
}
|
|
490
|
+
const running = !!row.lockedAt;
|
|
491
|
+
const runAt = !row.cron ? row.runAt : running ? null : nextCronRunAt(row.cron);
|
|
492
|
+
const { count } = await this.accessor.updateMany({
|
|
493
|
+
where: {
|
|
494
|
+
id: row.id,
|
|
495
|
+
pausedAt: { not: null },
|
|
496
|
+
failedAt: null,
|
|
497
|
+
runAt: { not: null }
|
|
498
|
+
},
|
|
499
|
+
data: {
|
|
500
|
+
pausedAt: null,
|
|
501
|
+
...row.cron && !running ? { runAt, attempts: 0 } : {}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
if (count === 0) {
|
|
505
|
+
const current = await this.#findActive(jobId);
|
|
506
|
+
return { wasPaused: false, runAt: current.runAt };
|
|
507
|
+
}
|
|
508
|
+
return { wasPaused: true, runAt };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Reads a job that can be paused or resumed: one that hasn't completed or
|
|
512
|
+
* failed for good. The whole row is read, so `pausedAt` is on it exactly
|
|
513
|
+
* when the model has the column
|
|
514
|
+
*/
|
|
515
|
+
async #findActive(jobId) {
|
|
516
|
+
const row = await this.accessor.findFirst({
|
|
517
|
+
where: { id: jobId }
|
|
518
|
+
});
|
|
519
|
+
if (!row) {
|
|
520
|
+
throw new JobIdNotFoundError(jobId);
|
|
521
|
+
}
|
|
522
|
+
if (!("pausedAt" in row)) {
|
|
523
|
+
throw new PausedAtColumnMissingError(String(this.model));
|
|
524
|
+
}
|
|
525
|
+
if (row.failedAt) {
|
|
526
|
+
throw new JobNotActiveError(jobId, "failed");
|
|
527
|
+
}
|
|
528
|
+
if (!row.runAt) {
|
|
529
|
+
throw new JobNotActiveError(jobId, "completed");
|
|
530
|
+
}
|
|
531
|
+
return row;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Lists the jobs that haven't completed or failed for good, the ones that
|
|
535
|
+
* run next first
|
|
536
|
+
*/
|
|
537
|
+
async list({
|
|
538
|
+
limit,
|
|
539
|
+
queues,
|
|
540
|
+
paused
|
|
541
|
+
}) {
|
|
542
|
+
if (!this.accessor.findMany) {
|
|
543
|
+
throw new ModelNameError(String(this.model));
|
|
544
|
+
}
|
|
545
|
+
let rows;
|
|
546
|
+
try {
|
|
547
|
+
rows = await this.accessor.findMany({
|
|
548
|
+
where: {
|
|
549
|
+
failedAt: null,
|
|
550
|
+
runAt: { not: null },
|
|
551
|
+
...queues && !(queues.length === 1 && queues[0] === "*") ? { queue: { in: queues } } : {},
|
|
552
|
+
...paused ? { pausedAt: { not: null } } : {}
|
|
553
|
+
},
|
|
554
|
+
orderBy: [{ runAt: "asc" }, { id: "asc" }],
|
|
555
|
+
take: limit
|
|
556
|
+
});
|
|
557
|
+
} catch (e) {
|
|
558
|
+
if (paused && isMissingPausedAtColumn(e)) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
throw e;
|
|
562
|
+
}
|
|
563
|
+
return rows.map((row) => ({
|
|
564
|
+
id: row.id,
|
|
565
|
+
name: JSON.parse(row.handler).name,
|
|
566
|
+
queue: row.queue,
|
|
567
|
+
cron: row.cron ?? null,
|
|
568
|
+
runAt: row.runAt,
|
|
569
|
+
pausedAt: row.pausedAt ?? null,
|
|
570
|
+
running: !!row.lockedAt,
|
|
571
|
+
attempts: row.attempts
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
255
574
|
async clear() {
|
|
256
575
|
await this.accessor.deleteMany();
|
|
257
576
|
}
|
|
@@ -5,4 +5,18 @@ import { CedarJSJobsError } from '../../errors.js';
|
|
|
5
5
|
export declare class ModelNameError extends CedarJSJobsError {
|
|
6
6
|
constructor(name: string);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a job is scheduled with a unique `key`, or jobs declare
|
|
10
|
+
* `schedules`, but the jobs model has no `key` column to store the key in
|
|
11
|
+
*/
|
|
12
|
+
export declare class KeyColumnMissingError extends CedarJSJobsError {
|
|
13
|
+
constructor(model: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a job is paused or resumed, but the jobs model has no
|
|
17
|
+
* `pausedAt` column to store the pause in
|
|
18
|
+
*/
|
|
19
|
+
export declare class PausedAtColumnMissingError extends CedarJSJobsError {
|
|
20
|
+
constructor(model: string);
|
|
21
|
+
}
|
|
8
22
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/adapters/PrismaAdapter/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;GAEG;AACH,qBAAa,cAAe,SAAQ,gBAAgB;gBACtC,IAAI,EAAE,MAAM;CAGzB"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/adapters/PrismaAdapter/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;GAEG;AACH,qBAAa,cAAe,SAAQ,gBAAgB;gBACtC,IAAI,EAAE,MAAM;CAGzB;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;gBAC7C,KAAK,EAAE,MAAM;CAQ1B;AAED;;;GAGG;AACH,qBAAa,0BAA2B,SAAQ,gBAAgB;gBAClD,KAAK,EAAE,MAAM;CAO1B"}
|
|
@@ -4,6 +4,22 @@ class ModelNameError extends CedarJSJobsError {
|
|
|
4
4
|
super(`Model \`${name}\` not found in PrismaClient`);
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
class KeyColumnMissingError extends CedarJSJobsError {
|
|
8
|
+
constructor(model) {
|
|
9
|
+
super(
|
|
10
|
+
`The \`${model}\` model has no \`key\` column, which unique job keys and declared schedules need. Add \`key String? @unique\` to the model in your Prisma schema and create a migration with \`yarn cedar prisma migrate dev\``
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class PausedAtColumnMissingError extends CedarJSJobsError {
|
|
15
|
+
constructor(model) {
|
|
16
|
+
super(
|
|
17
|
+
`The \`${model}\` model has no \`pausedAt\` column, which pausing jobs needs. Add \`pausedAt DateTime?\` to the model in your Prisma schema and create a migration with \`yarn cedar prisma migrate dev\``
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
7
21
|
export {
|
|
8
|
-
|
|
22
|
+
KeyColumnMissingError,
|
|
23
|
+
ModelNameError,
|
|
24
|
+
PausedAtColumnMissingError
|
|
9
25
|
};
|
|
@@ -4,6 +4,17 @@ export declare const processName: ({ id, queues, }: {
|
|
|
4
4
|
id: number;
|
|
5
5
|
queues: string | string[];
|
|
6
6
|
}) => string;
|
|
7
|
+
/**
|
|
8
|
+
* The name a worker locks jobs with (`lockedBy`). A worker takes back any job
|
|
9
|
+
* locked under its own name, so the name has to be unique to this process:
|
|
10
|
+
* the process name alone is the same for the same worker config in every
|
|
11
|
+
* app server, on every host, and two workers sharing it would both run a job,
|
|
12
|
+
* and each one's result would hand the job back to the other to run again
|
|
13
|
+
*/
|
|
14
|
+
export declare const lockName: ({ id, queues, }: {
|
|
15
|
+
id: number;
|
|
16
|
+
queues: string | string[];
|
|
17
|
+
}) => string;
|
|
7
18
|
export declare const getWorker: ({ index, id, clear, workoff, }: {
|
|
8
19
|
index: number;
|
|
9
20
|
id: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cedar-jobs-worker.d.ts","sourceRoot":"","sources":["../../src/bins/cedar-jobs-worker.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cedar-jobs-worker.d.ts","sourceRoot":"","sources":["../../src/bins/cedar-jobs-worker.ts"],"names":[],"mappings":";AAYA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAwC/C,eAAO,MAAM,WAAW,GAAI,iBAGzB;IACD,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,WAEA,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,iBAGtB;IACD,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC1B,WAEA,CAAA;AA0BD,eAAO,MAAM,SAAS,GAAU,gCAK7B;IACD,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;CACjB,oBA+BA,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { hostname } from "node:os";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { hideBin } from "yargs/helpers";
|
|
4
5
|
import yargs from "yargs/yargs";
|
|
@@ -34,6 +35,12 @@ const processName = ({
|
|
|
34
35
|
}) => {
|
|
35
36
|
return `${PROCESS_TITLE_PREFIX}.${[queues].flat().join("-")}.${id}`;
|
|
36
37
|
};
|
|
38
|
+
const lockName = ({
|
|
39
|
+
id,
|
|
40
|
+
queues
|
|
41
|
+
}) => {
|
|
42
|
+
return `${processName({ id, queues })} host:${hostname()} pid:${process.pid}`;
|
|
43
|
+
};
|
|
37
44
|
const setupSignals = ({ worker }) => {
|
|
38
45
|
process.on("SIGINT", () => {
|
|
39
46
|
worker.logger.warn(
|
|
@@ -65,11 +72,14 @@ const getWorker = async ({
|
|
|
65
72
|
if (!workerConfig) {
|
|
66
73
|
throw new WorkerConfigIndexNotFoundError(index);
|
|
67
74
|
}
|
|
75
|
+
if (!clear && !workoff) {
|
|
76
|
+
await manager.syncSchedules();
|
|
77
|
+
}
|
|
68
78
|
return manager.createWorker({
|
|
69
79
|
index,
|
|
70
80
|
clear,
|
|
71
81
|
workoff,
|
|
72
|
-
processName:
|
|
82
|
+
processName: lockName({ id, queues: workerConfig.queue })
|
|
73
83
|
});
|
|
74
84
|
};
|
|
75
85
|
const main = async () => {
|
|
@@ -87,5 +97,6 @@ if (process.env.NODE_ENV !== "test") {
|
|
|
87
97
|
}
|
|
88
98
|
export {
|
|
89
99
|
getWorker,
|
|
100
|
+
lockName,
|
|
90
101
|
processName
|
|
91
102
|
};
|
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { ChildProcess } from 'node:child_process';
|
|
3
|
-
import type {
|
|
3
|
+
import type { JobManager } from '../core/JobManager.js';
|
|
4
|
+
import type { Adapters, BasicLogger, QueueNames } from '../types.js';
|
|
4
5
|
export type NumWorkersConfig = [number, number][];
|
|
6
|
+
export declare const parseArgs: (argv: string[]) => {
|
|
7
|
+
command: any;
|
|
8
|
+
id: string | number | undefined;
|
|
9
|
+
adapter: string | undefined;
|
|
10
|
+
limit: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A job id from the command line: a number when it's a non-negative integer
|
|
14
|
+
* written the way JavaScript writes it (the `PrismaAdapter`'s ids are),
|
|
15
|
+
* otherwise the string as given, so ids like `00042` or ones past
|
|
16
|
+
* `Number.MAX_SAFE_INTEGER` reach the adapter unchanged
|
|
17
|
+
*/
|
|
18
|
+
export declare const parseJobId: (value: unknown) => string | number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* `cedar jobs pause`, `resume` and `list`. Returns what to print
|
|
21
|
+
*/
|
|
22
|
+
export declare const runPauseCommand: ({ command, id, adapter, limit, manager, }: {
|
|
23
|
+
command: "pause" | "resume" | "list";
|
|
24
|
+
id?: string | number | undefined;
|
|
25
|
+
adapter?: string | undefined;
|
|
26
|
+
limit?: number;
|
|
27
|
+
manager: JobManager<Adapters, QueueNames, BasicLogger>;
|
|
28
|
+
}) => Promise<string>;
|
|
5
29
|
export declare const buildNumWorkers: (config: any) => NumWorkersConfig;
|
|
6
30
|
export declare const startWorkers: ({ numWorkers, detach, workoff, logger, }: {
|
|
7
31
|
numWorkers: NumWorkersConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cedar-jobs.d.ts","sourceRoot":"","sources":["../../src/bins/cedar-jobs.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"cedar-jobs.d.ts","sourceRoot":"","sources":["../../src/bins/cedar-jobs.ts"],"names":[],"mappings":";AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAWtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAGvD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EAEX,UAAU,EACX,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;AAQjD,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE;;;;;CAgEvC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,MAAM,GAAG,SAgB7D,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAU,2CAMnC;IACD,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,UAAU,CAAC,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CAAA;CACvD,KAAG,OAAO,CAAC,MAAM,CAsEjB,CAAA;AA2BD,eAAO,MAAM,eAAe,GAAI,QAAQ,GAAG,qBAU1C,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,0CAK1B;IACD,UAAU,EAAE,gBAAgB,CAAA;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,WAAW,CAAA;CACpB,mBA2BA,CAAA;AAGD,eAAO,MAAM,WAAW,GAAU,iCAI/B;IACD,UAAU,EAAE,gBAAgB,CAAA;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAA;CACpB,kBAqBA,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,YAAY;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,SAG7D,CAAA"}
|