pg-boss 10.0.0-beta9 → 10.0.0

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,9 +1,7 @@
1
1
  const assert = require('assert')
2
2
  const EventEmitter = require('events')
3
3
  const { randomUUID } = require('crypto')
4
- const debounce = require('lodash.debounce')
5
4
  const { serializeError: stringify } = require('serialize-error')
6
- const pMap = require('p-map')
7
5
  const { delay } = require('./tools')
8
6
  const Attorney = require('./attorney')
9
7
  const Worker = require('./worker')
@@ -14,9 +12,6 @@ const { QUEUE_POLICIES } = plans
14
12
 
15
13
  const INTERNAL_QUEUES = Object.values(TIMEKEEPER_QUEUES).reduce((acc, i) => ({ ...acc, [i]: i }), {})
16
14
 
17
- const WIP_EVENT_INTERVAL = 2000
18
- const WIP_DEBOUNCE_OPTIONS = { leading: true, trailing: true, maxWait: WIP_EVENT_INTERVAL }
19
-
20
15
  const events = {
21
16
  error: 'error',
22
17
  wip: 'wip'
@@ -45,6 +40,7 @@ class Manager extends EventEmitter {
45
40
  this.db = db
46
41
 
47
42
  this.events = events
43
+ this.wipTs = Date.now()
48
44
  this.workers = new Map()
49
45
 
50
46
  this.nextJobCommand = plans.fetchNextJob(config.schema)
@@ -53,18 +49,27 @@ class Manager extends EventEmitter {
53
49
  this.completeJobsCommand = plans.completeJobs(config.schema)
54
50
  this.cancelJobsCommand = plans.cancelJobs(config.schema)
55
51
  this.resumeJobsCommand = plans.resumeJobs(config.schema)
52
+ this.deleteJobsCommand = plans.deleteJobs(config.schema)
56
53
  this.failJobsByIdCommand = plans.failJobsById(config.schema)
57
54
  this.getJobByIdCommand = plans.getJobById(config.schema)
58
55
  this.getArchivedJobByIdCommand = plans.getArchivedJobById(config.schema)
59
56
  this.subscribeCommand = plans.subscribe(config.schema)
60
57
  this.unsubscribeCommand = plans.unsubscribe(config.schema)
58
+ this.getQueuesCommand = plans.getQueues(config.schema)
59
+ this.getQueueByNameCommand = plans.getQueueByName(config.schema)
61
60
  this.getQueuesForEventCommand = plans.getQueuesForEvent(config.schema)
61
+ this.createQueueCommand = plans.createQueue(config.schema)
62
+ this.updateQueueCommand = plans.updateQueue(config.schema)
63
+ this.purgeQueueCommand = plans.purgeQueue(config.schema)
64
+ this.deleteQueueCommand = plans.deleteQueue(config.schema)
65
+ this.clearStorageCommand = plans.clearStorage(config.schema)
62
66
 
63
67
  // exported api to index
64
68
  this.functions = [
65
69
  this.complete,
66
70
  this.cancel,
67
71
  this.resume,
72
+ this.deleteJob,
68
73
  this.fail,
69
74
  this.fetch,
70
75
  this.work,
@@ -80,15 +85,14 @@ class Manager extends EventEmitter {
80
85
  this.sendAfter,
81
86
  this.createQueue,
82
87
  this.updateQueue,
83
- this.getQueue,
84
88
  this.deleteQueue,
85
89
  this.purgeQueue,
86
90
  this.getQueueSize,
91
+ this.getQueue,
92
+ this.getQueues,
87
93
  this.clearStorage,
88
94
  this.getJobById
89
95
  ]
90
-
91
- this.emitWipThrottled = debounce(() => this.emit(events.wip, this.getWipData()), WIP_EVENT_INTERVAL, WIP_DEBOUNCE_OPTIONS)
92
96
  }
93
97
 
94
98
  start () {
@@ -133,7 +137,12 @@ class Manager extends EventEmitter {
133
137
 
134
138
  emitWip (name) {
135
139
  if (!INTERNAL_QUEUES[name]) {
136
- this.emitWipThrottled()
140
+ const now = Date.now()
141
+
142
+ if (now - this.wipTs > 2000) {
143
+ this.emit(events.wip, this.getWipData())
144
+ this.wipTs = now
145
+ }
137
146
  }
138
147
  }
139
148
 
@@ -177,73 +186,35 @@ class Manager extends EventEmitter {
177
186
  }
178
187
 
179
188
  const {
180
- newJobCheckInterval: interval = this.config.newJobCheckInterval,
189
+ pollingInterval: interval = this.config.pollingInterval,
181
190
  batchSize,
182
- teamSize = 1,
183
- teamConcurrency = 1,
184
- teamRefill: refill = false,
185
191
  includeMetadata = false,
186
192
  priority = true
187
193
  } = options
188
194
 
189
195
  const id = randomUUID({ disableEntropyCache: true })
190
196
 
191
- let queueSize = 0
192
-
193
- let refillTeamPromise
194
- let resolveRefillTeam
195
-
196
- // Setup a promise that onFetch can await for when at least one
197
- // job is finished and so the team is ready to be topped up
198
- const createTeamRefillPromise = () => {
199
- refillTeamPromise = new Promise((resolve) => { resolveRefillTeam = resolve })
200
- }
201
-
202
- createTeamRefillPromise()
203
-
204
- const onRefill = () => {
205
- queueSize--
206
- resolveRefillTeam()
207
- createTeamRefillPromise()
208
- }
209
-
210
- const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata, priority })
197
+ const fetch = () => this.fetch(name, { batchSize, includeMetadata, priority })
211
198
 
212
199
  const onFetch = async (jobs) => {
200
+ if (!jobs.length) {
201
+ return
202
+ }
203
+
213
204
  if (this.config.__test__throw_worker) {
214
205
  throw new Error('__test__throw_worker')
215
206
  }
216
207
 
217
208
  this.emitWip(name)
218
209
 
219
- if (batchSize) {
220
- const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0)
221
-
222
- await resolveWithinSeconds(Promise.all([callback(jobs)]), maxExpiration)
223
- .then(() => this.complete(name, jobs.map(job => job.id)))
224
- .catch(err => this.fail(name, jobs.map(job => job.id), err))
225
- } else {
226
- if (refill) {
227
- queueSize += jobs.length || 1
228
- }
229
-
230
- const allTeamPromise = pMap(jobs, job =>
231
- resolveWithinSeconds(callback(job), job.expireInSeconds)
232
- .then(result => this.complete(name, job.id, result))
233
- .catch(err => this.fail(name, job.id, err))
234
- .then(() => refill ? onRefill() : null)
235
- , { concurrency: teamConcurrency }
236
- ).catch(() => {}) // allow promises & non-promises to live together in harmony
237
-
238
- if (refill) {
239
- if (queueSize < teamSize) {
240
- return
241
- } else {
242
- await refillTeamPromise
243
- }
244
- } else {
245
- await allTeamPromise
246
- }
210
+ const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0)
211
+ const jobIds = jobs.map(job => job.id)
212
+
213
+ try {
214
+ const result = await resolveWithinSeconds(callback(jobs), maxExpiration)
215
+ this.complete(name, jobIds, jobIds.length === 1 ? result : undefined)
216
+ } catch (err) {
217
+ this.fail(name, jobIds, err)
247
218
  }
248
219
 
249
220
  this.emitWip(name)
@@ -319,7 +290,7 @@ class Manager extends EventEmitter {
319
290
 
320
291
  const { rows } = await this.db.executeSql(this.getQueuesForEventCommand, [event])
321
292
 
322
- return await Promise.all(rows.map(({ name }) => this.send(name, ...args)))
293
+ await Promise.allSettled(rows.map(({ name }) => this.send(name, ...args)))
323
294
  }
324
295
 
325
296
  async send (...args) {
@@ -457,25 +428,20 @@ class Manager extends EventEmitter {
457
428
  return startAfter
458
429
  }
459
430
 
460
- async fetch (name, batchSize, options = {}) {
461
- const values = Attorney.checkFetchArgs(name, batchSize, options)
431
+ async fetch (name, options = {}) {
432
+ Attorney.checkFetchArgs(name, options)
462
433
  const db = options.db || this.db
463
434
  const nextJobSql = this.nextJobCommand({ ...options })
464
- const statementValues = [values.name, batchSize || 1]
465
435
 
466
436
  let result
467
437
 
468
438
  try {
469
- result = await db.executeSql(nextJobSql, statementValues)
439
+ result = await db.executeSql(nextJobSql, [name, options.batchSize])
470
440
  } catch (err) {
471
441
  // errors from fetchquery should only be unique constraint violations
472
442
  }
473
443
 
474
- if (!result || result.rows.length === 0) {
475
- return null
476
- }
477
-
478
- return result.rows.length === 1 && !batchSize ? result.rows[0] : result.rows
444
+ return result?.rows || []
479
445
  }
480
446
 
481
447
  mapCompletionIdArg (id, funcName) {
@@ -500,48 +466,56 @@ class Manager extends EventEmitter {
500
466
  return stringify(result)
501
467
  }
502
468
 
503
- mapCompletionResponse (ids, result) {
469
+ mapCommandResponse (ids, result) {
504
470
  return {
505
471
  jobs: ids,
506
472
  requested: ids.length,
507
- updated: result && result.rows ? parseInt(result.rows[0].count) : 0
473
+ affected: result && result.rows ? parseInt(result.rows[0].count) : 0
508
474
  }
509
475
  }
510
476
 
511
477
  async complete (name, id, data, options = {}) {
512
- assert(name, 'Missing queue name argument')
478
+ Attorney.assertQueueName(name)
513
479
  const db = options.db || this.db
514
480
  const ids = this.mapCompletionIdArg(id, 'complete')
515
481
  const result = await db.executeSql(this.completeJobsCommand, [name, ids, this.mapCompletionDataArg(data)])
516
- return this.mapCompletionResponse(ids, result)
482
+ return this.mapCommandResponse(ids, result)
517
483
  }
518
484
 
519
485
  async fail (name, id, data, options = {}) {
520
- assert(name, 'Missing queue name argument')
486
+ Attorney.assertQueueName(name)
521
487
  const db = options.db || this.db
522
488
  const ids = this.mapCompletionIdArg(id, 'fail')
523
489
  const result = await db.executeSql(this.failJobsByIdCommand, [name, ids, this.mapCompletionDataArg(data)])
524
- return this.mapCompletionResponse(ids, result)
490
+ return this.mapCommandResponse(ids, result)
525
491
  }
526
492
 
527
493
  async cancel (name, id, options = {}) {
528
- assert(name, 'Missing queue name argument')
494
+ Attorney.assertQueueName(name)
529
495
  const db = options.db || this.db
530
496
  const ids = this.mapCompletionIdArg(id, 'cancel')
531
497
  const result = await db.executeSql(this.cancelJobsCommand, [name, ids])
532
- return this.mapCompletionResponse(ids, result)
498
+ return this.mapCommandResponse(ids, result)
499
+ }
500
+
501
+ async deleteJob (name, id, options = {}) {
502
+ Attorney.assertQueueName(name)
503
+ const db = options.db || this.db
504
+ const ids = this.mapCompletionIdArg(id, 'deleteJob')
505
+ const result = await db.executeSql(this.deleteJobsCommand, [name, ids])
506
+ return this.mapCommandResponse(ids, result)
533
507
  }
534
508
 
535
509
  async resume (name, id, options = {}) {
536
- assert(name, 'Missing queue name argument')
510
+ Attorney.assertQueueName(name)
537
511
  const db = options.db || this.db
538
512
  const ids = this.mapCompletionIdArg(id, 'resume')
539
513
  const result = await db.executeSql(this.resumeJobsCommand, [name, ids])
540
- return this.mapCompletionResponse(ids, result)
514
+ return this.mapCommandResponse(ids, result)
541
515
  }
542
516
 
543
517
  async createQueue (name, options = {}) {
544
- assert(name, 'Missing queue name argument')
518
+ name = name || options.name
545
519
 
546
520
  Attorney.assertQueueName(name)
547
521
 
@@ -558,14 +532,12 @@ class Manager extends EventEmitter {
558
532
  deadLetter
559
533
  } = Attorney.checkQueueArgs(name, options)
560
534
 
561
- const paritionSql = plans.createPartition(this.config.schema, name)
562
-
563
- await this.db.executeSql(paritionSql)
564
-
565
- const sql = plans.createQueue(this.config.schema, name)
535
+ if (deadLetter) {
536
+ Attorney.assertQueueName(deadLetter)
537
+ }
566
538
 
567
- const params = [
568
- name,
539
+ // todo: pull in defaults from constructor config
540
+ const data = {
569
541
  policy,
570
542
  retryLimit,
571
543
  retryDelay,
@@ -573,13 +545,22 @@ class Manager extends EventEmitter {
573
545
  expireInSeconds,
574
546
  retentionMinutes,
575
547
  deadLetter
576
- ]
548
+ }
577
549
 
578
- await this.db.executeSql(sql, params)
550
+ await this.db.executeSql(this.createQueueCommand, [name, data])
551
+ }
552
+
553
+ async getQueues () {
554
+ const { rows } = await this.db.executeSql(this.getQueuesCommand)
555
+ return rows
579
556
  }
580
557
 
581
558
  async updateQueue (name, options = {}) {
582
- assert(name, 'Missing queue name argument')
559
+ Attorney.assertQueueName(name)
560
+
561
+ const { policy = QUEUE_POLICIES.standard } = options
562
+
563
+ assert(policy in QUEUE_POLICIES, `${policy} is not a valid queue policy`)
583
564
 
584
565
  const {
585
566
  retryLimit,
@@ -590,10 +571,9 @@ class Manager extends EventEmitter {
590
571
  deadLetter
591
572
  } = Attorney.checkQueueArgs(name, options)
592
573
 
593
- const sql = plans.updateQueue(this.config.schema)
594
-
595
574
  const params = [
596
575
  name,
576
+ policy,
597
577
  retryLimit,
598
578
  retryDelay,
599
579
  retryBackoff,
@@ -602,93 +582,61 @@ class Manager extends EventEmitter {
602
582
  deadLetter
603
583
  ]
604
584
 
605
- await this.db.executeSql(sql, params)
585
+ await this.db.executeSql(this.updateQueueCommand, params)
606
586
  }
607
587
 
608
588
  async getQueue (name) {
609
- assert(name, 'Missing queue name argument')
610
-
611
- const sql = plans.getQueueByName(this.config.schema)
612
- const result = await this.db.executeSql(sql, [name])
613
-
614
- if (result.rows.length === 0) {
615
- return null
616
- }
589
+ Attorney.assertQueueName(name)
617
590
 
618
- const {
619
- policy,
620
- retry_limit: retryLimit,
621
- retry_delay: retryDelay,
622
- retry_backoff: retryBackoff,
623
- expire_seconds: expireInSeconds,
624
- retention_minutes: retentionMinutes,
625
- dead_letter: deadLetter
626
- } = result.rows[0]
591
+ const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
627
592
 
628
- return {
629
- name,
630
- policy,
631
- retryLimit,
632
- retryDelay,
633
- retryBackoff,
634
- expireInSeconds,
635
- retentionMinutes,
636
- deadLetter
637
- }
593
+ return rows[0] || null
638
594
  }
639
595
 
640
596
  async deleteQueue (name) {
641
- assert(name, 'Missing queue name argument')
597
+ Attorney.assertQueueName(name)
642
598
 
643
- const queueSql = plans.getQueueByName(this.config.schema)
644
- const { rows } = await this.db.executeSql(queueSql, [name])
599
+ const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
645
600
 
646
- if (rows.length) {
647
- Attorney.assertQueueName(name)
648
- const sql = plans.dropPartition(this.config.schema, name)
649
- await this.db.executeSql(sql)
601
+ if (rows.length === 1) {
602
+ await this.db.executeSql(this.deleteQueueCommand, [name])
650
603
  }
651
-
652
- const sql = plans.deleteQueueRecords(this.config.schema)
653
- await this.db.executeSql(sql, [name])
654
604
  }
655
605
 
656
- async purgeQueue (queue) {
657
- assert(queue, 'Missing queue name argument')
658
- const sql = plans.purgeQueue(this.config.schema)
659
- await this.db.executeSql(sql, [queue])
606
+ async purgeQueue (name) {
607
+ Attorney.assertQueueName(name)
608
+ await this.db.executeSql(this.purgeQueueCommand, [name])
660
609
  }
661
610
 
662
611
  async clearStorage () {
663
- const sql = plans.clearStorage(this.config.schema)
664
- await this.db.executeSql(sql)
612
+ await this.db.executeSql(this.clearStorageCommand)
665
613
  }
666
614
 
667
- async getQueueSize (queue, options) {
668
- assert(queue, 'Missing queue name argument')
615
+ async getQueueSize (name, options) {
616
+ Attorney.assertQueueName(name)
669
617
 
670
618
  const sql = plans.getQueueSize(this.config.schema, options)
671
619
 
672
- const result = await this.db.executeSql(sql, [queue])
620
+ const result = await this.db.executeSql(sql, [name])
673
621
 
674
622
  return result ? parseFloat(result.rows[0].count) : null
675
623
  }
676
624
 
677
- async getJobById (queue, id, options = {}) {
678
- const db = options.db || this.db
679
- const result1 = await db.executeSql(this.getJobByIdCommand, [queue, id])
625
+ async getJobById (name, id, options = {}) {
626
+ Attorney.assertQueueName(name)
680
627
 
681
- if (result1 && result1.rows && result1.rows.length === 1) {
682
- return result1.rows[0]
683
- }
628
+ const db = options.db || this.db
684
629
 
685
- const result2 = await db.executeSql(this.getArchivedJobByIdCommand, [queue, id])
630
+ const result1 = await db.executeSql(this.getJobByIdCommand, [name, id])
686
631
 
687
- if (result2 && result2.rows && result2.rows.length === 1) {
688
- return result2.rows[0]
632
+ if (result1?.rows?.length === 1) {
633
+ return result1.rows[0]
634
+ } else if (options.includeArchive) {
635
+ const result2 = await db.executeSql(this.getArchivedJobByIdCommand, [name, id])
636
+ return result2?.rows[0] || null
637
+ } else {
638
+ return null
689
639
  }
690
-
691
- return null
692
640
  }
693
641
  }
694
642