pg-boss 9.0.2 → 10.0.0-beta1

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/src/manager.js CHANGED
@@ -1,21 +1,19 @@
1
1
  const assert = require('assert')
2
2
  const EventEmitter = require('events')
3
- const delay = require('delay')
4
- const uuid = require('uuid')
3
+ const { randomUUID } = require('crypto')
5
4
  const debounce = require('lodash.debounce')
6
5
  const { serializeError: stringify } = require('serialize-error')
6
+ const pMap = require('p-map')
7
+ const { delay } = require('./tools')
7
8
  const Attorney = require('./attorney')
8
9
  const Worker = require('./worker')
10
+ const plans = require('./plans')
9
11
  const Db = require('./db')
10
- const pMap = require('p-map')
11
12
 
12
- const { QUEUES: BOSS_QUEUES } = require('./boss')
13
13
  const { QUEUES: TIMEKEEPER_QUEUES } = require('./timekeeper')
14
+ const { QUEUE_POLICY } = plans
14
15
 
15
- const INTERNAL_QUEUES = Object.values(BOSS_QUEUES).concat(Object.values(TIMEKEEPER_QUEUES)).reduce((acc, i) => ({ ...acc, [i]: i }), {})
16
-
17
- const plans = require('./plans')
18
- const { COMPLETION_JOB_PREFIX, SINGLETON_QUEUE_KEY } = plans
16
+ const INTERNAL_QUEUES = Object.values(TIMEKEEPER_QUEUES).reduce((acc, i) => ({ ...acc, [i]: i }), {})
19
17
 
20
18
  const WIP_EVENT_INTERVAL = 2000
21
19
  const WIP_DEBOUNCE_OPTIONS = { leading: true, trailing: true, maxWait: WIP_EVENT_INTERVAL }
@@ -27,16 +25,14 @@ const events = {
27
25
 
28
26
  const resolveWithinSeconds = async (promise, seconds) => {
29
27
  const timeout = Math.max(1, seconds) * 1000
30
- const reject = delay.reject(timeout, { value: new Error(`handler execution exceeded ${timeout}ms`) })
28
+ const reject = delay(timeout, `handler execution exceeded ${timeout}ms`)
31
29
 
32
30
  let result
33
31
 
34
32
  try {
35
33
  result = await Promise.race([promise, reject])
36
34
  } finally {
37
- try {
38
- reject.clear()
39
- } catch {}
35
+ reject.abort()
40
36
  }
41
37
 
42
38
  return result
@@ -58,7 +54,7 @@ class Manager extends EventEmitter {
58
54
  this.completeJobsCommand = plans.completeJobs(config.schema)
59
55
  this.cancelJobsCommand = plans.cancelJobs(config.schema)
60
56
  this.resumeJobsCommand = plans.resumeJobs(config.schema)
61
- this.failJobsCommand = plans.failJobs(config.schema)
57
+ this.failJobsByIdCommand = plans.failJobsById(config.schema)
62
58
  this.getJobByIdCommand = plans.getJobById(config.schema)
63
59
  this.getArchivedJobByIdCommand = plans.getArchivedJobById(config.schema)
64
60
  this.subscribeCommand = plans.subscribe(config.schema)
@@ -72,12 +68,9 @@ class Manager extends EventEmitter {
72
68
  this.resume,
73
69
  this.fail,
74
70
  this.fetch,
75
- this.fetchCompleted,
76
71
  this.work,
77
72
  this.offWork,
78
73
  this.notifyWorker,
79
- this.onComplete,
80
- this.offComplete,
81
74
  this.publish,
82
75
  this.subscribe,
83
76
  this.unsubscribe,
@@ -85,13 +78,14 @@ class Manager extends EventEmitter {
85
78
  this.send,
86
79
  this.sendDebounced,
87
80
  this.sendThrottled,
88
- this.sendOnce,
89
81
  this.sendAfter,
90
- this.sendSingleton,
82
+ this.createQueue,
83
+ this.updateQueue,
84
+ this.getQueue,
91
85
  this.deleteQueue,
92
- this.deleteAllQueues,
93
- this.clearStorage,
86
+ this.purgeQueue,
94
87
  this.getQueueSize,
88
+ this.clearStorage,
95
89
  this.getJobById
96
90
  ]
97
91
 
@@ -99,27 +93,30 @@ class Manager extends EventEmitter {
99
93
  }
100
94
 
101
95
  start () {
102
- this.stopping = false
96
+ this.stopped = false
103
97
  }
104
98
 
105
99
  async stop () {
106
- this.stopping = true
100
+ this.stopped = true
107
101
 
108
- for (const sub of this.workers.values()) {
109
- if (!INTERNAL_QUEUES[sub.name]) {
110
- await this.offWork(sub.name)
102
+ for (const worker of this.workers.values()) {
103
+ if (!INTERNAL_QUEUES[worker.name]) {
104
+ await this.offWork(worker.name)
111
105
  }
112
106
  }
113
107
  }
114
108
 
115
- async work (name, ...args) {
116
- const { options, callback } = Attorney.checkWorkArgs(name, args, this.config)
117
- return await this.watch(name, options, callback)
109
+ async failWip () {
110
+ const jobIds = Array.from(this.workers.values()).flatMap(w => w.jobs.map(j => j.id))
111
+
112
+ if (jobIds.length) {
113
+ await this.fail(jobIds, 'pg-boss shut down while active')
114
+ }
118
115
  }
119
116
 
120
- async onComplete (name, ...args) {
117
+ async work (name, ...args) {
121
118
  const { options, callback } = Attorney.checkWorkArgs(name, args, this.config)
122
- return await this.watch(COMPLETION_JOB_PREFIX + name, options, callback)
119
+ return await this.watch(name, options, callback)
123
120
  }
124
121
 
125
122
  addWorker (worker) {
@@ -175,8 +172,8 @@ class Manager extends EventEmitter {
175
172
  }
176
173
 
177
174
  async watch (name, options, callback) {
178
- if (this.stopping) {
179
- throw new Error('Workers are disabled. pg-boss is stopping.')
175
+ if (this.stopped) {
176
+ throw new Error('Workers are disabled. pg-boss is stopped')
180
177
  }
181
178
 
182
179
  const {
@@ -186,10 +183,10 @@ class Manager extends EventEmitter {
186
183
  teamConcurrency = 1,
187
184
  teamRefill: refill = false,
188
185
  includeMetadata = false,
189
- enforceSingletonQueueActiveLimit = false
186
+ priority = true
190
187
  } = options
191
188
 
192
- const id = uuid.v4()
189
+ const id = randomUUID({ disableEntropyCache: true })
193
190
 
194
191
  let queueSize = 0
195
192
 
@@ -210,7 +207,7 @@ class Manager extends EventEmitter {
210
207
  createTeamRefillPromise()
211
208
  }
212
209
 
213
- const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata, enforceSingletonQueueActiveLimit })
210
+ const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata, priority })
214
211
 
215
212
  const onFetch = async (jobs) => {
216
213
  if (this.config.__test__throw_worker) {
@@ -329,39 +326,11 @@ class Manager extends EventEmitter {
329
326
  return await Promise.all(result.rows.map(({ name }) => this.send(name, ...args)))
330
327
  }
331
328
 
332
- async offComplete (value) {
333
- if (typeof value === 'string') {
334
- value = COMPLETION_JOB_PREFIX + value
335
- }
336
-
337
- return await this.offWork(value)
338
- }
339
-
340
329
  async send (...args) {
341
330
  const { name, data, options } = Attorney.checkSendArgs(args, this.config)
342
331
  return await this.createJob(name, data, options)
343
332
  }
344
333
 
345
- async sendOnce (name, data, options, key) {
346
- options = options ? { ...options } : {}
347
-
348
- options.singletonKey = key || name
349
-
350
- const result = Attorney.checkSendArgs([name, data, options], this.config)
351
-
352
- return await this.createJob(result.name, result.data, result.options)
353
- }
354
-
355
- async sendSingleton (name, data, options) {
356
- options = options ? { ...options } : {}
357
-
358
- options.singletonKey = SINGLETON_QUEUE_KEY
359
-
360
- const result = Attorney.checkSendArgs([name, data, options], this.config)
361
-
362
- return await this.createJob(result.name, result.data, result.options)
363
- }
364
-
365
334
  async sendAfter (name, data, options, after) {
366
335
  options = options ? { ...options } : {}
367
336
  options.startAfter = after
@@ -395,37 +364,47 @@ class Manager extends EventEmitter {
395
364
 
396
365
  async createJob (name, data, options, singletonOffset = 0) {
397
366
  const {
367
+ id = null,
398
368
  db: wrapper,
399
- expireIn,
400
369
  priority,
401
370
  startAfter,
402
- keepUntil,
403
371
  singletonKey = null,
404
372
  singletonSeconds,
405
- retryBackoff,
373
+ deadLetter = null,
374
+ expireIn,
375
+ expireInDefault,
376
+ keepUntil,
377
+ keepUntilDefault,
406
378
  retryLimit,
379
+ retryLimitDefault,
407
380
  retryDelay,
408
- onComplete
381
+ retryDelayDefault,
382
+ retryBackoff,
383
+ retryBackoffDefault
409
384
  } = options
410
385
 
411
- const id = uuid[this.config.uuid]()
412
-
413
386
  const values = [
414
387
  id, // 1
415
388
  name, // 2
416
- priority, // 3
417
- retryLimit, // 4
389
+ data, // 3
390
+ priority, // 4
418
391
  startAfter, // 5
419
- expireIn, // 6
420
- data, // 7
421
- singletonKey, // 8
422
- singletonSeconds, // 9
423
- singletonOffset, // 10
424
- retryDelay, // 11
425
- retryBackoff, // 12
426
- keepUntil, // 13
427
- onComplete // 14
392
+ singletonKey, // 6
393
+ singletonSeconds, // 7
394
+ singletonOffset, // 8
395
+ deadLetter, // 9
396
+ expireIn, // 10
397
+ expireInDefault, // 11
398
+ keepUntil, // 12
399
+ keepUntilDefault, // 13
400
+ retryLimit, // 14
401
+ retryLimitDefault, // 15
402
+ retryDelay, // 16
403
+ retryDelayDefault, // 17
404
+ retryBackoff, // 18
405
+ retryBackoffDefault // 19
428
406
  ]
407
+
429
408
  const db = wrapper || this.db
430
409
  const result = await db.executeSql(this.insertJobCommand, values)
431
410
 
@@ -449,11 +428,20 @@ class Manager extends EventEmitter {
449
428
  }
450
429
 
451
430
  async insert (jobs, options = {}) {
452
- const { db: wrapper } = options
453
- const db = wrapper || this.db
454
- const checkedJobs = Attorney.checkInsertArgs(jobs)
455
- const data = JSON.stringify(checkedJobs)
456
- return await db.executeSql(this.insertJobsCommand, [data])
431
+ assert(Array.isArray(jobs), 'jobs argument should be an array')
432
+
433
+ const db = options.db || this.db
434
+
435
+ const params = [
436
+ JSON.stringify(jobs), // 1
437
+ this.config.expireIn, // 2
438
+ this.config.keepUntil, // 3
439
+ this.config.retryLimit, // 4
440
+ this.config.retryDelay, // 5
441
+ this.config.retryBackoff // 6
442
+ ]
443
+
444
+ return await db.executeSql(this.insertJobsCommand, params)
457
445
  }
458
446
 
459
447
  getDebounceStartAfter (singletonSeconds, clockOffset) {
@@ -474,27 +462,34 @@ class Manager extends EventEmitter {
474
462
  }
475
463
 
476
464
  async fetch (name, batchSize, options = {}) {
465
+ const patternMatch = Attorney.queueNameHasPatternMatch(name)
477
466
  const values = Attorney.checkFetchArgs(name, batchSize, options)
478
467
  const db = options.db || this.db
479
- const preparedStatement = this.nextJobCommand(options.includeMetadata || false, options.enforceSingletonQueueActiveLimit || false)
468
+ const nextJobSql = this.nextJobCommand({ ...options, patternMatch })
480
469
  const statementValues = [values.name, batchSize || 1]
481
470
 
482
471
  let result
483
- if (options.enforceSingletonQueueActiveLimit && !options.db) {
484
- // Prepare/format now and send multi-statement transaction
485
- const fetchQuery = preparedStatement
486
- .replace('$1', Db.quotePostgresStr(statementValues[0]))
487
- .replace('$2', statementValues[1].toString())
488
- // eslint-disable-next-line no-unused-vars
489
- const [_begin, _setLocal, fetchResult, _commit] = await db.executeSql([
490
- 'BEGIN',
491
- 'SET LOCAL jit = OFF', // JIT can slow things down significantly
492
- fetchQuery,
493
- 'COMMIT'
494
- ].join(';\n'))
495
- result = fetchResult
496
- } else {
497
- result = await db.executeSql(preparedStatement, statementValues)
472
+
473
+ try {
474
+ if (!options.db) {
475
+ // Prepare/format now and send multi-statement transaction
476
+ const fetchQuery = nextJobSql
477
+ .replace('$1', Db.quotePostgresStr(statementValues[0]))
478
+ .replace('$2', statementValues[1].toString())
479
+
480
+ // eslint-disable-next-line no-unused-vars
481
+ const [_begin, _setLocal, fetchResult, _commit] = await db.executeSql([
482
+ 'BEGIN',
483
+ 'SET LOCAL jit = OFF', // JIT can slow things down significantly
484
+ fetchQuery,
485
+ 'COMMIT'
486
+ ].join(';\n'))
487
+ result = fetchResult
488
+ } else {
489
+ result = await db.executeSql(nextJobSql, statementValues)
490
+ }
491
+ } catch (err) {
492
+ // errors from fetchquery should only be unique constraint violations
498
493
  }
499
494
 
500
495
  if (!result || result.rows.length === 0) {
@@ -504,10 +499,6 @@ class Manager extends EventEmitter {
504
499
  return result.rows.length === 1 && !batchSize ? result.rows[0] : result.rows
505
500
  }
506
501
 
507
- async fetchCompleted (name, batchSize, options = {}) {
508
- return await this.fetch(COMPLETION_JOB_PREFIX + name, batchSize, options)
509
- }
510
-
511
502
  mapCompletionIdArg (id, funcName) {
512
503
  const errorMessage = `${funcName}() requires an id`
513
504
 
@@ -548,7 +539,7 @@ class Manager extends EventEmitter {
548
539
  async fail (id, data, options = {}) {
549
540
  const db = options.db || this.db
550
541
  const ids = this.mapCompletionIdArg(id, 'fail')
551
- const result = await db.executeSql(this.failJobsCommand, [ids, this.mapCompletionDataArg(data)])
542
+ const result = await db.executeSql(this.failJobsByIdCommand, [ids, this.mapCompletionDataArg(data)])
552
543
  return this.mapCompletionResponse(ids, result)
553
544
  }
554
545
 
@@ -566,17 +557,122 @@ class Manager extends EventEmitter {
566
557
  return this.mapCompletionResponse(ids, result)
567
558
  }
568
559
 
569
- async deleteQueue (queue, options) {
570
- assert(queue, 'Missing queue name argument')
571
- const sql = plans.deleteQueue(this.config.schema, options)
572
- const result = await this.db.executeSql(sql, [queue])
573
- return result ? result.rowCount : null
560
+ async createQueue (name, options = {}) {
561
+ assert(name, 'Missing queue name argument')
562
+
563
+ const { policy = QUEUE_POLICY.standard } = options
564
+
565
+ assert(policy in QUEUE_POLICY, `${policy} is not a valid queue policy`)
566
+
567
+ const {
568
+ retryLimit,
569
+ retryDelay,
570
+ retryBackoff,
571
+ expireInSeconds,
572
+ retentionMinutes,
573
+ deadLetter
574
+ } = Attorney.checkQueueArgs(name, options)
575
+
576
+ const paritionSql = plans.partitionCreateJobName(this.config.schema, name)
577
+
578
+ await this.db.executeSql(paritionSql)
579
+
580
+ const sql = plans.createQueue(this.config.schema, name)
581
+
582
+ const params = [
583
+ name,
584
+ policy,
585
+ retryLimit,
586
+ retryDelay,
587
+ retryBackoff,
588
+ expireInSeconds,
589
+ retentionMinutes,
590
+ deadLetter
591
+ ]
592
+
593
+ await this.db.executeSql(sql, params)
594
+ }
595
+
596
+ async updateQueue (name, options = {}) {
597
+ assert(name, 'Missing queue name argument')
598
+
599
+ const {
600
+ retryLimit,
601
+ retryDelay,
602
+ retryBackoff,
603
+ expireInSeconds,
604
+ retentionMinutes,
605
+ deadLetter
606
+ } = Attorney.checkQueueArgs(name, options)
607
+
608
+ const sql = plans.updateQueue(this.config.schema)
609
+
610
+ const params = [
611
+ name,
612
+ retryLimit,
613
+ retryDelay,
614
+ retryBackoff,
615
+ expireInSeconds,
616
+ retentionMinutes,
617
+ deadLetter
618
+ ]
619
+
620
+ await this.db.executeSql(sql, params)
574
621
  }
575
622
 
576
- async deleteAllQueues (options) {
577
- const sql = plans.deleteAllQueues(this.config.schema, options)
578
- const result = await this.db.executeSql(sql)
579
- return result ? result.rowCount : null
623
+ async getQueue (name) {
624
+ assert(name, 'Missing queue name argument')
625
+
626
+ const sql = plans.getQueueByName(this.config.schema)
627
+ const result = await this.db.executeSql(sql, [name])
628
+
629
+ if (result.rows.length === 0) {
630
+ return null
631
+ }
632
+
633
+ const {
634
+ policy,
635
+ retry_limit: retryLimit,
636
+ retry_delay: retryDelay,
637
+ retry_backoff: retryBackoff,
638
+ expire_seconds: expireInSeconds,
639
+ retention_minutes: retentionMinutes,
640
+ dead_letter: deadLetter
641
+ } = result.rows[0]
642
+
643
+ return {
644
+ name,
645
+ policy,
646
+ retryLimit,
647
+ retryDelay,
648
+ retryBackoff,
649
+ expireInSeconds,
650
+ retentionMinutes,
651
+ deadLetter
652
+ }
653
+ }
654
+
655
+ async deleteQueue (name) {
656
+ assert(name, 'Missing queue name argument')
657
+
658
+ const queueSql = plans.getQueueByName(this.config.schema)
659
+ const result = await this.db.executeSql(queueSql, [name])
660
+
661
+ if (result?.rows?.length) {
662
+ Attorney.assertPostgresObjectName(name)
663
+ const sql = plans.dropJobTablePartition(this.config.schema, name)
664
+ await this.db.executeSql(sql)
665
+ }
666
+
667
+ const sql = plans.deleteQueueRecords(this.config.schema)
668
+ const result2 = await this.db.executeSql(sql, [name])
669
+ return result2?.rowCount || null
670
+ }
671
+
672
+ async purgeQueue (queue) {
673
+ assert(queue, 'Missing queue name argument')
674
+ const sql = plans.purgeQueue(this.config.schema)
675
+ await this.db.executeSql(sql, [queue])
580
676
  }
581
677
 
582
678
  async clearStorage () {