pg-boss 10.0.0-beta2 → 10.0.0-beta21

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,22 +1,17 @@
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')
10
8
  const plans = require('./plans')
11
9
 
12
10
  const { QUEUES: TIMEKEEPER_QUEUES } = require('./timekeeper')
13
- const { QUEUE_POLICY } = plans
11
+ 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.expire_in_seconds), 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.expire_in_seconds)
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)
@@ -317,13 +288,9 @@ class Manager extends EventEmitter {
317
288
  async publish (event, ...args) {
318
289
  assert(event, 'Missing required argument')
319
290
 
320
- const result = await this.db.executeSql(this.getQueuesForEventCommand, [event])
291
+ const { rows } = await this.db.executeSql(this.getQueuesForEventCommand, [event])
321
292
 
322
- if (!result || result.rowCount === 0) {
323
- return []
324
- }
325
-
326
- return await Promise.all(result.rows.map(({ name }) => this.send(name, ...args)))
293
+ await Promise.allSettled(rows.map(({ name }) => this.send(name, ...args)))
327
294
  }
328
295
 
329
296
  async send (...args) {
@@ -406,10 +373,10 @@ class Manager extends EventEmitter {
406
373
  ]
407
374
 
408
375
  const db = wrapper || this.db
409
- const result = await db.executeSql(this.insertJobCommand, values)
376
+ const { rows } = await db.executeSql(this.insertJobCommand, values)
410
377
 
411
- if (result && result.rowCount === 1) {
412
- return result.rows[0].id
378
+ if (rows.length === 1) {
379
+ return rows[0].id
413
380
  }
414
381
 
415
382
  if (!options.singletonNextSlot) {
@@ -461,25 +428,20 @@ class Manager extends EventEmitter {
461
428
  return startAfter
462
429
  }
463
430
 
464
- async fetch (name, batchSize, options = {}) {
465
- const values = Attorney.checkFetchArgs(name, batchSize, options)
431
+ async fetch (name, options = {}) {
432
+ Attorney.checkFetchArgs(name, options)
466
433
  const db = options.db || this.db
467
434
  const nextJobSql = this.nextJobCommand({ ...options })
468
- const statementValues = [values.name, batchSize || 1]
469
435
 
470
436
  let result
471
437
 
472
438
  try {
473
- result = await db.executeSql(nextJobSql, statementValues)
439
+ result = await db.executeSql(nextJobSql, [name, options.batchSize])
474
440
  } catch (err) {
475
441
  // errors from fetchquery should only be unique constraint violations
476
442
  }
477
443
 
478
- if (!result || result.rows.length === 0) {
479
- return null
480
- }
481
-
482
- return result.rows.length === 1 && !batchSize ? result.rows[0] : result.rows
444
+ return result?.rows || []
483
445
  }
484
446
 
485
447
  mapCompletionIdArg (id, funcName) {
@@ -504,52 +466,62 @@ class Manager extends EventEmitter {
504
466
  return stringify(result)
505
467
  }
506
468
 
507
- mapCompletionResponse (ids, result) {
469
+ mapCommandResponse (ids, result) {
508
470
  return {
509
471
  jobs: ids,
510
472
  requested: ids.length,
511
- updated: result && result.rows ? parseInt(result.rows[0].count) : 0
473
+ affected: result && result.rows ? parseInt(result.rows[0].count) : 0
512
474
  }
513
475
  }
514
476
 
515
477
  async complete (name, id, data, options = {}) {
516
- assert(name, 'Missing queue name argument')
478
+ Attorney.assertQueueName(name)
517
479
  const db = options.db || this.db
518
480
  const ids = this.mapCompletionIdArg(id, 'complete')
519
481
  const result = await db.executeSql(this.completeJobsCommand, [name, ids, this.mapCompletionDataArg(data)])
520
- return this.mapCompletionResponse(ids, result)
482
+ return this.mapCommandResponse(ids, result)
521
483
  }
522
484
 
523
485
  async fail (name, id, data, options = {}) {
524
- assert(name, 'Missing queue name argument')
486
+ Attorney.assertQueueName(name)
525
487
  const db = options.db || this.db
526
488
  const ids = this.mapCompletionIdArg(id, 'fail')
527
489
  const result = await db.executeSql(this.failJobsByIdCommand, [name, ids, this.mapCompletionDataArg(data)])
528
- return this.mapCompletionResponse(ids, result)
490
+ return this.mapCommandResponse(ids, result)
529
491
  }
530
492
 
531
493
  async cancel (name, id, options = {}) {
532
- assert(name, 'Missing queue name argument')
494
+ Attorney.assertQueueName(name)
533
495
  const db = options.db || this.db
534
496
  const ids = this.mapCompletionIdArg(id, 'cancel')
535
497
  const result = await db.executeSql(this.cancelJobsCommand, [name, ids])
536
- 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)
537
507
  }
538
508
 
539
509
  async resume (name, id, options = {}) {
540
- assert(name, 'Missing queue name argument')
510
+ Attorney.assertQueueName(name)
541
511
  const db = options.db || this.db
542
512
  const ids = this.mapCompletionIdArg(id, 'resume')
543
513
  const result = await db.executeSql(this.resumeJobsCommand, [name, ids])
544
- return this.mapCompletionResponse(ids, result)
514
+ return this.mapCommandResponse(ids, result)
545
515
  }
546
516
 
547
517
  async createQueue (name, options = {}) {
548
- assert(name, 'Missing queue name argument')
518
+ name = name || options.name
549
519
 
550
- const { policy = QUEUE_POLICY.standard } = options
520
+ Attorney.assertQueueName(name)
551
521
 
552
- assert(policy in QUEUE_POLICY, `${policy} is not a valid queue policy`)
522
+ const { policy = QUEUE_POLICIES.standard } = options
523
+
524
+ assert(policy in QUEUE_POLICIES, `${policy} is not a valid queue policy`)
553
525
 
554
526
  const {
555
527
  retryLimit,
@@ -560,14 +532,12 @@ class Manager extends EventEmitter {
560
532
  deadLetter
561
533
  } = Attorney.checkQueueArgs(name, options)
562
534
 
563
- const paritionSql = plans.partitionCreateJobName(this.config.schema, name)
564
-
565
- await this.db.executeSql(paritionSql)
566
-
567
- const sql = plans.createQueue(this.config.schema, name)
535
+ if (deadLetter) {
536
+ Attorney.assertQueueName(deadLetter)
537
+ }
568
538
 
569
- const params = [
570
- name,
539
+ // todo: pull in defaults from constructor config
540
+ const data = {
571
541
  policy,
572
542
  retryLimit,
573
543
  retryDelay,
@@ -575,13 +545,22 @@ class Manager extends EventEmitter {
575
545
  expireInSeconds,
576
546
  retentionMinutes,
577
547
  deadLetter
578
- ]
548
+ }
579
549
 
580
- 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
581
556
  }
582
557
 
583
558
  async updateQueue (name, options = {}) {
584
- 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`)
585
564
 
586
565
  const {
587
566
  retryLimit,
@@ -592,10 +571,9 @@ class Manager extends EventEmitter {
592
571
  deadLetter
593
572
  } = Attorney.checkQueueArgs(name, options)
594
573
 
595
- const sql = plans.updateQueue(this.config.schema)
596
-
597
574
  const params = [
598
575
  name,
576
+ policy,
599
577
  retryLimit,
600
578
  retryDelay,
601
579
  retryBackoff,
@@ -604,94 +582,61 @@ class Manager extends EventEmitter {
604
582
  deadLetter
605
583
  ]
606
584
 
607
- await this.db.executeSql(sql, params)
585
+ await this.db.executeSql(this.updateQueueCommand, params)
608
586
  }
609
587
 
610
588
  async getQueue (name) {
611
- assert(name, 'Missing queue name argument')
612
-
613
- const sql = plans.getQueueByName(this.config.schema)
614
- const result = await this.db.executeSql(sql, [name])
589
+ Attorney.assertQueueName(name)
615
590
 
616
- if (result.rows.length === 0) {
617
- return null
618
- }
619
-
620
- const {
621
- policy,
622
- retry_limit: retryLimit,
623
- retry_delay: retryDelay,
624
- retry_backoff: retryBackoff,
625
- expire_seconds: expireInSeconds,
626
- retention_minutes: retentionMinutes,
627
- dead_letter: deadLetter
628
- } = result.rows[0]
591
+ const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
629
592
 
630
- return {
631
- name,
632
- policy,
633
- retryLimit,
634
- retryDelay,
635
- retryBackoff,
636
- expireInSeconds,
637
- retentionMinutes,
638
- deadLetter
639
- }
593
+ return rows[0] || null
640
594
  }
641
595
 
642
596
  async deleteQueue (name) {
643
- assert(name, 'Missing queue name argument')
597
+ Attorney.assertQueueName(name)
644
598
 
645
- const queueSql = plans.getQueueByName(this.config.schema)
646
- const result = await this.db.executeSql(queueSql, [name])
599
+ const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
647
600
 
648
- if (result?.rows?.length) {
649
- Attorney.assertPostgresObjectName(name)
650
- const sql = plans.dropJobTablePartition(this.config.schema, name)
651
- await this.db.executeSql(sql)
601
+ if (rows.length === 1) {
602
+ await this.db.executeSql(this.deleteQueueCommand, [name])
652
603
  }
653
-
654
- const sql = plans.deleteQueueRecords(this.config.schema)
655
- const result2 = await this.db.executeSql(sql, [name])
656
- return result2?.rowCount || null
657
604
  }
658
605
 
659
- async purgeQueue (queue) {
660
- assert(queue, 'Missing queue name argument')
661
- const sql = plans.purgeQueue(this.config.schema)
662
- await this.db.executeSql(sql, [queue])
606
+ async purgeQueue (name) {
607
+ Attorney.assertQueueName(name)
608
+ await this.db.executeSql(this.purgeQueueCommand, [name])
663
609
  }
664
610
 
665
611
  async clearStorage () {
666
- const sql = plans.clearStorage(this.config.schema)
667
- await this.db.executeSql(sql)
612
+ await this.db.executeSql(this.clearStorageCommand)
668
613
  }
669
614
 
670
- async getQueueSize (queue, options) {
671
- assert(queue, 'Missing queue name argument')
615
+ async getQueueSize (name, options) {
616
+ Attorney.assertQueueName(name)
672
617
 
673
618
  const sql = plans.getQueueSize(this.config.schema, options)
674
619
 
675
- const result = await this.db.executeSql(sql, [queue])
620
+ const result = await this.db.executeSql(sql, [name])
676
621
 
677
622
  return result ? parseFloat(result.rows[0].count) : null
678
623
  }
679
624
 
680
- async getJobById (queue, id, options = {}) {
681
- const db = options.db || this.db
682
- const result1 = await db.executeSql(this.getJobByIdCommand, [queue, id])
625
+ async getJobById (name, id, options = {}) {
626
+ Attorney.assertQueueName(name)
683
627
 
684
- if (result1 && result1.rows && result1.rows.length === 1) {
685
- return result1.rows[0]
686
- }
628
+ const db = options.db || this.db
687
629
 
688
- const result2 = await db.executeSql(this.getArchivedJobByIdCommand, [queue, id])
630
+ const result1 = await db.executeSql(this.getJobByIdCommand, [name, id])
689
631
 
690
- if (result2 && result2.rows && result2.rows.length === 1) {
691
- 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
692
639
  }
693
-
694
- return null
695
640
  }
696
641
  }
697
642