@cedarjs/jobs 7.1.0 → 7.1.1-next.469

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.
Files changed (40) hide show
  1. package/dist/adapters/BaseAdapter/BaseAdapter.d.ts +133 -0
  2. package/dist/adapters/BaseAdapter/BaseAdapter.d.ts.map +1 -1
  3. package/dist/adapters/BaseAdapter/BaseAdapter.js +6 -0
  4. package/dist/adapters/PrismaAdapter/PrismaAdapter.d.ts +67 -3
  5. package/dist/adapters/PrismaAdapter/PrismaAdapter.d.ts.map +1 -1
  6. package/dist/adapters/PrismaAdapter/PrismaAdapter.js +378 -59
  7. package/dist/adapters/PrismaAdapter/errors.d.ts +14 -0
  8. package/dist/adapters/PrismaAdapter/errors.d.ts.map +1 -1
  9. package/dist/adapters/PrismaAdapter/errors.js +17 -1
  10. package/dist/bins/cedar-jobs-worker.d.ts +11 -0
  11. package/dist/bins/cedar-jobs-worker.d.ts.map +1 -1
  12. package/dist/bins/cedar-jobs-worker.js +12 -1
  13. package/dist/bins/cedar-jobs.d.ts +25 -1
  14. package/dist/bins/cedar-jobs.d.ts.map +1 -1
  15. package/dist/bins/cedar-jobs.js +104 -3
  16. package/dist/consts.d.ts +9 -0
  17. package/dist/consts.d.ts.map +1 -1
  18. package/dist/consts.js +7 -1
  19. package/dist/core/Executor.d.ts.map +1 -1
  20. package/dist/core/Executor.js +11 -4
  21. package/dist/core/JobManager.d.ts +76 -0
  22. package/dist/core/JobManager.d.ts.map +1 -1
  23. package/dist/core/JobManager.js +210 -1
  24. package/dist/core/Scheduler.d.ts.map +1 -1
  25. package/dist/core/Scheduler.js +16 -3
  26. package/dist/core/Worker.d.ts +8 -0
  27. package/dist/core/Worker.d.ts.map +1 -1
  28. package/dist/core/Worker.js +46 -1
  29. package/dist/errors.d.ts +54 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +60 -0
  32. package/dist/index.d.ts +5 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +16 -1
  35. package/dist/loaders.d.ts +29 -0
  36. package/dist/loaders.d.ts.map +1 -1
  37. package/dist/loaders.js +48 -1
  38. package/dist/types.d.ts +101 -2
  39. package/dist/types.d.ts.map +1 -1
  40. 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 { ModelNameError } from "./errors.js";
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 where = {
58
- AND: [
59
- {
60
- OR: [
61
- {
62
- AND: [
63
- { runAt: { lte: /* @__PURE__ */ new Date() } },
64
- {
65
- OR: [
66
- { lockedAt: null },
67
- {
68
- lockedAt: {
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
- { lockedBy: processName }
77
- ]
78
- },
79
- { failedAt: null }
80
- ]
81
- };
82
- const whereWithQueue = where;
83
- if (queues.length > 1 || queues[0] !== "*") {
84
- Object.assign(whereWithQueue, {
85
- AND: [...where.AND, { queue: { in: queues } }]
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 whereWithQueueAndId = Object.assign(whereWithQueue, {
96
- AND: [...whereWithQueue.AND, { id: job.id }]
97
- });
139
+ const whereWithId = {
140
+ AND: [...conditions(this.#hasPausedAtColumn === true), { id: job.id }]
141
+ };
98
142
  const { count } = await this.accessor.updateMany({
99
- where: whereWithQueueAndId,
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
- const { name, path, args } = JSON.parse(data.handler);
109
- return { ...data, name, path, args };
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 worker
125
- * process names are deterministic and reused across restarts
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
- // Schedules a job by creating a new record in the background job table.
211
- // Returns the created job so that callers get access to its `id` (to cancel
212
- // it later, poll its status, etc.)
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 data = await this.accessor.create({
223
- data: {
224
- handler: JSON.stringify({ name, path, args }),
225
- runAt,
226
- cron,
227
- queue,
228
- priority
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
- const row = data;
232
- return { ...row, name, path, args };
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
- ModelNameError
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":";AAWA,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;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,oBAqBA,CAAA"}
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: processName({ id, queues: workerConfig.queue })
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 { BasicLogger } from '../types.js';
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;AAatD,OAAO,KAAK,EAEV,WAAW,EAGZ,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;AAyEjD,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"}
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"}