pg-boss 10.0.0-beta16 → 10.0.0-beta18

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/README.md CHANGED
@@ -19,7 +19,7 @@ async function readme() {
19
19
 
20
20
  console.log(`created job ${id} in queue ${queue}`)
21
21
 
22
- await boss.work(queue, async job => {
22
+ await boss.work(queue, async ([ job ]) => {
23
23
  console.log(`received job ${job.id} with data ${JSON.stringify(job.data)}`)
24
24
  })
25
25
  }
@@ -47,7 +47,7 @@ This will likely cater the most to teams already familiar with the simplicity of
47
47
 
48
48
  ## Installation
49
49
 
50
- ``` bash
50
+ ```bash
51
51
  # npm
52
52
  npm install pg-boss
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.0.0-beta16",
3
+ "version": "10.0.0-beta18",
4
4
  "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
@@ -8,7 +8,6 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
11
- "p-map": "^4.0.0",
12
11
  "pg": "^8.5.1",
13
12
  "serialize-error": "^8.1.0"
14
13
  },
package/src/attorney.js CHANGED
@@ -124,24 +124,23 @@ function checkWorkArgs (name, args, defaults) {
124
124
 
125
125
  applyPollingInterval(options, defaults)
126
126
 
127
- assert(!('teamConcurrency' in options) ||
128
- (Number.isInteger(options.teamConcurrency) && options.teamConcurrency >= 1 && options.teamConcurrency <= 1000),
129
- 'teamConcurrency must be an integer between 1 and 1000')
130
-
131
- assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
132
127
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
133
128
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
129
+ assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean')
130
+
131
+ options.batchSize = options.batchSize || 1
134
132
 
135
133
  return { options, callback }
136
134
  }
137
135
 
138
- function checkFetchArgs (name, batchSize, options) {
136
+ function checkFetchArgs (name, options) {
139
137
  assert(name, 'missing queue name')
140
138
 
141
- assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
139
+ assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
142
140
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
141
+ assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean')
143
142
 
144
- return { name }
143
+ options.batchSize = options.batchSize || 1
145
144
  }
146
145
 
147
146
  function getConfig (value) {
package/src/manager.js CHANGED
@@ -2,7 +2,6 @@ const assert = require('assert')
2
2
  const EventEmitter = require('events')
3
3
  const { randomUUID } = require('crypto')
4
4
  const { serializeError: stringify } = require('serialize-error')
5
- const pMap = require('p-map')
6
5
  const { delay } = require('./tools')
7
6
  const Attorney = require('./attorney')
8
7
  const Worker = require('./worker')
@@ -50,27 +49,27 @@ class Manager extends EventEmitter {
50
49
  this.completeJobsCommand = plans.completeJobs(config.schema)
51
50
  this.cancelJobsCommand = plans.cancelJobs(config.schema)
52
51
  this.resumeJobsCommand = plans.resumeJobs(config.schema)
52
+ this.deleteJobsCommand = plans.deleteJobs(config.schema)
53
53
  this.failJobsByIdCommand = plans.failJobsById(config.schema)
54
54
  this.getJobByIdCommand = plans.getJobById(config.schema)
55
55
  this.getArchivedJobByIdCommand = plans.getArchivedJobById(config.schema)
56
56
  this.subscribeCommand = plans.subscribe(config.schema)
57
57
  this.unsubscribeCommand = plans.unsubscribe(config.schema)
58
58
  this.getQueuesCommand = plans.getQueues(config.schema)
59
- this.getQueuesForEventCommand = plans.getQueuesForEvent(config.schema)
60
59
  this.getQueueByNameCommand = plans.getQueueByName(config.schema)
61
- this.deleteQueueRecordsCommand = plans.deleteQueueRecords(config.schema)
62
- this.insertQueueCommand = plans.insertQueue(config.schema)
60
+ this.getQueuesForEventCommand = plans.getQueuesForEvent(config.schema)
61
+ this.createQueueCommand = plans.createQueue(config.schema)
63
62
  this.updateQueueCommand = plans.updateQueue(config.schema)
64
- this.createPartitionCommand = plans.createPartition(config.schema)
65
- this.dropPartitionCommand = plans.dropPartition(config.schema)
66
- this.clearStorageCommand = plans.clearStorage(config.schema)
67
63
  this.purgeQueueCommand = plans.purgeQueue(config.schema)
64
+ this.deleteQueueCommand = plans.deleteQueue(config.schema)
65
+ this.clearStorageCommand = plans.clearStorage(config.schema)
68
66
 
69
67
  // exported api to index
70
68
  this.functions = [
71
69
  this.complete,
72
70
  this.cancel,
73
71
  this.resume,
72
+ this.delete,
74
73
  this.fail,
75
74
  this.fetch,
76
75
  this.work,
@@ -189,71 +188,33 @@ class Manager extends EventEmitter {
189
188
  const {
190
189
  pollingInterval: interval = this.config.pollingInterval,
191
190
  batchSize,
192
- teamSize = 1,
193
- teamConcurrency = 1,
194
- teamRefill: refill = false,
195
191
  includeMetadata = false,
196
192
  priority = true
197
193
  } = options
198
194
 
199
195
  const id = randomUUID({ disableEntropyCache: true })
200
196
 
201
- let queueSize = 0
202
-
203
- let refillTeamPromise
204
- let resolveRefillTeam
205
-
206
- // Setup a promise that onFetch can await for when at least one
207
- // job is finished and so the team is ready to be topped up
208
- const createTeamRefillPromise = () => {
209
- refillTeamPromise = new Promise((resolve) => { resolveRefillTeam = resolve })
210
- }
211
-
212
- createTeamRefillPromise()
213
-
214
- const onRefill = () => {
215
- queueSize--
216
- resolveRefillTeam()
217
- createTeamRefillPromise()
218
- }
219
-
220
- const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata, priority })
197
+ const fetch = () => this.fetch(name, { batchSize, includeMetadata, priority })
221
198
 
222
199
  const onFetch = async (jobs) => {
200
+ if (!jobs.length) {
201
+ return
202
+ }
203
+
223
204
  if (this.config.__test__throw_worker) {
224
205
  throw new Error('__test__throw_worker')
225
206
  }
226
207
 
227
208
  this.emitWip(name)
228
209
 
229
- if (batchSize) {
230
- const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0)
231
-
232
- await resolveWithinSeconds(Promise.all([callback(jobs)]), maxExpiration)
233
- .then(() => this.complete(name, jobs.map(job => job.id)))
234
- .catch(err => this.fail(name, jobs.map(job => job.id), err))
235
- } else {
236
- if (refill) {
237
- queueSize += jobs.length || 1
238
- }
239
-
240
- const allTeamPromise = pMap(jobs, job =>
241
- resolveWithinSeconds(callback(job), job.expireInSeconds)
242
- .then(result => this.complete(name, job.id, result))
243
- .catch(err => this.fail(name, job.id, err))
244
- .then(() => refill ? onRefill() : null)
245
- , { concurrency: teamConcurrency }
246
- ).catch(() => {}) // allow promises & non-promises to live together in harmony
247
-
248
- if (refill) {
249
- if (queueSize < teamSize) {
250
- return
251
- } else {
252
- await refillTeamPromise
253
- }
254
- } else {
255
- await allTeamPromise
256
- }
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)
257
218
  }
258
219
 
259
220
  this.emitWip(name)
@@ -329,7 +290,7 @@ class Manager extends EventEmitter {
329
290
 
330
291
  const { rows } = await this.db.executeSql(this.getQueuesForEventCommand, [event])
331
292
 
332
- return await Promise.all(rows.map(({ name }) => this.send(name, ...args)))
293
+ await Promise.allSettled(rows.map(({ name }) => this.send(name, ...args)))
333
294
  }
334
295
 
335
296
  async send (...args) {
@@ -467,25 +428,20 @@ class Manager extends EventEmitter {
467
428
  return startAfter
468
429
  }
469
430
 
470
- async fetch (name, batchSize, options = {}) {
471
- const values = Attorney.checkFetchArgs(name, batchSize, options)
431
+ async fetch (name, options = {}) {
432
+ Attorney.checkFetchArgs(name, options)
472
433
  const db = options.db || this.db
473
434
  const nextJobSql = this.nextJobCommand({ ...options })
474
- const statementValues = [values.name, batchSize || 1]
475
435
 
476
436
  let result
477
437
 
478
438
  try {
479
- result = await db.executeSql(nextJobSql, statementValues)
439
+ result = await db.executeSql(nextJobSql, [name, options.batchSize])
480
440
  } catch (err) {
481
441
  // errors from fetchquery should only be unique constraint violations
482
442
  }
483
443
 
484
- if (!result || result.rows.length === 0) {
485
- return null
486
- }
487
-
488
- return result.rows.length === 1 && !batchSize ? result.rows[0] : result.rows
444
+ return result?.rows || []
489
445
  }
490
446
 
491
447
  mapCompletionIdArg (id, funcName) {
@@ -510,11 +466,11 @@ class Manager extends EventEmitter {
510
466
  return stringify(result)
511
467
  }
512
468
 
513
- mapCompletionResponse (ids, result) {
469
+ mapCommandResponse (ids, result) {
514
470
  return {
515
471
  jobs: ids,
516
472
  requested: ids.length,
517
- updated: result && result.rows ? parseInt(result.rows[0].count) : 0
473
+ affected: result && result.rows ? parseInt(result.rows[0].count) : 0
518
474
  }
519
475
  }
520
476
 
@@ -523,7 +479,7 @@ class Manager extends EventEmitter {
523
479
  const db = options.db || this.db
524
480
  const ids = this.mapCompletionIdArg(id, 'complete')
525
481
  const result = await db.executeSql(this.completeJobsCommand, [name, ids, this.mapCompletionDataArg(data)])
526
- return this.mapCompletionResponse(ids, result)
482
+ return this.mapCommandResponse(ids, result)
527
483
  }
528
484
 
529
485
  async fail (name, id, data, options = {}) {
@@ -531,7 +487,7 @@ class Manager extends EventEmitter {
531
487
  const db = options.db || this.db
532
488
  const ids = this.mapCompletionIdArg(id, 'fail')
533
489
  const result = await db.executeSql(this.failJobsByIdCommand, [name, ids, this.mapCompletionDataArg(data)])
534
- return this.mapCompletionResponse(ids, result)
490
+ return this.mapCommandResponse(ids, result)
535
491
  }
536
492
 
537
493
  async cancel (name, id, options = {}) {
@@ -539,7 +495,15 @@ class Manager extends EventEmitter {
539
495
  const db = options.db || this.db
540
496
  const ids = this.mapCompletionIdArg(id, 'cancel')
541
497
  const result = await db.executeSql(this.cancelJobsCommand, [name, ids])
542
- return this.mapCompletionResponse(ids, result)
498
+ return this.mapCommandResponse(ids, result)
499
+ }
500
+
501
+ async delete (name, id, options = {}) {
502
+ Attorney.assertQueueName(name)
503
+ const db = options.db || this.db
504
+ const ids = this.mapCompletionIdArg(id, 'delete')
505
+ const result = await db.executeSql(this.deleteJobsCommand, [name, ids])
506
+ return this.mapCommandResponse(ids, result)
543
507
  }
544
508
 
545
509
  async resume (name, id, options = {}) {
@@ -547,7 +511,7 @@ class Manager extends EventEmitter {
547
511
  const db = options.db || this.db
548
512
  const ids = this.mapCompletionIdArg(id, 'resume')
549
513
  const result = await db.executeSql(this.resumeJobsCommand, [name, ids])
550
- return this.mapCompletionResponse(ids, result)
514
+ return this.mapCommandResponse(ids, result)
551
515
  }
552
516
 
553
517
  async createQueue (name, options = {}) {
@@ -572,10 +536,8 @@ class Manager extends EventEmitter {
572
536
  Attorney.assertQueueName(deadLetter)
573
537
  }
574
538
 
575
- await this.db.executeSql(this.createPartitionCommand, [name])
576
-
577
- const params = [
578
- name,
539
+ // todo: pull in defaults from constructor config
540
+ const data = {
579
541
  policy,
580
542
  retryLimit,
581
543
  retryDelay,
@@ -583,9 +545,9 @@ class Manager extends EventEmitter {
583
545
  expireInSeconds,
584
546
  retentionMinutes,
585
547
  deadLetter
586
- ]
548
+ }
587
549
 
588
- await this.db.executeSql(this.insertQueueCommand, params)
550
+ await this.db.executeSql(this.createQueueCommand, [name, data])
589
551
  }
590
552
 
591
553
  async getQueues () {
@@ -626,32 +588,9 @@ class Manager extends EventEmitter {
626
588
  async getQueue (name) {
627
589
  Attorney.assertQueueName(name)
628
590
 
629
- const result = await this.db.executeSql(this.getQueueByNameCommand, [name])
630
-
631
- if (result.rows.length === 0) {
632
- return null
633
- }
634
-
635
- const {
636
- policy,
637
- retry_limit: retryLimit,
638
- retry_delay: retryDelay,
639
- retry_backoff: retryBackoff,
640
- expire_seconds: expireInSeconds,
641
- retention_minutes: retentionMinutes,
642
- dead_letter: deadLetter
643
- } = result.rows[0]
591
+ const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
644
592
 
645
- return {
646
- name,
647
- policy,
648
- retryLimit,
649
- retryDelay,
650
- retryBackoff,
651
- expireInSeconds,
652
- retentionMinutes,
653
- deadLetter
654
- }
593
+ return rows[0] || null
655
594
  }
656
595
 
657
596
  async deleteQueue (name) {
@@ -659,9 +598,8 @@ class Manager extends EventEmitter {
659
598
 
660
599
  const { rows } = await this.db.executeSql(this.getQueueByNameCommand, [name])
661
600
 
662
- if (rows.length) {
663
- await this.db.executeSql(this.dropPartitionCommand, [name])
664
- await this.db.executeSql(this.deleteQueueRecordsCommand, [name])
601
+ if (rows.length === 1) {
602
+ await this.db.executeSql(this.deleteQueueCommand, [name])
665
603
  }
666
604
  }
667
605
 
@@ -688,19 +626,17 @@ class Manager extends EventEmitter {
688
626
  Attorney.assertQueueName(name)
689
627
 
690
628
  const db = options.db || this.db
629
+
691
630
  const result1 = await db.executeSql(this.getJobByIdCommand, [name, id])
692
631
 
693
- if (result1 && result1.rows && result1.rows.length === 1) {
632
+ if (result1?.rows?.length === 1) {
694
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
695
639
  }
696
-
697
- const result2 = await db.executeSql(this.getArchivedJobByIdCommand, [name, id])
698
-
699
- if (result2 && result2.rows && result2.rows.length === 1) {
700
- return result2.rows[0]
701
- }
702
-
703
- return null
704
640
  }
705
641
  }
706
642
 
package/src/plans.js CHANGED
@@ -28,6 +28,7 @@ module.exports = {
28
28
  completeJobs,
29
29
  cancelJobs,
30
30
  resumeJobs,
31
+ deleteJobs,
31
32
  failJobsById,
32
33
  failJobsByTimeout,
33
34
  insertJob,
@@ -42,11 +43,9 @@ module.exports = {
42
43
  archive,
43
44
  drop,
44
45
  countStates,
45
- insertQueue,
46
46
  updateQueue,
47
- createPartition,
48
- dropPartition,
49
- deleteQueueRecords,
47
+ createQueue,
48
+ deleteQueue,
50
49
  getQueues,
51
50
  getQueueByName,
52
51
  getQueueSize,
@@ -85,9 +84,8 @@ function create (schema, version) {
85
84
  createColumnArchiveArchivedOn(schema),
86
85
  createIndexArchiveArchivedOn(schema),
87
86
 
88
- getPartitionFunction(schema),
89
- createPartitionFunction(schema),
90
- dropPartitionFunction(schema),
87
+ createQueueFunction(schema),
88
+ deleteQueueFunction(schema),
91
89
 
92
90
  insertVersion(schema, version)
93
91
  ]
@@ -137,8 +135,10 @@ function createTableQueue (schema) {
137
135
  retry_backoff bool,
138
136
  expire_seconds int,
139
137
  retention_minutes int,
140
- dead_letter text,
138
+ dead_letter text REFERENCES ${schema}.queue (name),
139
+ partition_name text,
141
140
  created_on timestamp with time zone not null default now(),
141
+ updated_on timestamp with time zone not null default now(),
142
142
  PRIMARY KEY (name)
143
143
  )
144
144
  `
@@ -199,7 +199,10 @@ function createTableJob (schema) {
199
199
  }
200
200
 
201
201
  const baseJobColumns = 'id, name, data, EXTRACT(epoch FROM expire_in) as "expireInSeconds"'
202
- const allJobColumns = `${baseJobColumns}, policy, state, priority,
202
+ const allJobColumns = `${baseJobColumns},
203
+ policy,
204
+ state,
205
+ priority,
203
206
  retry_limit as "retryLimit",
204
207
  retry_count as "retryCount",
205
208
  retry_delay as "retryDelay",
@@ -216,29 +219,38 @@ const allJobColumns = `${baseJobColumns}, policy, state, priority,
216
219
  output
217
220
  `
218
221
 
219
- function createPartition (schema) {
220
- return `SELECT ${schema}.create_partition($1)`
221
- }
222
-
223
- function getPartitionFunction (schema) {
224
- return `
225
- CREATE FUNCTION ${schema}.get_partition(queue_name text, out name text) AS
226
- $$
227
- SELECT 'j' || lower(left(regexp_replace(queue_name, '\\W', '', 'g'),10)) || left(encode(sha224(queue_name::bytea), 'hex'),10);
228
- $$
229
- LANGUAGE SQL
230
- IMMUTABLE
231
- `
232
- }
233
-
234
- function createPartitionFunction (schema) {
222
+ function createQueueFunction (schema) {
235
223
  return `
236
- CREATE FUNCTION ${schema}.create_partition(queue_name text)
224
+ CREATE FUNCTION ${schema}.create_queue(queue_name text, options json)
237
225
  RETURNS VOID AS
238
226
  $$
239
227
  DECLARE
240
- table_name varchar := ${schema}.get_partition(queue_name);
228
+ table_name varchar := 'j' || encode(sha224(queue_name::bytea), 'hex');
241
229
  BEGIN
230
+
231
+ INSERT INTO ${schema}.queue (
232
+ name,
233
+ policy,
234
+ retry_limit,
235
+ retry_delay,
236
+ retry_backoff,
237
+ expire_seconds,
238
+ retention_minutes,
239
+ dead_letter,
240
+ partition_name
241
+ )
242
+ VALUES (
243
+ queue_name,
244
+ options->>'policy',
245
+ (options->>'retryLimit')::int,
246
+ (options->>'retryDelay')::int,
247
+ (options->>'retryBackoff')::bool,
248
+ (options->>'expireInSeconds')::int,
249
+ (options->>'retentionMinutes')::int,
250
+ options->>'deadLetter',
251
+ table_name
252
+ );
253
+
242
254
  EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', table_name);
243
255
 
244
256
  EXECUTE format('${formatPartitionCommand(createPrimaryKeyJob(schema))}', table_name);
@@ -259,24 +271,37 @@ function createPartitionFunction (schema) {
259
271
  }
260
272
 
261
273
  function formatPartitionCommand (command) {
262
- return command.replace('.job', '.%1$I').replace('job_idx', '%1$s_idx').replaceAll('\'', '\'\'')
274
+ return command.replace('.job', '.%1$I').replace('job_i', '%1$s_i').replaceAll('\'', '\'\'')
263
275
  }
264
276
 
265
- function dropPartitionFunction (schema) {
277
+ function deleteQueueFunction (schema) {
266
278
  return `
267
- CREATE FUNCTION ${schema}.drop_partition(queue_name text)
279
+ CREATE FUNCTION ${schema}.delete_queue(queue_name text)
268
280
  RETURNS VOID AS
269
281
  $$
282
+ DECLARE
283
+ table_name varchar;
270
284
  BEGIN
271
- EXECUTE format('DROP TABLE IF EXISTS ${schema}.%I', ${schema}.get_partition(queue_name));
285
+ WITH deleted as (
286
+ DELETE FROM ${schema}.queue
287
+ WHERE name = queue_name
288
+ RETURNING partition_name
289
+ )
290
+ SELECT partition_name from deleted INTO table_name;
291
+
292
+ EXECUTE format('DROP TABLE IF EXISTS ${schema}.%I', table_name);
272
293
  END;
273
294
  $$
274
295
  LANGUAGE plpgsql;
275
296
  `
276
297
  }
277
298
 
278
- function dropPartition (schema) {
279
- return `SELECT ${schema}.drop_partition($1)`
299
+ function createQueue (schema) {
300
+ return `SELECT ${schema}.create_queue($1, $2)`
301
+ }
302
+
303
+ function deleteQueue (schema) {
304
+ return `SELECT ${schema}.delete_queue($1)`
280
305
  }
281
306
 
282
307
  function createPrimaryKeyJob (schema) {
@@ -284,11 +309,11 @@ function createPrimaryKeyJob (schema) {
284
309
  }
285
310
 
286
311
  function createQueueForeignKeyJob (schema) {
287
- return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT`
312
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`
288
313
  }
289
314
 
290
315
  function createQueueForeignKeyJobDeadLetter (schema) {
291
- return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT`
316
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`
292
317
  }
293
318
 
294
319
  function createPrimaryKeyArchive (schema) {
@@ -296,23 +321,23 @@ function createPrimaryKeyArchive (schema) {
296
321
  }
297
322
 
298
323
  function createIndexJobPolicyShort (schema) {
299
- return `CREATE UNIQUE INDEX job_idx_psh ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.created}' AND policy = '${QUEUE_POLICIES.short}';`
324
+ return `CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.created}' AND policy = '${QUEUE_POLICIES.short}';`
300
325
  }
301
326
 
302
327
  function createIndexJobPolicySingleton (schema) {
303
- return `CREATE UNIQUE INDEX job_idx_psi ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.singleton}'`
328
+ return `CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.singleton}'`
304
329
  }
305
330
 
306
331
  function createIndexJobPolicyStately (schema) {
307
- return `CREATE UNIQUE INDEX job_idx_pst ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.stately}'`
332
+ return `CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.stately}'`
308
333
  }
309
334
 
310
335
  function createIndexJobThrottle (schema) {
311
- return `CREATE UNIQUE INDEX job_idx_to ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> '${JOB_STATES.cancelled}' AND singleton_on IS NOT NULL`
336
+ return `CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> '${JOB_STATES.cancelled}' AND singleton_on IS NOT NULL`
312
337
  }
313
338
 
314
339
  function createIndexJobFetch (schema) {
315
- return `CREATE INDEX job_idx_f ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${JOB_STATES.active}'`
340
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${JOB_STATES.active}'`
316
341
  }
317
342
 
318
343
  function createTableArchive (schema) {
@@ -324,7 +349,7 @@ function createColumnArchiveArchivedOn (schema) {
324
349
  }
325
350
 
326
351
  function createIndexArchiveArchivedOn (schema) {
327
- return `CREATE INDEX archive_idx_ao ON ${schema}.archive(archived_on)`
352
+ return `CREATE INDEX archive_i1 ON ${schema}.archive(archived_on)`
328
353
  }
329
354
 
330
355
  function trySetMaintenanceTime (schema) {
@@ -347,13 +372,6 @@ function trySetTimestamp (schema, column) {
347
372
  `
348
373
  }
349
374
 
350
- function insertQueue (schema) {
351
- return `
352
- INSERT INTO ${schema}.queue (name, policy, retry_limit, retry_delay, retry_backoff, expire_seconds, retention_minutes, dead_letter)
353
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
354
- `
355
- }
356
-
357
375
  function updateQueue (schema) {
358
376
  return `
359
377
  UPDATE ${schema}.queue SET
@@ -363,21 +381,28 @@ function updateQueue (schema) {
363
381
  retry_backoff = COALESCE($5, retry_backoff),
364
382
  expire_seconds = COALESCE($6, expire_seconds),
365
383
  retention_minutes = COALESCE($7, retention_minutes),
366
- dead_letter = COALESCE($8, dead_letter)
384
+ dead_letter = COALESCE($8, dead_letter),
385
+ updated_on = now()
367
386
  WHERE name = $1
368
387
  `
369
388
  }
370
389
 
371
390
  function getQueues (schema) {
372
- return `SELECT * FROM ${schema}.queue`
391
+ return `
392
+ SELECT
393
+ policy,
394
+ retry_limit as "retryLimit",
395
+ retry_delay as "retryDelay",
396
+ retry_backoff as "retryBackoff",
397
+ expire_seconds as "expireInSeconds",
398
+ retention_minutes as "retentionMinutes",
399
+ dead_letter as "deadLetter"
400
+ FROM ${schema}.queue
401
+ `
373
402
  }
374
403
 
375
404
  function getQueueByName (schema) {
376
- return `SELECT * FROM ${schema}.queue WHERE name = $1`
377
- }
378
-
379
- function deleteQueueRecords (schema) {
380
- return `DELETE FROM ${schema}.queue WHERE name = $1`
405
+ return `${getQueues(schema)} WHERE name = $1`
381
406
  }
382
407
 
383
408
  function purgeQueue (schema) {
@@ -577,6 +602,19 @@ function resumeJobs (schema) {
577
602
  state = '${JOB_STATES.created}'
578
603
  WHERE name = $1
579
604
  AND id IN (SELECT UNNEST($2::uuid[]))
605
+ AND state = '${JOB_STATES.cancelled}'
606
+ RETURNING 1
607
+ )
608
+ SELECT COUNT(*) from results
609
+ `
610
+ }
611
+
612
+ function deleteJobs (schema) {
613
+ return `
614
+ with results as (
615
+ DELETE FROM ${schema}.job
616
+ WHERE name = $1
617
+ AND id IN (SELECT UNNEST($2::uuid[]))
580
618
  RETURNING 1
581
619
  )
582
620
  SELECT COUNT(*) from results
@@ -654,7 +692,7 @@ function insertJob (schema) {
654
692
  $17::int as retry_delay_default,
655
693
  $18::bool as retry_backoff,
656
694
  $19::bool as retry_backoff_default
657
- ) j LEFT JOIN ${schema}.queue q ON j.name = q.name
695
+ ) j JOIN ${schema}.queue q ON j.name = q.name
658
696
  ON CONFLICT DO NOTHING
659
697
  RETURNING id
660
698
  `
@@ -689,10 +727,10 @@ function insertJobs (schema) {
689
727
  COALESCE(id, gen_random_uuid()) as id,
690
728
  j.name,
691
729
  data,
692
- COALESCE(priority, 0),
693
- COALESCE("startAfter", now()),
694
- "singletonKey",
695
- COALESCE("deadLetter", q.dead_letter),
730
+ COALESCE(priority, 0) as priority,
731
+ j.start_after,
732
+ "singletonKey" as singleton_key,
733
+ COALESCE("deadLetter", q.dead_letter) as dead_letter,
696
734
  CASE
697
735
  WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds" * interval '1s'
698
736
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
@@ -701,7 +739,7 @@ function insertJobs (schema) {
701
739
  END as expire_in,
702
740
  CASE
703
741
  WHEN "keepUntil" IS NOT NULL THEN "keepUntil"
704
- ELSE COALESCE("startAfter", now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keep_until, '14 days') as interval)
742
+ ELSE COALESCE(j.start_after, now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keep_until, '14 days') as interval)
705
743
  END as keep_until,
706
744
  COALESCE("retryLimit", q.retry_limit, defaults.retry_limit, 2),
707
745
  CASE
@@ -711,21 +749,29 @@ function insertJobs (schema) {
711
749
  END as retry_delay,
712
750
  COALESCE("retryBackoff", q.retry_backoff, defaults.retry_backoff, false) as retry_backoff,
713
751
  q.policy
714
- FROM json_to_recordset($1) as j (
715
- id uuid,
716
- name text,
717
- priority integer,
718
- data jsonb,
719
- "startAfter" timestamp with time zone,
720
- "retryLimit" integer,
721
- "retryDelay" integer,
722
- "retryBackoff" boolean,
723
- "singletonKey" text,
724
- "expireInSeconds" integer,
725
- "keepUntil" timestamp with time zone,
726
- "deadLetter" text
727
- )
728
- LEFT JOIN ${schema}.queue q ON j.name = q.name,
752
+ FROM (
753
+ SELECT *,
754
+ CASE
755
+ WHEN right("startAfter", 1) = 'Z' THEN CAST("startAfter" as timestamp with time zone)
756
+ ELSE now() + CAST(COALESCE("startAfter",'0') as interval)
757
+ END as start_after
758
+ FROM json_to_recordset($1) as x (
759
+ id uuid,
760
+ name text,
761
+ priority integer,
762
+ data jsonb,
763
+ "startAfter" text,
764
+ "retryLimit" integer,
765
+ "retryDelay" integer,
766
+ "retryBackoff" boolean,
767
+ "singletonKey" text,
768
+ "singletonOn" text,
769
+ "expireInSeconds" integer,
770
+ "keepUntil" timestamp with time zone,
771
+ "deadLetter" text
772
+ )
773
+ ) j
774
+ JOIN ${schema}.queue q ON j.name = q.name,
729
775
  defaults
730
776
  ON CONFLICT DO NOTHING
731
777
  `
@@ -752,6 +798,7 @@ function archive (schema, completedInterval, failedInterval = completedInterval)
752
798
  INSERT INTO ${schema}.archive (${columns})
753
799
  SELECT ${columns}
754
800
  FROM archived_rows
801
+ ON CONFLICT DO NOTHING
755
802
  `
756
803
  }
757
804
 
@@ -779,7 +826,7 @@ function locked (schema, query) {
779
826
  }
780
827
 
781
828
  function advisoryLock (schema, key) {
782
- return `SELECT pg_advisory_xact_lock(
829
+ return `SELECT pg_advisory_xact_lock(
783
830
  ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ''}')::bytea), 'hex'))::bit(64)::bigint
784
831
  )`
785
832
  }
package/src/timekeeper.js CHANGED
@@ -2,13 +2,12 @@ const EventEmitter = require('events')
2
2
  const plans = require('./plans')
3
3
  const cronParser = require('cron-parser')
4
4
  const Attorney = require('./attorney')
5
- const pMap = require('p-map')
6
5
 
7
- const queues = {
6
+ const QUEUES = {
8
7
  SEND_IT: '__pgboss__send-it'
9
8
  }
10
9
 
11
- const events = {
10
+ const EVENTS = {
12
11
  error: 'error',
13
12
  schedule: 'schedule'
14
13
  }
@@ -24,7 +23,7 @@ class Timekeeper extends EventEmitter {
24
23
  this.cronMonitorIntervalMs = config.cronMonitorIntervalSeconds * 1000
25
24
  this.clockSkew = 0
26
25
 
27
- this.events = events
26
+ this.events = EVENTS
28
27
 
29
28
  this.getTimeCommand = plans.getTime(config.schema)
30
29
  this.getQueueCommand = plans.getQueueByName(config.schema)
@@ -53,16 +52,15 @@ class Timekeeper extends EventEmitter {
53
52
  await this.cacheClockSkew()
54
53
 
55
54
  try {
56
- await this.manager.createQueue(queues.SEND_IT)
55
+ await this.manager.createQueue(QUEUES.SEND_IT)
57
56
  } catch {}
58
57
 
59
58
  const options = {
60
59
  pollingIntervalSeconds: this.config.cronWorkerIntervalSeconds,
61
- teamSize: 50,
62
- teamConcurrency: 5
60
+ batchSize: 50
63
61
  }
64
62
 
65
- await this.manager.work(queues.SEND_IT, options, (job) => this.onSendIt(job))
63
+ await this.manager.work(QUEUES.SEND_IT, options, (jobs) => this.manager.insert(jobs.map(i => i.data)))
66
64
 
67
65
  setImmediate(() => this.onCron())
68
66
 
@@ -77,7 +75,7 @@ class Timekeeper extends EventEmitter {
77
75
 
78
76
  this.stopped = true
79
77
 
80
- await this.manager.offWork(queues.SEND_IT)
78
+ await this.manager.offWork(QUEUES.SEND_IT)
81
79
 
82
80
  if (this.skewMonitorInterval) {
83
81
  clearInterval(this.skewMonitorInterval)
@@ -141,12 +139,15 @@ class Timekeeper extends EventEmitter {
141
139
  }
142
140
 
143
141
  async cron () {
144
- const items = await this.getSchedules()
142
+ const schedules = await this.getSchedules()
145
143
 
146
- const sending = items.filter(i => this.shouldSendIt(i.cron, i.timezone))
144
+ const scheduled = schedules
145
+ .filter(i => this.shouldSendIt(i.cron, i.timezone))
146
+ .map(({ name, data, options }) =>
147
+ ({ name: QUEUES.SEND_IT, data: { name, data, options }, options: { singletonKey: name, singletonSeconds: 60 } }))
147
148
 
148
- if (sending.length && !this.stopped) {
149
- await pMap(sending, it => this.send(it), { concurrency: 5 })
149
+ if (scheduled.length > 0 && !this.stopped) {
150
+ await this.manager.insert(scheduled)
150
151
  }
151
152
  }
152
153
 
@@ -162,16 +163,6 @@ class Timekeeper extends EventEmitter {
162
163
  return prevDiff < 60
163
164
  }
164
165
 
165
- async send (job) {
166
- await this.manager.send(queues.SEND_IT, job, { singletonKey: job.name, singletonSeconds: 60 })
167
- }
168
-
169
- async onSendIt (job) {
170
- if (this.stopped) return
171
- const { name, data, options } = job.data
172
- await this.manager.send(name, data, options)
173
- }
174
-
175
166
  async getSchedules () {
176
167
  const { rows } = await this.db.executeSql(this.getSchedulesCommand)
177
168
  return rows
@@ -203,4 +194,4 @@ class Timekeeper extends EventEmitter {
203
194
  }
204
195
 
205
196
  module.exports = Timekeeper
206
- module.exports.QUEUES = queues
197
+ module.exports.QUEUES = QUEUES
package/types.d.ts CHANGED
@@ -114,7 +114,7 @@ declare namespace PgBoss {
114
114
 
115
115
  type QueuePolicy = 'standard' | 'short' | 'singleton' | 'stately'
116
116
 
117
- type Queue = RetryOptions & ExpirationOptions & RetentionOptions & { name: string, policy: QueuePolicy, deadLetter?: string }
117
+ type Queue = RetryOptions & ExpirationOptions & RetentionOptions & { name: string, policy?: QueuePolicy, deadLetter?: string }
118
118
 
119
119
  type ScheduleOptions = SendOptions & { tz?: string }
120
120
 
@@ -122,41 +122,20 @@ declare namespace PgBoss {
122
122
  pollingIntervalSeconds?: number;
123
123
  }
124
124
 
125
- interface CommonJobFetchOptions {
125
+ interface JobFetchOptions {
126
126
  includeMetadata?: boolean;
127
127
  priority?: boolean;
128
- }
129
-
130
- type JobFetchOptions = CommonJobFetchOptions & {
131
- teamSize?: number;
132
- teamConcurrency?: number;
133
- teamRefill?: boolean;
134
- }
135
-
136
- type BatchJobFetchOptions = CommonJobFetchOptions & {
137
- batchSize: number;
128
+ batchSize?: number;
138
129
  }
139
130
 
140
131
  type WorkOptions = JobFetchOptions & JobPollingOptions
141
- type BatchWorkOptions = BatchJobFetchOptions & JobPollingOptions
142
-
143
- type FetchOptions = {
144
- includeMetadata?: boolean;
145
- } & ConnectionOptions;
132
+ type FetchOptions = JobFetchOptions & ConnectionOptions;
146
133
 
147
134
  interface WorkHandler<ReqData> {
148
- (job: PgBoss.Job<ReqData>): Promise<any>;
149
- }
150
-
151
- interface BatchWorkHandler<ReqData> {
152
135
  (job: PgBoss.Job<ReqData>[]): Promise<any>;
153
136
  }
154
137
 
155
138
  interface WorkWithMetadataHandler<ReqData> {
156
- (job: PgBoss.JobWithMetadata<ReqData>): Promise<any>;
157
- }
158
-
159
- interface BatchWorkWithMetadataHandler<ReqData> {
160
139
  (job: PgBoss.JobWithMetadata<ReqData>[]): Promise<any>;
161
140
  }
162
141
 
@@ -328,13 +307,14 @@ declare class PgBoss extends EventEmitter {
328
307
  insert(jobs: PgBoss.JobInsert[]): Promise<void>;
329
308
  insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
330
309
 
310
+ fetch<T>(name: string): Promise<PgBoss.Job<T>[]>;
311
+ fetch<T>(name: string, options: PgBoss.FetchOptions & { includeMetadata: true }): Promise<PgBoss.JobWithMetadata<T>[]>;
312
+ fetch<T>(name: string, options: PgBoss.FetchOptions): Promise<PgBoss.Job<T>[]>;
313
+
331
314
  work<ReqData>(name: string, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
332
315
  work<ReqData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData>): Promise<string>;
333
316
  work<ReqData>(name: string, options: PgBoss.WorkOptions, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
334
317
 
335
- work<ReqData>(name: string, options: PgBoss.BatchWorkOptions & { includeMetadata: true }, handler: PgBoss.BatchWorkWithMetadataHandler<ReqData>): Promise<string>;
336
- work<ReqData>(name: string, options: PgBoss.BatchWorkOptions, handler: PgBoss.BatchWorkHandler<ReqData>): Promise<string>;
337
-
338
318
  offWork(name: string): Promise<void>;
339
319
  offWork(options: PgBoss.OffWorkOptions): Promise<void>;
340
320
 
@@ -342,14 +322,9 @@ declare class PgBoss extends EventEmitter {
342
322
 
343
323
  subscribe(event: string, name: string): Promise<void>;
344
324
  unsubscribe(event: string, name: string): Promise<void>;
345
- publish(event: string): Promise<string[]>;
346
- publish(event: string, data: object): Promise<string[]>;
347
- publish(event: string, data: object, options: PgBoss.SendOptions): Promise<string[]>;
348
-
349
- fetch<T>(name: string): Promise<PgBoss.Job<T> | null>;
350
- fetch<T>(name: string, batchSize: number): Promise<PgBoss.Job<T>[] | null>;
351
- fetch<T>(name: string, batchSize: number, options: PgBoss.FetchOptions & { includeMetadata: true }): Promise<PgBoss.JobWithMetadata<T>[] | null>;
352
- fetch<T>(name: string, batchSize: number, options: PgBoss.FetchOptions): Promise<PgBoss.Job<T>[] | null>;
325
+ publish(event: string): Promise<void>;
326
+ publish(event: string, data: object): Promise<void>;
327
+ publish(event: string, data: object, options: PgBoss.SendOptions): Promise<void>;
353
328
 
354
329
  cancel(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
355
330
  cancel(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
@@ -357,6 +332,9 @@ declare class PgBoss extends EventEmitter {
357
332
  resume(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
358
333
  resume(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
359
334
 
335
+ delete(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
336
+ delete(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
337
+
360
338
  complete(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
361
339
  complete(name: string, id: string, data: object, options?: PgBoss.ConnectionOptions): Promise<void>;
362
340
  complete(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
@@ -366,11 +344,11 @@ declare class PgBoss extends EventEmitter {
366
344
  fail(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
367
345
 
368
346
  getQueueSize(name: string, options?: object): Promise<number>;
369
- getJobById(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<PgBoss.JobWithMetadata | null>;
347
+ getJobById(name: string, id: string, options?: PgBoss.ConnectionOptions & { includeArchive: bool }): Promise<PgBoss.JobWithMetadata | null>;
370
348
 
371
349
  createQueue(name: string, options?: PgBoss.Queue): Promise<void>;
372
350
  getQueue(name: string): Promise<PgBoss.Queue | null>;
373
- getQueues(): Promise<[PgBoss.Queue]>;
351
+ getQueues(): Promise<PgBoss.Queue[]>;
374
352
  updateQueue(name: string, options?: PgBoss.Queue): Promise<void>;
375
353
  deleteQueue(name: string): Promise<void>;
376
354
  purgeQueue(name: string): Promise<void>;