pg-boss 7.2.1 → 8.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/README.md CHANGED
@@ -50,7 +50,7 @@ This will likely cater the most to teams already familiar with the simplicity of
50
50
  * Automatic maintenance operations to manage table growth
51
51
 
52
52
  ## Requirements
53
- * Node 12 or higher
53
+ * Node 14 or higher
54
54
  * PostgreSQL 9.5 or higher
55
55
 
56
56
  ## Installation
package/package.json CHANGED
@@ -1,31 +1,34 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "7.2.1",
3
+ "version": "8.0.0",
4
4
  "description": "Queueing jobs in Node.js using PostgreSQL like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
7
- "node": ">=12.0.0"
7
+ "node": ">=14"
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
11
11
  "delay": "^5.0.0",
12
12
  "lodash.debounce": "^4.0.8",
13
- "p-map": "^4.0.0",
13
+ "p-map": "^5.3.0",
14
14
  "pg": "^8.5.1",
15
+ "serialize-error": "^11.0.0",
15
16
  "uuid": "^8.3.2"
16
17
  },
17
18
  "devDependencies": {
18
- "@types/node": "^17.0.2",
19
+ "@types/node": "^18.0.0",
19
20
  "coveralls": "^3.1.0",
20
- "luxon": "^2.0.1",
21
- "mocha": "^9.0.1",
21
+ "luxon": "^3.0.1",
22
+ "mocha": "^10.0.0",
22
23
  "nyc": "^15.1.0",
23
- "standard": "^16.0.3"
24
+ "standard": "^17.0.0"
24
25
  },
25
26
  "scripts": {
26
27
  "test": "standard && mocha",
27
28
  "cover": "nyc --reporter=text npm test",
28
- "forcover": "npm run cover && nyc report --reporter=text-lcov | coveralls",
29
+ "travis-cover": "nyc --reporter=text npm run travis-test",
30
+ "travis-test": "standard && mocha --jobs 0 --exit",
31
+ "forcover": "npm run travis-cover && nyc report --reporter=text-lcov | coveralls",
29
32
  "export-schema": "node ./scripts/construct.js",
30
33
  "export-migration": "node ./scripts/migrate.js",
31
34
  "export-rollback": "node ./scripts/rollback.js",
package/src/attorney.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const assert = require('assert')
2
- const { DEFAULT_SCHEMA } = require('./plans')
2
+ const { DEFAULT_SCHEMA, SINGLETON_QUEUE_KEY } = require('./plans')
3
3
 
4
4
  module.exports = {
5
5
  getConfig,
6
6
  checkSendArgs,
7
+ checkInsertArgs,
7
8
  checkWorkArgs,
8
9
  checkFetchArgs,
9
10
  warnClockSkew
@@ -60,6 +61,7 @@ function checkSendArgs (args, defaults) {
60
61
  applyExpirationConfig(options, defaults)
61
62
  applyRetentionConfig(options, defaults)
62
63
  applyCompletionConfig(options, defaults)
64
+ applySingletonKeyConfig(options)
63
65
 
64
66
  const { startAfter, singletonSeconds, singletonMinutes, singletonHours } = options
65
67
 
@@ -84,6 +86,22 @@ function checkSendArgs (args, defaults) {
84
86
  return { name, data, options }
85
87
  }
86
88
 
89
+ function checkInsertArgs (jobs) {
90
+ assert(Array.isArray(jobs), `jobs argument should be an array. Received '${typeof jobs}'`)
91
+ return jobs.map(job => {
92
+ job = { ...job }
93
+ applySingletonKeyConfig(job)
94
+ return job
95
+ })
96
+ }
97
+
98
+ function applySingletonKeyConfig (options) {
99
+ if (options.singletonKey && options.useSingletonQueue && options.singletonKey !== SINGLETON_QUEUE_KEY) {
100
+ options.singletonKey = SINGLETON_QUEUE_KEY + options.singletonKey
101
+ }
102
+ delete options.useSingletonQueue
103
+ }
104
+
87
105
  function checkWorkArgs (name, args, defaults) {
88
106
  let options, callback
89
107
 
package/src/index.js CHANGED
@@ -90,6 +90,8 @@ class PgBoss extends EventEmitter {
90
90
  }
91
91
 
92
92
  async start () {
93
+ const { serializeError } = await import('serialize-error')
94
+
93
95
  if (!this.stopped) {
94
96
  return this
95
97
  }
@@ -104,7 +106,7 @@ class PgBoss extends EventEmitter {
104
106
 
105
107
  this.started = true
106
108
 
107
- this.manager.start()
109
+ this.manager.start({ stringify: serializeError })
108
110
 
109
111
  if (!this.config.noSupervisor) {
110
112
  await this.boss.supervise()
package/src/manager.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const assert = require('assert')
2
2
  const EventEmitter = require('events')
3
- const pMap = require('p-map')
4
3
  const delay = require('delay')
5
4
  const uuid = require('uuid')
6
5
  const debounce = require('lodash.debounce')
@@ -45,6 +44,8 @@ class Manager extends EventEmitter {
45
44
  constructor (db, config) {
46
45
  super()
47
46
 
47
+ this.stringify = null
48
+
48
49
  this.config = config
49
50
  this.db = db
50
51
 
@@ -74,6 +75,7 @@ class Manager extends EventEmitter {
74
75
  this.fetchCompleted,
75
76
  this.work,
76
77
  this.offWork,
78
+ this.notifyWorker,
77
79
  this.onComplete,
78
80
  this.offComplete,
79
81
  this.publish,
@@ -96,7 +98,8 @@ class Manager extends EventEmitter {
96
98
  this.emitWipThrottled = debounce(() => this.emit(events.wip, this.getWipData()), WIP_EVENT_INTERVAL, WIP_DEBOUNCE_OPTIONS)
97
99
  }
98
100
 
99
- start () {
101
+ start ({ stringify }) {
102
+ this.stringify = stringify
100
103
  this.stopping = false
101
104
  }
102
105
 
@@ -210,6 +213,8 @@ class Manager extends EventEmitter {
210
213
  const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata })
211
214
 
212
215
  const onFetch = async (jobs) => {
216
+ const { default: pMap } = await import('p-map')
217
+
213
218
  if (this.config.__test__throw_worker) {
214
219
  throw new Error('__test__throw_worker')
215
220
  }
@@ -294,6 +299,12 @@ class Manager extends EventEmitter {
294
299
  })
295
300
  }
296
301
 
302
+ notifyWorker (workerId) {
303
+ if (this.workers.has(workerId)) {
304
+ this.workers.get(workerId).notify()
305
+ }
306
+ }
307
+
297
308
  async subscribe (event, name) {
298
309
  assert(event, 'Missing required argument')
299
310
  assert(name, 'Missing required argument')
@@ -386,6 +397,7 @@ class Manager extends EventEmitter {
386
397
 
387
398
  async createJob (name, data, options, singletonOffset = 0) {
388
399
  const {
400
+ db: wrapper,
389
401
  expireIn,
390
402
  priority,
391
403
  startAfter,
@@ -416,8 +428,8 @@ class Manager extends EventEmitter {
416
428
  keepUntil, // 13
417
429
  onComplete // 14
418
430
  ]
419
-
420
- const result = await this.db.executeSql(this.insertJobCommand, values)
431
+ const db = wrapper || this.db
432
+ const result = await db.executeSql(this.insertJobCommand, values)
421
433
 
422
434
  if (result && result.rowCount === 1) {
423
435
  return result.rows[0].id
@@ -438,10 +450,12 @@ class Manager extends EventEmitter {
438
450
  return await this.createJob(name, data, options, singletonOffset)
439
451
  }
440
452
 
441
- async insert (jobs) {
442
- assert(Array.isArray(jobs), `jobs argument should be an array. Received '${typeof jobs}'`)
443
- const data = JSON.stringify(jobs)
444
- return await this.db.executeSql(this.insertJobsCommand, [data])
453
+ async insert (jobs, options = {}) {
454
+ const { db: wrapper } = options
455
+ const db = wrapper || this.db
456
+ const checkedJobs = Attorney.checkInsertArgs(jobs)
457
+ const data = JSON.stringify(checkedJobs)
458
+ return await db.executeSql(this.insertJobsCommand, [data])
445
459
  }
446
460
 
447
461
  getDebounceStartAfter (singletonSeconds, clockOffset) {
@@ -463,8 +477,8 @@ class Manager extends EventEmitter {
463
477
 
464
478
  async fetch (name, batchSize, options = {}) {
465
479
  const values = Attorney.checkFetchArgs(name, batchSize, options)
466
-
467
- const result = await this.db.executeSql(
480
+ const db = options.db || this.db
481
+ const result = await db.executeSql(
468
482
  this.nextJobCommand(options.includeMetadata || false),
469
483
  [values.name, batchSize || 1]
470
484
  )
@@ -506,15 +520,11 @@ class Manager extends EventEmitter {
506
520
  mapCompletionDataArg (data) {
507
521
  if (data === null || typeof data === 'undefined' || typeof data === 'function') { return null }
508
522
 
509
- if (data instanceof Error) {
510
- const newData = {}
511
- Object.getOwnPropertyNames(data).forEach(key => { newData[key] = data[key] })
512
- data = newData
513
- }
514
-
515
- return (typeof data === 'object' && !Array.isArray(data))
523
+ const result = (typeof data === 'object' && !Array.isArray(data))
516
524
  ? data
517
525
  : { value: data }
526
+
527
+ return this.stringify(result)
518
528
  }
519
529
 
520
530
  mapCompletionResponse (ids, result) {
@@ -62,11 +62,25 @@ function migrate (value, version, migrations) {
62
62
  return flatten(schema, result.install, result.version)
63
63
  }
64
64
 
65
- function getAll (schema, config) {
66
- const DEFAULT_RETENTION = '30 days'
67
- const keepUntil = config ? config.keepUntil : DEFAULT_RETENTION
68
-
65
+ function getAll (schema) {
69
66
  return [
67
+ {
68
+ release: '7.4.0',
69
+ version: 20,
70
+ previous: 19,
71
+ install: [
72
+ `DROP INDEX ${schema}.job_singletonKey`,
73
+ `DROP INDEX ${schema}.job_singleton_queue`,
74
+ `CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL AND NOT singletonKey LIKE '\\_\\_pgboss\\_\\_singleton\\_queue%'`,
75
+ `CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < 'active' AND singletonOn IS NULL AND singletonKey LIKE '\\_\\_pgboss\\_\\_singleton\\_queue%'`
76
+ ],
77
+ uninstall: [
78
+ `DROP INDEX ${schema}.job_singletonKey`,
79
+ `DROP INDEX ${schema}.job_singleton_queue`,
80
+ `CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL AND NOT singletonKey = '__pgboss__singleton_queue'`,
81
+ `CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < 'active' AND singletonOn IS NULL AND singletonKey = '__pgboss__singleton_queue'`
82
+ ]
83
+ },
70
84
  {
71
85
  release: '7.0.0',
72
86
  version: 19,
@@ -154,218 +168,6 @@ function getAll (schema, config) {
154
168
  uninstall: [
155
169
  `ALTER TABLE ${schema}.version DROP COLUMN maintained_on`
156
170
  ]
157
- },
158
- {
159
- release: '5.0.0-beta1',
160
- version: 13,
161
- previous: 12,
162
- install: [
163
- `CREATE TABLE ${schema}.schedule (
164
- name text primary key,
165
- cron text not null,
166
- timezone text,
167
- data jsonb,
168
- options jsonb,
169
- created_on timestamp with time zone not null default now(),
170
- updated_on timestamp with time zone not null default now()
171
- )`
172
- ],
173
- uninstall: [
174
- `DROP TABLE ${schema}.schedule`
175
- ]
176
- },
177
- {
178
- release: '4.0.0',
179
- version: 12,
180
- previous: 11,
181
- install: [
182
- `ALTER TABLE ${schema}.version ALTER COLUMN version TYPE int USING version::int`,
183
- `ALTER TABLE ${schema}.job ADD COLUMN keepUntil timestamptz`,
184
- `ALTER TABLE ${schema}.archive ADD COLUMN keepUntil timestamptz`,
185
- `ALTER TABLE ${schema}.job ALTER COLUMN keepUntil SET DEFAULT now() + interval '${DEFAULT_RETENTION}'`,
186
- `UPDATE ${schema}.job SET keepUntil = startAfter + interval '${keepUntil}'`,
187
- `ALTER TABLE ${schema}.job ALTER COLUMN keepUntil SET NOT NULL`
188
- ],
189
- uninstall: [
190
- `ALTER TABLE ${schema}.version ALTER COLUMN version TYPE text USING version::text`,
191
- `ALTER TABLE ${schema}.job DROP COLUMN keepUntil`,
192
- `ALTER TABLE ${schema}.archive DROP COLUMN keepUntil`
193
- ]
194
- },
195
- {
196
- release: '3.2.0',
197
- version: 11,
198
- previous: 10,
199
- install: [
200
- `CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
201
- ],
202
- uninstall: [
203
- `DROP INDEX ${schema}.archive_archivedon_idx`
204
- ]
205
- },
206
- {
207
- release: '3.1.3',
208
- version: 10,
209
- previous: 9,
210
- install: [
211
- `CREATE INDEX archive_id_idx ON ${schema}.archive(id)`
212
- ],
213
- uninstall: [
214
- `DROP INDEX ${schema}.archive_id_idx`
215
- ]
216
- },
217
- {
218
- release: '3.1.0',
219
- version: 9,
220
- previous: 8,
221
- install: [
222
- `DROP INDEX ${schema}.job_fetch`,
223
- `DROP INDEX ${schema}.job_name`,
224
- `CREATE INDEX job_name ON ${schema}.job (name text_pattern_ops)`,
225
- `UPDATE ${schema}.job set name = '__state__completed__' || substr(name, 1, position('__state__completed' in name) - 1) WHERE name LIKE '%__state__completed'`
226
- ],
227
- uninstall: [
228
- `UPDATE ${schema}.job set name = substr(name, 21) || '__state__completed' WHERE name LIKE '__state__completed__%'`,
229
- `CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
230
- `DROP INDEX ${schema}.job_name`,
231
- `CREATE INDEX job_name ON ${schema}.job (name) WHERE state < 'active'`
232
- ]
233
- },
234
- {
235
- release: '3.0.0',
236
- version: 8,
237
- previous: 7,
238
- install: [
239
- 'CREATE EXTENSION IF NOT EXISTS pgcrypto',
240
- `ALTER TABLE ${schema}.job ALTER COLUMN id SET DEFAULT gen_random_uuid()`,
241
- `ALTER TABLE ${schema}.job ADD retryDelay integer not null DEFAULT (0)`,
242
- `ALTER TABLE ${schema}.job ADD retryBackoff boolean not null DEFAULT false`,
243
- `ALTER TABLE ${schema}.job ADD startAfter timestamp with time zone not null default now()`,
244
- `UPDATE ${schema}.job SET startAfter = createdOn + startIn`,
245
- `ALTER TABLE ${schema}.job DROP COLUMN startIn`,
246
- `UPDATE ${schema}.job SET expireIn = interval '15 minutes' WHERE expireIn IS NULL`,
247
- `ALTER TABLE ${schema}.job ALTER COLUMN expireIn SET NOT NULL`,
248
- `ALTER TABLE ${schema}.job ALTER COLUMN expireIn SET DEFAULT interval '15 minutes'`,
249
- // archive table schema changes
250
- `ALTER TABLE ${schema}.archive ADD retryDelay integer not null DEFAULT (0)`,
251
- `ALTER TABLE ${schema}.archive ADD retryBackoff boolean not null DEFAULT false`,
252
- `ALTER TABLE ${schema}.archive ADD startAfter timestamp with time zone`,
253
- `UPDATE ${schema}.archive SET startAfter = createdOn + startIn`,
254
- `ALTER TABLE ${schema}.archive DROP COLUMN startIn`,
255
- // rename complete to completed for state enum - can't use ALTER TYPE :(
256
- `DROP INDEX ${schema}.job_fetch`,
257
- `DROP INDEX ${schema}.job_singletonOn`,
258
- `DROP INDEX ${schema}.job_singletonKeyOn`,
259
- `DROP INDEX ${schema}.job_singletonKey`,
260
- `ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
261
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE text USING state::text`,
262
- `ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE text USING state::text`,
263
- `DROP TYPE ${schema}.job_state`,
264
- `CREATE TYPE ${schema}.job_state AS ENUM ('created', 'retry', 'active', 'completed', 'expired', 'cancelled', 'failed')`,
265
- `UPDATE ${schema}.job SET state = 'completed' WHERE state = 'complete'`,
266
- `UPDATE ${schema}.archive SET state = 'completed' WHERE state = 'complete'`,
267
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
268
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT 'created'`,
269
- `ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
270
- `CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
271
- `CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL`,
272
- `CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'`,
273
- `CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL`,
274
- // add new job name index
275
- `CREATE INDEX job_name ON ${schema}.job (name) WHERE state < 'active'`
276
- ],
277
- uninstall: [
278
- `ALTER TABLE ${schema}.job ALTER COLUMN id DROP DEFAULT`,
279
- // won't know if we should drop pgcrypto extension so it stays
280
- `ALTER TABLE ${schema}.job DROP COLUMN retryDelay`,
281
- `ALTER TABLE ${schema}.job DROP COLUMN retryBackoff`,
282
- `ALTER TABLE ${schema}.job DROP COLUMN startAfter`,
283
- `ALTER TABLE ${schema}.job ADD COLUMN startIn interval not null default(interval '0')`,
284
- // leaving migrated default data for expireIn
285
- `ALTER TABLE ${schema}.job ALTER COLUMN expireIn DROP NOT NULL`,
286
- `ALTER TABLE ${schema}.job ALTER COLUMN expireIn DROP DEFAULT`,
287
- // archive table restore
288
- `ALTER TABLE ${schema}.archive DROP COLUMN retryDelay`,
289
- `ALTER TABLE ${schema}.archive DROP COLUMN retryBackoff`,
290
- `ALTER TABLE ${schema}.archive DROP COLUMN startAfter`,
291
- `ALTER TABLE ${schema}.archive ADD COLUMN startIn interval`,
292
- // drop new job name index
293
- `DROP INDEX ${schema}.job_name`,
294
- // roll back to old enum def
295
- `DROP INDEX ${schema}.job_fetch`,
296
- `DROP INDEX ${schema}.job_singletonOn`,
297
- `DROP INDEX ${schema}.job_singletonKeyOn`,
298
- `DROP INDEX ${schema}.job_singletonKey`,
299
- `ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
300
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE text USING state::text`,
301
- `ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE text USING state::text`,
302
- `DROP TYPE ${schema}.job_state`,
303
- `CREATE TYPE ${schema}.job_state AS ENUM ('created', 'retry', 'active', 'complete', 'expired', 'cancelled', 'failed')`,
304
- `UPDATE ${schema}.job SET state = 'completed' WHERE state = 'complete'`,
305
- `UPDATE ${schema}.archive SET state = 'complete' WHERE state = 'completed'`,
306
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
307
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT 'created'`,
308
- `ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
309
- `CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
310
- `CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL`,
311
- `CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'`,
312
- `CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL`
313
- ]
314
- },
315
- {
316
- release: '2.5.0',
317
- version: 7,
318
- previous: 6,
319
- install: [
320
- `CREATE TABLE IF NOT EXISTS ${schema}.archive (LIKE ${schema}.job)`,
321
- `ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
322
- ],
323
- uninstall: [
324
- `DROP TABLE ${schema}.archive`
325
- ]
326
- },
327
- {
328
- release: '2.0.0',
329
- version: 6,
330
- previous: 5,
331
- install: [
332
- `CREATE INDEX job_fetch ON ${schema}.job (priority desc, createdOn, id) WHERE state < 'active'`
333
- ],
334
- uninstall: [
335
- `DROP INDEX ${schema}.job_fetch`
336
- ]
337
- },
338
- {
339
- release: '2.0.0',
340
- version: 5,
341
- previous: 4,
342
- install: [
343
- `ALTER TABLE ${schema}.job ALTER COLUMN startIn SET DEFAULT (interval '0')`,
344
- `ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT ('created')`,
345
- `UPDATE ${schema}.job SET name = left(name, -9) || '__state__expired' WHERE name LIKE '%__expired'`
346
- ],
347
- uninstall: [
348
- `ALTER TABLE ${schema}.job ALTER COLUMN startIn DROP DEFAULT`,
349
- `ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
350
- `UPDATE ${schema}.job SET name = left(name, -16) || '__expired' WHERE name LIKE '%__state__expired'`
351
- ]
352
- },
353
- {
354
- release: '1.1.0',
355
- version: 4,
356
- previous: 3,
357
- install: [
358
- `ALTER TABLE ${schema}.job ADD COLUMN priority integer not null default(0)`,
359
- `ALTER TABLE ${schema}.job ALTER COLUMN createdOn SET DATA TYPE timestamptz`,
360
- `ALTER TABLE ${schema}.job ALTER COLUMN startedOn SET DATA TYPE timestamptz`,
361
- `ALTER TABLE ${schema}.job ALTER COLUMN completedOn SET DATA TYPE timestamptz`
362
- ],
363
- uninstall: [
364
- `ALTER TABLE ${schema}.job DROP COLUMN priority`,
365
- `ALTER TABLE ${schema}.job ALTER COLUMN createdOn SET DATA TYPE timestamp`,
366
- `ALTER TABLE ${schema}.job ALTER COLUMN startedOn SET DATA TYPE timestamp`,
367
- `ALTER TABLE ${schema}.job ALTER COLUMN completedOn SET DATA TYPE timestamp`
368
- ]
369
171
  }
370
172
  ]
371
173
  }
package/src/plans.js CHANGED
@@ -13,6 +13,7 @@ const states = {
13
13
  const DEFAULT_SCHEMA = 'pgboss'
14
14
  const COMPLETION_JOB_PREFIX = `__state__${states.completed}__`
15
15
  const SINGLETON_QUEUE_KEY = '__pgboss__singleton_queue'
16
+ const SINGLETON_QUEUE_KEY_ESCAPED = SINGLETON_QUEUE_KEY.replace(/_/g, '\\_')
16
17
 
17
18
  const MIGRATE_RACE_MESSAGE = 'division by zero'
18
19
  const CREATE_RACE_MESSAGE = 'already exists'
@@ -68,6 +69,7 @@ function locked (schema, query) {
68
69
 
69
70
  return `
70
71
  BEGIN;
72
+ SET LOCAL statement_timeout = '30s';
71
73
  ${advisoryLock(schema)};
72
74
  ${query};
73
75
  COMMIT;
@@ -214,14 +216,14 @@ function getQueueSize (schema, options = {}) {
214
216
  function createIndexSingletonKey (schema) {
215
217
  // anything with singletonKey means "only 1 job can be queued or active at a time"
216
218
  return `
217
- CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < '${states.completed}' AND singletonOn IS NULL AND NOT singletonKey = '${SINGLETON_QUEUE_KEY}'
219
+ CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < '${states.completed}' AND singletonOn IS NULL AND NOT singletonKey LIKE '${SINGLETON_QUEUE_KEY_ESCAPED}%'
218
220
  `
219
221
  }
220
222
 
221
223
  function createIndexSingletonQueue (schema) {
222
224
  // "singleton queue" means "only 1 job can be queued at a time"
223
225
  return `
224
- CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < '${states.active}' AND singletonOn IS NULL AND singletonKey = '${SINGLETON_QUEUE_KEY}'
226
+ CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < '${states.active}' AND singletonOn IS NULL AND singletonKey LIKE '${SINGLETON_QUEUE_KEY_ESCAPED}%'
225
227
  `
226
228
  }
227
229
 
package/src/timekeeper.js CHANGED
@@ -1,4 +1,3 @@
1
- const pMap = require('p-map')
2
1
  const EventEmitter = require('events')
3
2
  const plans = require('./plans')
4
3
  const cronParser = require('cron-parser')
@@ -44,18 +43,23 @@ class Timekeeper extends EventEmitter {
44
43
  }
45
44
 
46
45
  async start () {
46
+ // setting the archive config too low breaks the cron 60s debounce interval so don't even try
47
47
  if (this.config.archiveSeconds < 60) {
48
48
  return
49
49
  }
50
50
 
51
+ // cache the clock skew from the db server
51
52
  await this.cacheClockSkew()
52
53
 
53
54
  await this.manager.work(queues.CRON, { newJobCheckIntervalSeconds: 4 }, (job) => this.onCron(job))
54
55
  await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: 4, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
55
56
 
56
- await this.cronMonitorAsync()
57
+ // uses sendDebounced() to enqueue a cron check
58
+ await this.checkSchedulesAsync()
57
59
 
60
+ // create monitoring interval to make sure cron hasn't crashed
58
61
  this.cronMonitorInterval = setInterval(async () => await this.monitorCron(), this.cronMonitorIntervalMs)
62
+ // create monitoring interval to measure and adjust for drift in clock skew
59
63
  this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
60
64
 
61
65
  this.stopped = false
@@ -86,7 +90,7 @@ class Timekeeper extends EventEmitter {
86
90
  const { secondsAgo } = await this.getCronTime()
87
91
 
88
92
  if (secondsAgo > 60) {
89
- await this.cronMonitorAsync()
93
+ await this.checkSchedulesAsync()
90
94
  }
91
95
  }
92
96
 
@@ -108,7 +112,7 @@ class Timekeeper extends EventEmitter {
108
112
  this.clockSkew = skew
109
113
  }
110
114
 
111
- async cronMonitorAsync () {
115
+ async checkSchedulesAsync () {
112
116
  const opts = {
113
117
  retryLimit: 2,
114
118
  retentionSeconds: 60,
@@ -121,6 +125,8 @@ class Timekeeper extends EventEmitter {
121
125
  async onCron () {
122
126
  if (this.stopped) return
123
127
 
128
+ const { default: pMap } = await import('p-map')
129
+
124
130
  try {
125
131
  if (this.config.__test__throw_clock_monitoring) {
126
132
  throw new Error('clock monitoring error')
@@ -136,6 +142,7 @@ class Timekeeper extends EventEmitter {
136
142
 
137
143
  if (this.stopped) return
138
144
 
145
+ // set last time cron was evaluated for downstream usage in cron monitoring
139
146
  await this.setCronTime()
140
147
  } catch (err) {
141
148
  this.emit(this.events.error, err)
@@ -143,7 +150,8 @@ class Timekeeper extends EventEmitter {
143
150
 
144
151
  if (this.stopped) return
145
152
 
146
- await this.cronMonitorAsync()
153
+ // uses sendDebounced() to enqueue a cron check
154
+ await this.checkSchedulesAsync()
147
155
  }
148
156
 
149
157
  shouldSendIt (cron, tz) {
package/src/worker.js CHANGED
@@ -27,6 +27,15 @@ class Worker {
27
27
  this.stopping = false
28
28
  this.stopped = false
29
29
  this.loopDelayPromise = null
30
+ this.beenNotified = false
31
+ }
32
+
33
+ notify () {
34
+ this.beenNotified = true
35
+
36
+ if (this.loopDelayPromise) {
37
+ this.loopDelayPromise.clear()
38
+ }
30
39
  }
31
40
 
32
41
  async start () {
@@ -36,6 +45,7 @@ class Worker {
36
45
  const started = Date.now()
37
46
 
38
47
  try {
48
+ this.beenNotified = false
39
49
  const jobs = await this.fetch()
40
50
 
41
51
  this.lastFetchedOn = Date.now()
@@ -64,7 +74,7 @@ class Worker {
64
74
 
65
75
  this.lastJobDuration = duration
66
76
 
67
- if (!this.stopping && duration < this.interval) {
77
+ if (!this.stopping && !this.beenNotified && duration < this.interval) {
68
78
  this.loopDelayPromise = delay(this.interval - duration)
69
79
  await this.loopDelayPromise
70
80
  this.loopDelayPromise = null
package/types.d.ts CHANGED
@@ -84,13 +84,20 @@ declare namespace PgBoss {
84
84
  priority?: number;
85
85
  startAfter?: number | string | Date;
86
86
  singletonKey?: string;
87
+ useSingletonQueue?: boolean;
87
88
  singletonSeconds?: number;
88
89
  singletonMinutes?: number;
89
90
  singletonHours?: number;
90
91
  singletonNextSlot?: boolean;
91
92
  }
92
93
 
93
- type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & CompletionOptions
94
+ interface ConnectionOptions {
95
+ db?: Db;
96
+ }
97
+
98
+ type InsertOptions = ConnectionOptions;
99
+
100
+ type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & CompletionOptions & ConnectionOptions;
94
101
 
95
102
  type ScheduleOptions = SendOptions & { tz?: string }
96
103
 
@@ -111,7 +118,7 @@ declare namespace PgBoss {
111
118
 
112
119
  type FetchOptions = {
113
120
  includeMetadata?: boolean;
114
- }
121
+ } & ConnectionOptions;
115
122
 
116
123
  interface WorkHandler<ReqData, ResData> {
117
124
  (job: PgBoss.JobWithDoneCallback<ReqData, ResData>): Promise<ResData> | void;
@@ -189,7 +196,7 @@ declare namespace PgBoss {
189
196
  retryLimit?: number;
190
197
  retryDelay?: number;
191
198
  retryBackoff?: boolean;
192
- startAfter?: Date | string;
199
+ startAfter?: Date | string;
193
200
  singletonKey?: string;
194
201
  expireInSeconds?: number;
195
202
  keepUntil?: Date | string;
@@ -297,6 +304,7 @@ declare class PgBoss extends EventEmitter {
297
304
  sendDebounced(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key: string): Promise<string | null>;
298
305
 
299
306
  insert(jobs: PgBoss.JobInsert[]): Promise<void>;
307
+ insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
300
308
 
301
309
  work<ReqData, ResData>(name: string, handler: PgBoss.WorkHandler<ReqData, ResData>): Promise<string>;
302
310
  work<ReqData, ResData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData, ResData>): Promise<string>;
@@ -308,6 +316,12 @@ declare class PgBoss extends EventEmitter {
308
316
  offWork(name: string): Promise<void>;
309
317
  offWork(options: PgBoss.OffWorkOptions): Promise<void>;
310
318
 
319
+ /**
320
+ * Notify worker that something has changed
321
+ * @param workerId
322
+ */
323
+ notifyWorker(workerId: string): void;
324
+
311
325
  subscribe(event: string, name: string): Promise<void>;
312
326
  unsubscribe(event: string, name: string): Promise<void>;
313
327
  publish(event: string): Promise<string[]>;
package/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "schema": 19
2
+ "schema": 20
3
3
  }