pg-boss 10.0.0-beta5 → 10.0.0-beta7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.0.0-beta5",
3
+ "version": "10.0.0-beta7",
4
4
  "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
package/src/attorney.js CHANGED
@@ -152,6 +152,10 @@ function getConfig (value) {
152
152
  ? { connectionString: value }
153
153
  : { ...value }
154
154
 
155
+ config.schedule = ('schedule' in config) ? config.schedule : true
156
+ config.supervise = ('supervise' in config) ? config.supervise : true
157
+ config.migrate = ('migrate' in config) ? config.migrate : true
158
+
155
159
  applySchemaConfig(config)
156
160
  applyMaintenanceConfig(config)
157
161
  applyArchiveConfig(config)
@@ -303,10 +307,6 @@ function applyMaintenanceConfig (config) {
303
307
  : 120
304
308
 
305
309
  assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
306
-
307
- config.schedule = ('schedule' in config) ? config.schedule : true
308
- config.supervise = ('supervise' in config) ? config.supervise : true
309
- config.migrate = ('migrate' in config) ? config.migrate : true
310
310
  }
311
311
 
312
312
  function applyDeleteConfig (config) {
package/src/boss.js CHANGED
@@ -24,10 +24,8 @@ class Boss extends EventEmitter {
24
24
  this.failJobsByTimeoutCommand = plans.locked(config.schema, plans.failJobsByTimeout(config.schema))
25
25
  this.archiveCommand = plans.locked(config.schema, plans.archive(config.schema, config.archiveInterval, config.archiveFailedInterval))
26
26
  this.dropCommand = plans.locked(config.schema, plans.drop(config.schema, config.deleteAfter))
27
- this.getMaintenanceTimeCommand = plans.getMaintenanceTime(config.schema)
28
- this.setMaintenanceTimeCommand = plans.setMaintenanceTime(config.schema)
29
- this.getMonitorTimeCommand = plans.getMonitorTime(config.schema)
30
- this.setMonitorTimeCommand = plans.setMonitorTime(config.schema)
27
+ this.trySetMaintenanceTimeCommand = plans.trySetMaintenanceTime(config.schema)
28
+ this.trySetMonitorTimeCommand = plans.trySetMonitorTime(config.schema)
31
29
  this.countStatesCommand = plans.countStates(config.schema)
32
30
 
33
31
  this.functions = [
@@ -48,8 +46,6 @@ class Boss extends EventEmitter {
48
46
  }
49
47
 
50
48
  async onMonitor () {
51
- let locker
52
-
53
49
  try {
54
50
  if (this.monitoring) {
55
51
  return
@@ -65,26 +61,24 @@ class Boss extends EventEmitter {
65
61
  throw new Error(this.config.__test__throw_monitor)
66
62
  }
67
63
 
68
- locker = await this.db.lock({ key: 'monitor' })
64
+ if (this.stopped) {
65
+ return
66
+ }
69
67
 
70
- const { secondsAgo } = await this.getMonitorTime()
68
+ const { rows } = await this.db.executeSql(this.trySetMonitorTimeCommand, [this.config.monitorStateIntervalSeconds])
71
69
 
72
- if (secondsAgo > this.monitorStateIntervalSeconds && !this.stopped) {
70
+ if (rows.length === 1 && !this.stopped) {
73
71
  const states = await this.countStates()
74
- this.setMonitorTime()
75
72
  this.emit(events.monitorStates, states)
76
73
  }
77
74
  } catch (err) {
78
75
  this.emit(events.error, err)
79
76
  } finally {
80
- await locker?.unlock()
81
77
  this.monitoring = false
82
78
  }
83
79
  }
84
80
 
85
81
  async onSupervise () {
86
- let locker
87
-
88
82
  try {
89
83
  if (this.maintaining) {
90
84
  return
@@ -105,18 +99,15 @@ class Boss extends EventEmitter {
105
99
  return
106
100
  }
107
101
 
108
- locker = await this.db.lock({ key: 'maintenance' })
109
-
110
- const { secondsAgo } = await this.getMaintenanceTime()
102
+ const { rows } = await this.db.executeSql(this.trySetMaintenanceTimeCommand, [this.config.maintenanceIntervalSeconds])
111
103
 
112
- if (secondsAgo > this.maintenanceIntervalSeconds) {
104
+ if (rows.length === 1 && !this.stopped) {
113
105
  const result = await this.maintain()
114
106
  this.emit(events.maintenance, result)
115
107
  }
116
108
  } catch (err) {
117
109
  this.emit(events.error, err)
118
110
  } finally {
119
- await locker?.unlock()
120
111
  this.maintaining = false
121
112
  }
122
113
  }
@@ -130,8 +121,6 @@ class Boss extends EventEmitter {
130
121
 
131
122
  const ended = Date.now()
132
123
 
133
- await this.setMaintenanceTime()
134
-
135
124
  return { ms: ended - started }
136
125
  }
137
126
 
package/src/contractor.js CHANGED
@@ -24,26 +24,26 @@ class Contractor {
24
24
 
25
25
  // exported api to index
26
26
  this.functions = [
27
- this.version,
27
+ this.schemaVersion,
28
28
  this.isInstalled
29
29
  ]
30
30
  }
31
31
 
32
- async version () {
32
+ async schemaVersion () {
33
33
  const result = await this.db.executeSql(plans.getVersion(this.config.schema))
34
34
  return result.rows.length ? parseInt(result.rows[0].version) : null
35
35
  }
36
36
 
37
37
  async isInstalled () {
38
38
  const result = await this.db.executeSql(plans.versionTableExists(this.config.schema))
39
- return result.rows.length ? result.rows[0].name : null
39
+ return !!result.rows[0].name
40
40
  }
41
41
 
42
42
  async start () {
43
43
  const installed = await this.isInstalled()
44
44
 
45
45
  if (installed) {
46
- const version = await this.version()
46
+ const version = await this.schemaVersion()
47
47
 
48
48
  if (schemaVersion > version) {
49
49
  throw new Error('Migrations are not supported to v10')
@@ -61,7 +61,7 @@ class Contractor {
61
61
  throw new Error('pg-boss is not installed')
62
62
  }
63
63
 
64
- const version = await this.version()
64
+ const version = await this.schemaVersion()
65
65
 
66
66
  if (schemaVersion !== version) {
67
67
  throw new Error('pg-boss database requires migrations')
package/src/db.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const EventEmitter = require('events')
2
2
  const pg = require('pg')
3
- const { advisoryLock } = require('./plans')
4
3
 
5
4
  class Db extends EventEmitter {
6
5
  constructor (config) {
@@ -43,31 +42,6 @@ class Db extends EventEmitter {
43
42
  return await this.pool.query(text, values)
44
43
  }
45
44
  }
46
-
47
- async lock ({ timeout = 30, key } = {}) {
48
- const lockedClient = await this.pool.connect()
49
-
50
- const query = `
51
- BEGIN;
52
- SET LOCAL lock_timeout = '${timeout}s';
53
- SET LOCAL idle_in_transaction_session_timeout = '${timeout}s';
54
- ${advisoryLock(this.config.schema, key)};
55
- `
56
-
57
- await lockedClient.query(query)
58
-
59
- const locker = {
60
- unlock: async function () {
61
- try {
62
- await lockedClient.query('COMMIT')
63
- } finally {
64
- lockedClient.release()
65
- }
66
- }
67
- }
68
-
69
- return locker
70
- }
71
45
  }
72
46
 
73
47
  module.exports = Db
package/src/manager.js CHANGED
@@ -217,7 +217,7 @@ class Manager extends EventEmitter {
217
217
  this.emitWip(name)
218
218
 
219
219
  if (batchSize) {
220
- const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expire_in_seconds), 0)
220
+ const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0)
221
221
 
222
222
  await resolveWithinSeconds(Promise.all([callback(jobs)]), maxExpiration)
223
223
  .then(() => this.complete(name, jobs.map(job => job.id)))
@@ -228,7 +228,7 @@ class Manager extends EventEmitter {
228
228
  }
229
229
 
230
230
  const allTeamPromise = pMap(jobs, job =>
231
- resolveWithinSeconds(callback(job), job.expire_in_seconds)
231
+ resolveWithinSeconds(callback(job), job.expireInSeconds)
232
232
  .then(result => this.complete(name, job.id, result))
233
233
  .catch(err => this.fail(name, job.id, err))
234
234
  .then(() => refill ? onRefill() : null)
@@ -317,13 +317,9 @@ class Manager extends EventEmitter {
317
317
  async publish (event, ...args) {
318
318
  assert(event, 'Missing required argument')
319
319
 
320
- const result = await this.db.executeSql(this.getQueuesForEventCommand, [event])
320
+ const { rows } = await this.db.executeSql(this.getQueuesForEventCommand, [event])
321
321
 
322
- if (!result || result.rowCount === 0) {
323
- return []
324
- }
325
-
326
- return await Promise.all(result.rows.map(({ name }) => this.send(name, ...args)))
322
+ return await Promise.all(rows.map(({ name }) => this.send(name, ...args)))
327
323
  }
328
324
 
329
325
  async send (...args) {
@@ -406,10 +402,10 @@ class Manager extends EventEmitter {
406
402
  ]
407
403
 
408
404
  const db = wrapper || this.db
409
- const result = await db.executeSql(this.insertJobCommand, values)
405
+ const { rows } = await db.executeSql(this.insertJobCommand, values)
410
406
 
411
- if (result && result.rowCount === 1) {
412
- return result.rows[0].id
407
+ if (rows.length === 1) {
408
+ return rows[0].id
413
409
  }
414
410
 
415
411
  if (!options.singletonNextSlot) {
@@ -645,17 +641,16 @@ class Manager extends EventEmitter {
645
641
  assert(name, 'Missing queue name argument')
646
642
 
647
643
  const queueSql = plans.getQueueByName(this.config.schema)
648
- const result = await this.db.executeSql(queueSql, [name])
644
+ const { rows } = await this.db.executeSql(queueSql, [name])
649
645
 
650
- if (result?.rows?.length) {
646
+ if (rows.length) {
651
647
  Attorney.assertQueueName(name)
652
648
  const sql = plans.dropPartition(this.config.schema, name)
653
649
  await this.db.executeSql(sql)
654
650
  }
655
651
 
656
652
  const sql = plans.deleteQueueRecords(this.config.schema)
657
- const result2 = await this.db.executeSql(sql, [name])
658
- return result2?.rowCount || null
653
+ await this.db.executeSql(sql, [name])
659
654
  }
660
655
 
661
656
  async purgeQueue (queue) {
package/src/plans.js CHANGED
@@ -53,14 +53,10 @@ module.exports = {
53
53
  getQueueSize,
54
54
  purgeQueue,
55
55
  clearStorage,
56
- getMaintenanceTime,
57
- setMaintenanceTime,
58
- getMonitorTime,
59
- setMonitorTime,
60
- getCronTime,
61
- setCronTime,
56
+ trySetMaintenanceTime,
57
+ trySetMonitorTime,
58
+ trySetCronTime,
62
59
  locked,
63
- advisoryLock,
64
60
  assertMigration,
65
61
  getArchivedJobById,
66
62
  getJobById,
@@ -77,7 +73,6 @@ function create (schema, version) {
77
73
  createEnumJobState(schema),
78
74
 
79
75
  createTableJob(schema),
80
- createIndexJobName(schema),
81
76
  createIndexJobFetch(schema),
82
77
  createIndexJobPolicyStately(schema),
83
78
  createIndexJobPolicyShort(schema),
@@ -89,7 +84,6 @@ function create (schema, version) {
89
84
  createPrimaryKeyArchive(schema),
90
85
  createColumnArchiveArchivedOn(schema),
91
86
  createIndexArchiveArchivedOn(schema),
92
- createIndexArchiveName(schema),
93
87
 
94
88
  createTableVersion(schema),
95
89
  createTableQueue(schema),
@@ -146,26 +140,44 @@ function createTableJob (schema) {
146
140
  priority integer not null default(0),
147
141
  data jsonb,
148
142
  state ${schema}.job_state not null default('${states.created}'),
149
- retryLimit integer not null default(0),
150
- retryCount integer not null default(0),
151
- retryDelay integer not null default(0),
152
- retryBackoff boolean not null default false,
153
- startAfter timestamp with time zone not null default now(),
154
- startedOn timestamp with time zone,
155
- singletonKey text,
156
- singletonOn timestamp without time zone,
157
- expireIn interval not null default interval '15 minutes',
158
- createdOn timestamp with time zone not null default now(),
159
- completedOn timestamp with time zone,
160
- keepUntil timestamp with time zone NOT NULL default now() + interval '14 days',
143
+ retry_limit integer not null default(0),
144
+ retry_count integer not null default(0),
145
+ retry_delay integer not null default(0),
146
+ retry_backoff boolean not null default false,
147
+ start_after timestamp with time zone not null default now(),
148
+ started_on timestamp with time zone,
149
+ singleton_key text,
150
+ singleton_on timestamp without time zone,
151
+ expire_in interval not null default interval '15 minutes',
152
+ created_on timestamp with time zone not null default now(),
153
+ completed_on timestamp with time zone,
154
+ keep_until timestamp with time zone NOT NULL default now() + interval '14 days',
161
155
  output jsonb,
162
- deadletter text,
156
+ dead_letter text,
163
157
  policy text,
164
158
  CONSTRAINT job_pkey PRIMARY KEY (name, id)
165
159
  ) PARTITION BY LIST (name)
166
160
  `
167
161
  }
168
162
 
163
+ const baseJobColumns = 'id, name, data, EXTRACT(epoch FROM expire_in) as "expireInSeconds"'
164
+ const allJobColumns = `${baseJobColumns}, policy, state, priority,
165
+ retry_limit as "retryLimit",
166
+ retry_count as "retryCount",
167
+ retry_delay as "retryDelay",
168
+ retry_backoff as "retryBackoff",
169
+ start_after as "startAfter",
170
+ started_on as "startedOn",
171
+ singleton_key as "singletonKey",
172
+ singleton_on as "singletonOn",
173
+ expire_in as "expireIn",
174
+ created_on as "createdOn",
175
+ completed_on as "completedOn",
176
+ keep_until as "keepUntil",
177
+ dead_letter as "deadLetter",
178
+ output
179
+ `
180
+
169
181
  function createPartition (schema, name) {
170
182
  return `SELECT ${schema}.create_partition('${name}');`
171
183
  }
@@ -232,19 +244,15 @@ function createIndexJobPolicyStately (schema) {
232
244
  }
233
245
 
234
246
  function createIndexJobThrottleOn (schema) {
235
- return `CREATE UNIQUE INDEX job_throttle_on ON ${schema}.job (name, singletonOn, COALESCE(singletonKey, '')) WHERE state <= '${states.completed}' AND singletonOn IS NOT NULL`
247
+ return `CREATE UNIQUE INDEX job_throttle_on ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <= '${states.completed}' AND singleton_on IS NOT NULL`
236
248
  }
237
249
 
238
250
  function createIndexJobThrottleKey (schema) {
239
- return `CREATE UNIQUE INDEX job_throttle_key ON ${schema}.job (name, singletonKey) WHERE state <= '${states.completed}' AND singletonOn IS NULL`
240
- }
241
-
242
- function createIndexJobName (schema) {
243
- return `CREATE INDEX job_name ON ${schema}.job (name)`
251
+ return `CREATE UNIQUE INDEX job_throttle_key ON ${schema}.job (name, singleton_key) WHERE state <= '${states.completed}' AND singleton_on IS NULL`
244
252
  }
245
253
 
246
254
  function createIndexJobFetch (schema) {
247
- return `CREATE INDEX job_fetch ON ${schema}.job (name, startAfter) INCLUDE (priority, createdOn, id) WHERE state < '${states.active}'`
255
+ return `CREATE INDEX job_fetch ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${states.active}'`
248
256
  }
249
257
 
250
258
  function createTableArchive (schema) {
@@ -252,40 +260,31 @@ function createTableArchive (schema) {
252
260
  }
253
261
 
254
262
  function createColumnArchiveArchivedOn (schema) {
255
- return `ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
263
+ return `ALTER TABLE ${schema}.archive ADD archived_on timestamptz NOT NULL DEFAULT now()`
256
264
  }
257
265
 
258
266
  function createIndexArchiveArchivedOn (schema) {
259
- return `CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
260
- }
261
-
262
- function createIndexArchiveName (schema) {
263
- return `CREATE INDEX archive_name_idx ON ${schema}.archive(name)`
264
- }
265
-
266
- function getMaintenanceTime (schema) {
267
- return `SELECT maintained_on, EXTRACT( EPOCH FROM (now() - maintained_on) ) seconds_ago FROM ${schema}.version`
268
- }
269
-
270
- function setMaintenanceTime (schema) {
271
- return `UPDATE ${schema}.version SET maintained_on = now()`
267
+ return `CREATE INDEX archive_archived_on_idx ON ${schema}.archive(archived_on)`
272
268
  }
273
269
 
274
- function getMonitorTime (schema) {
275
- return `SELECT monitored_on, EXTRACT( EPOCH FROM (now() - monitored_on) ) seconds_ago FROM ${schema}.version`
270
+ function trySetMaintenanceTime (schema) {
271
+ return trySetTimestamp(schema, 'maintained_on')
276
272
  }
277
273
 
278
- function setMonitorTime (schema) {
279
- return `UPDATE ${schema}.version SET monitored_on = now()`
274
+ function trySetMonitorTime (schema) {
275
+ return trySetTimestamp(schema, 'monitored_on')
280
276
  }
281
277
 
282
- function setCronTime (schema, time) {
283
- time = time || 'now()'
284
- return `UPDATE ${schema}.version SET cron_on = ${time}`
278
+ function trySetCronTime (schema) {
279
+ return trySetTimestamp(schema, 'cron_on')
285
280
  }
286
281
 
287
- function getCronTime (schema) {
288
- return `SELECT cron_on, EXTRACT( EPOCH FROM (now() - cron_on) ) seconds_ago FROM ${schema}.version`
282
+ function trySetTimestamp (schema, column) {
283
+ return `
284
+ UPDATE ${schema}.version SET ${column} = now()
285
+ WHERE EXTRACT( EPOCH FROM (now() - COALESCE(${column}, now() - interval '1 week') ) ) > $1
286
+ RETURNING true
287
+ `
289
288
  }
290
289
 
291
290
  function createQueue (schema) {
@@ -458,19 +457,18 @@ function fetchNextJob (schema) {
458
457
  FROM ${schema}.job
459
458
  WHERE name = $1
460
459
  AND state < '${states.active}'
461
- AND startAfter < now()
462
- ORDER BY ${priority && 'priority desc, '} createdOn, id
460
+ AND start_after < now()
461
+ ORDER BY ${priority && 'priority desc, '} created_on, id
463
462
  LIMIT $2
464
463
  FOR UPDATE SKIP LOCKED
465
464
  )
466
465
  UPDATE ${schema}.job j SET
467
466
  state = '${states.active}',
468
- startedOn = now(),
469
- retryCount = CASE WHEN startedOn IS NOT NULL THEN retryCount + 1 ELSE retryCount END
467
+ started_on = now(),
468
+ retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
470
469
  FROM next
471
470
  WHERE name = $1 AND j.id = next.id
472
- RETURNING ${includeMetadata ? 'j.*' : 'j.id, name, data'},
473
- EXTRACT(epoch FROM expireIn) as expire_in_seconds
471
+ RETURNING j.${includeMetadata ? allJobColumns : baseJobColumns}
474
472
  `
475
473
  }
476
474
 
@@ -478,7 +476,7 @@ function completeJobs (schema) {
478
476
  return `
479
477
  WITH results AS (
480
478
  UPDATE ${schema}.job
481
- SET completedOn = now(),
479
+ SET completed_on = now(),
482
480
  state = '${states.completed}',
483
481
  output = $3::jsonb
484
482
  WHERE name = $1
@@ -498,7 +496,7 @@ function failJobsById (schema) {
498
496
  }
499
497
 
500
498
  function failJobsByTimeout (schema) {
501
- const where = `state = '${states.active}' AND (startedOn + expireIn) < now()`
499
+ const where = `state = '${states.active}' AND (started_on + expire_in) < now()`
502
500
  const output = '\'{ "value": { "message": "job failed by timeout in active state" } }\'::jsonb'
503
501
  return failJobs(schema, where, output)
504
502
  }
@@ -508,36 +506,36 @@ function failJobs (schema, where, output) {
508
506
  WITH results AS (
509
507
  UPDATE ${schema}.job SET
510
508
  state = CASE
511
- WHEN retryCount < retryLimit THEN '${states.retry}'::${schema}.job_state
509
+ WHEN retry_count < retry_limit THEN '${states.retry}'::${schema}.job_state
512
510
  ELSE '${states.failed}'::${schema}.job_state
513
511
  END,
514
- completedOn = CASE
515
- WHEN retryCount < retryLimit THEN NULL
512
+ completed_on = CASE
513
+ WHEN retry_count < retry_limit THEN NULL
516
514
  ELSE now()
517
515
  END,
518
- startAfter = CASE
519
- WHEN retryCount = retryLimit THEN startAfter
520
- WHEN NOT retryBackoff THEN now() + retryDelay * interval '1'
516
+ start_after = CASE
517
+ WHEN retry_count = retry_limit THEN start_after
518
+ WHEN NOT retry_backoff THEN now() + retry_delay * interval '1'
521
519
  ELSE now() + (
522
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 +
523
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 * random()
520
+ retry_delay * 2 ^ LEAST(16, retry_count + 1) / 2 +
521
+ retry_delay * 2 ^ LEAST(16, retry_count + 1) / 2 * random()
524
522
  ) * interval '1'
525
523
  END,
526
524
  output = ${output}
527
525
  WHERE ${where}
528
526
  RETURNING *
529
527
  ), dlq_jobs as (
530
- INSERT INTO ${schema}.job (name, data, output, retryLimit, keepUntil)
528
+ INSERT INTO ${schema}.job (name, data, output, retry_limit, keep_until)
531
529
  SELECT
532
- deadletter,
530
+ dead_letter,
533
531
  data,
534
532
  output,
535
- retryLimit,
536
- keepUntil + (keepUntil - startAfter)
533
+ retry_limit,
534
+ keep_until + (keep_until - start_after)
537
535
  FROM results
538
536
  WHERE state = '${states.failed}'
539
- AND deadletter IS NOT NULL
540
- AND NOT name = deadletter
537
+ AND dead_letter IS NOT NULL
538
+ AND NOT name = dead_letter
541
539
  )
542
540
  SELECT COUNT(*) FROM results
543
541
  `
@@ -547,7 +545,7 @@ function cancelJobs (schema) {
547
545
  return `
548
546
  with results as (
549
547
  UPDATE ${schema}.job
550
- SET completedOn = now(),
548
+ SET completed_on = now(),
551
549
  state = '${states.cancelled}'
552
550
  WHERE name = $1
553
551
  AND id IN (SELECT UNNEST($2::uuid[]))
@@ -562,7 +560,7 @@ function resumeJobs (schema) {
562
560
  return `
563
561
  with results as (
564
562
  UPDATE ${schema}.job
565
- SET completedOn = NULL,
563
+ SET completed_on = NULL,
566
564
  state = '${states.created}'
567
565
  WHERE name = $1
568
566
  AND id IN (SELECT UNNEST($2::uuid[]))
@@ -579,15 +577,15 @@ function insertJob (schema) {
579
577
  name,
580
578
  data,
581
579
  priority,
582
- startAfter,
583
- singletonKey,
584
- singletonOn,
585
- deadletter,
586
- expireIn,
587
- keepUntil,
588
- retryLimit,
589
- retryDelay,
590
- retryBackoff,
580
+ start_after,
581
+ singleton_key,
582
+ singleton_on,
583
+ dead_letter,
584
+ expire_in,
585
+ keep_until,
586
+ retry_limit,
587
+ retry_delay,
588
+ retry_backoff,
591
589
  policy
592
590
  )
593
591
  SELECT
@@ -595,27 +593,27 @@ function insertJob (schema) {
595
593
  j.name,
596
594
  data,
597
595
  priority,
598
- startAfter,
599
- singletonKey,
600
- singletonOn,
601
- COALESCE(deadLetter, q.dead_letter) as deadletter,
596
+ start_after,
597
+ singleton_key,
598
+ singleton_on,
599
+ COALESCE(j.dead_letter, q.dead_letter) as dead_letter,
602
600
  CASE
603
- WHEN expireIn IS NOT NULL THEN CAST(expireIn as interval)
601
+ WHEN expire_in IS NOT NULL THEN CAST(expire_in as interval)
604
602
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
605
- WHEN expireInDefault IS NOT NULL THEN CAST(expireInDefault as interval)
603
+ WHEN expire_in_default IS NOT NULL THEN CAST(expire_in_default as interval)
606
604
  ELSE interval '15 minutes'
607
- END as expireIn,
605
+ END as expire_in,
608
606
  CASE
609
- WHEN right(keepUntil, 1) = 'Z' THEN CAST(keepUntil as timestamp with time zone)
610
- ELSE startAfter + CAST(COALESCE(keepUntil, (q.retention_minutes * 60)::text, keepUntilDefault, '14 days') as interval)
611
- END as keepUntil,
612
- COALESCE(retryLimit, q.retry_limit, retryLimitDefault, 2) as retryLimit,
607
+ WHEN right(keep_until, 1) = 'Z' THEN CAST(keep_until as timestamp with time zone)
608
+ ELSE start_after + CAST(COALESCE(keep_until, (q.retention_minutes * 60)::text, keep_until_default, '14 days') as interval)
609
+ END as keep_until,
610
+ COALESCE(j.retry_limit, q.retry_limit, retry_limit_default, 2) as retry_limit,
613
611
  CASE
614
- WHEN COALESCE(retryBackoff, q.retry_backoff, retryBackoffDefault, false)
615
- THEN GREATEST(COALESCE(retryDelay, q.retry_delay, retryDelayDefault), 1)
616
- ELSE COALESCE(retryDelay, q.retry_delay, retryDelayDefault, 0)
617
- END as retryDelay,
618
- COALESCE(retryBackoff, q.retry_backoff, retryBackoffDefault, false) as retryBackoff,
612
+ WHEN COALESCE(j.retry_backoff, q.retry_backoff, retry_backoff_default, false)
613
+ THEN GREATEST(COALESCE(j.retry_delay, q.retry_delay, retry_delay_default), 1)
614
+ ELSE COALESCE(j.retry_delay, q.retry_delay, retry_delay_default, 0)
615
+ END as retry_delay,
616
+ COALESCE(j.retry_backoff, q.retry_backoff, retry_backoff_default, false) as retry_backoff,
619
617
  q.policy
620
618
  FROM
621
619
  ( SELECT
@@ -626,23 +624,23 @@ function insertJob (schema) {
626
624
  CASE
627
625
  WHEN right($5, 1) = 'Z' THEN CAST($5 as timestamp with time zone)
628
626
  ELSE now() + CAST(COALESCE($5,'0') as interval)
629
- END as startAfter,
630
- $6 as singletonKey,
627
+ END as start_after,
628
+ $6 as singleton_key,
631
629
  CASE
632
630
  WHEN $7::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($7 * floor((date_part('epoch', now()) + $8) / $7))
633
631
  ELSE NULL
634
- END as singletonOn,
635
- $9 as deadletter,
636
- $10 as expireIn,
637
- $11 as expireInDefault,
638
- $12 as keepUntil,
639
- $13 as keepUntilDefault,
640
- $14::int as retryLimit,
641
- $15::int as retryLimitDefault,
642
- $16::int as retryDelay,
643
- $17::int as retryDelayDefault,
644
- $18::bool as retryBackoff,
645
- $19::bool as retryBackoffDefault
632
+ END as singleton_on,
633
+ $9 as dead_letter,
634
+ $10 as expire_in,
635
+ $11 as expire_in_default,
636
+ $12 as keep_until,
637
+ $13 as keep_until_default,
638
+ $14::int as retry_limit,
639
+ $15::int as retry_limit_default,
640
+ $16::int as retry_delay,
641
+ $17::int as retry_delay_default,
642
+ $18::bool as retry_backoff,
643
+ $19::bool as retry_backoff_default
646
644
  ) j LEFT JOIN ${schema}.queue q ON j.name = q.name
647
645
  ON CONFLICT DO NOTHING
648
646
  RETURNING id
@@ -653,25 +651,25 @@ function insertJobs (schema) {
653
651
  return `
654
652
  WITH defaults as (
655
653
  SELECT
656
- $2 as expireIn,
657
- $3 as keepUntil,
658
- $4::int as retryLimit,
659
- $5::int as retryDelay,
660
- $6::bool as retryBackoff
654
+ $2 as expire_in,
655
+ $3 as keep_until,
656
+ $4::int as retry_limit,
657
+ $5::int as retry_delay,
658
+ $6::bool as retry_backoff
661
659
  )
662
660
  INSERT INTO ${schema}.job (
663
661
  id,
664
662
  name,
665
663
  data,
666
664
  priority,
667
- startAfter,
668
- singletonKey,
669
- deadletter,
670
- expireIn,
671
- keepUntil,
672
- retryLimit,
673
- retryDelay,
674
- retryBackoff,
665
+ start_after,
666
+ singleton_key,
667
+ dead_letter,
668
+ expire_in,
669
+ keep_until,
670
+ retry_limit,
671
+ retry_delay,
672
+ retry_backoff,
675
673
  policy
676
674
  )
677
675
  SELECT
@@ -685,20 +683,20 @@ function insertJobs (schema) {
685
683
  CASE
686
684
  WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds" * interval '1s'
687
685
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
688
- WHEN defaults.expireIn IS NOT NULL THEN CAST(defaults.expireIn as interval)
686
+ WHEN defaults.expire_in IS NOT NULL THEN CAST(defaults.expire_in as interval)
689
687
  ELSE interval '15 minutes'
690
- END as expireIn,
688
+ END as expire_in,
691
689
  CASE
692
690
  WHEN "keepUntil" IS NOT NULL THEN "keepUntil"
693
- ELSE COALESCE("startAfter", now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keepUntil, '14 days') as interval)
694
- END as keepUntil,
695
- COALESCE("retryLimit", q.retry_limit, defaults.retryLimit, 2),
691
+ ELSE COALESCE("startAfter", now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keep_until, '14 days') as interval)
692
+ END as keep_until,
693
+ COALESCE("retryLimit", q.retry_limit, defaults.retry_limit, 2),
696
694
  CASE
697
- WHEN COALESCE("retryBackoff", q.retry_backoff, defaults.retryBackoff, false)
698
- THEN GREATEST(COALESCE("retryDelay", q.retry_delay, defaults.retryDelay), 1)
699
- ELSE COALESCE("retryDelay", q.retry_delay, defaults.retryDelay, 0)
700
- END as retryDelay,
701
- COALESCE("retryBackoff", q.retry_backoff, defaults.retryBackoff, false) as retryBackoff,
695
+ WHEN COALESCE("retryBackoff", q.retry_backoff, defaults.retry_backoff, false)
696
+ THEN GREATEST(COALESCE("retryDelay", q.retry_delay, defaults.retry_delay), 1)
697
+ ELSE COALESCE("retryDelay", q.retry_delay, defaults.retry_delay, 0)
698
+ END as retry_delay,
699
+ COALESCE("retryBackoff", q.retry_backoff, defaults.retry_backoff, false) as retry_backoff,
702
700
  q.policy
703
701
  FROM json_to_recordset($1) as j (
704
702
  id uuid,
@@ -723,24 +721,23 @@ function insertJobs (schema) {
723
721
  function drop (schema, interval) {
724
722
  return `
725
723
  DELETE FROM ${schema}.archive
726
- WHERE archivedOn < (now() - interval '${interval}')
724
+ WHERE archived_on < (now() - interval '${interval}')
727
725
  `
728
726
  }
729
727
 
730
728
  function archive (schema, completedInterval, failedInterval = completedInterval) {
729
+ const columns = 'id, name, priority, data, state, retry_limit, retry_count, retry_delay, retry_backoff, start_after, started_on, singleton_key, singleton_on, expire_in, created_on, completed_on, keep_until, dead_letter, policy, output'
730
+
731
731
  return `
732
732
  WITH archived_rows AS (
733
733
  DELETE FROM ${schema}.job
734
- WHERE (state <> '${states.failed}' AND completedOn < (now() - interval '${completedInterval}'))
735
- OR (state = '${states.failed}' AND completedOn < (now() - interval '${failedInterval}'))
736
- OR (state < '${states.active}' AND keepUntil < now())
734
+ WHERE (state <> '${states.failed}' AND completed_on < (now() - interval '${completedInterval}'))
735
+ OR (state = '${states.failed}' AND completed_on < (now() - interval '${failedInterval}'))
736
+ OR (state < '${states.active}' AND keep_until < now())
737
737
  RETURNING *
738
738
  )
739
- INSERT INTO ${schema}.archive (
740
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
741
- )
742
- SELECT
743
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
739
+ INSERT INTO ${schema}.archive (${columns})
740
+ SELECT ${columns}
744
741
  FROM archived_rows
745
742
  `
746
743
  }
@@ -761,6 +758,7 @@ function locked (schema, query) {
761
758
  return `
762
759
  BEGIN;
763
760
  SET LOCAL lock_timeout = '30s';
761
+ SET LOCAL idle_in_transaction_session_timeout = '30s';
764
762
  ${advisoryLock(schema)};
765
763
  ${query};
766
764
  COMMIT;
@@ -779,13 +777,13 @@ function assertMigration (schema, version) {
779
777
  }
780
778
 
781
779
  function getJobById (schema) {
782
- return getJobByTableAndId(schema, 'job')
780
+ return getJobByTableQueueId(schema, 'job')
783
781
  }
784
782
 
785
783
  function getArchivedJobById (schema) {
786
- return getJobByTableAndId(schema, 'archive')
784
+ return getJobByTableQueueId(schema, 'archive')
787
785
  }
788
786
 
789
- function getJobByTableAndId (schema, table) {
790
- return `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = $2`
787
+ function getJobByTableQueueId (schema, table) {
788
+ return `SELECT ${allJobColumns} FROM ${schema}.${table} WHERE name = $1 AND id = $2`
791
789
  }
package/src/timekeeper.js CHANGED
@@ -31,8 +31,7 @@ class Timekeeper extends EventEmitter {
31
31
  this.getSchedulesCommand = plans.getSchedules(config.schema)
32
32
  this.scheduleCommand = plans.schedule(config.schema)
33
33
  this.unscheduleCommand = plans.unschedule(config.schema)
34
- this.getCronTimeCommand = plans.getCronTime(config.schema)
35
- this.setCronTimeCommand = plans.setCronTime(config.schema)
34
+ this.trySetCronTimeCommand = plans.trySetCronTime(config.schema)
36
35
 
37
36
  this.functions = [
38
37
  this.schedule,
@@ -44,27 +43,30 @@ class Timekeeper extends EventEmitter {
44
43
  }
45
44
 
46
45
  async start () {
47
- this.stopped = false
48
-
49
46
  // setting the archive config too low breaks the cron 60s debounce interval so don't even try
50
47
  if (this.config.archiveSeconds < 60 || this.config.archiveFailedSeconds < 60) {
51
48
  return
52
49
  }
53
50
 
54
- // cache the clock skew from the db server
51
+ this.stopped = false
52
+
55
53
  await this.cacheClockSkew()
56
54
 
57
55
  try {
58
56
  await this.manager.createQueue(queues.SEND_IT)
59
57
  } catch {}
60
58
 
61
- await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
59
+ const options = {
60
+ newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds,
61
+ teamSize: 50,
62
+ teamConcurrency: 5
63
+ }
64
+
65
+ await this.manager.work(queues.SEND_IT, options, (job) => this.onSendIt(job))
62
66
 
63
67
  setImmediate(() => this.onCron())
64
68
 
65
- // create monitoring interval to make sure cron hasn't crashed
66
69
  this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.cronMonitorIntervalMs)
67
- // create monitoring interval to measure and adjust for drift in clock skew
68
70
  this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
69
71
  }
70
72
 
@@ -117,8 +119,6 @@ class Timekeeper extends EventEmitter {
117
119
  }
118
120
 
119
121
  async onCron () {
120
- let locker
121
-
122
122
  try {
123
123
  if (this.stopped || this.timekeeping) return
124
124
 
@@ -128,19 +128,15 @@ class Timekeeper extends EventEmitter {
128
128
 
129
129
  this.timekeeping = true
130
130
 
131
- locker = await this.db.lock({ key: 'timekeeper' })
132
-
133
- const { secondsAgo } = await this.getCronTime()
131
+ const { rows } = await this.db.executeSql(this.trySetCronTimeCommand, [this.config.cronMonitorIntervalSeconds])
134
132
 
135
- if (secondsAgo > this.config.cronMonitorIntervalSeconds) {
133
+ if (rows.length === 1 && !this.stopped) {
136
134
  await this.cron()
137
- await this.setCronTime()
138
135
  }
139
136
  } catch (err) {
140
137
  this.emit(this.events.error, err)
141
138
  } finally {
142
139
  this.timekeeping = false
143
- await locker?.unlock()
144
140
  }
145
141
  }
146
142
 
@@ -198,28 +194,11 @@ class Timekeeper extends EventEmitter {
198
194
 
199
195
  const values = [name, cron, tz, data, options]
200
196
 
201
- const result = await this.db.executeSql(this.scheduleCommand, values)
202
-
203
- return result ? result.rowCount : null
197
+ await this.db.executeSql(this.scheduleCommand, values)
204
198
  }
205
199
 
206
200
  async unschedule (name) {
207
- const result = await this.db.executeSql(this.unscheduleCommand, [name])
208
- return result ? result.rowCount : null
209
- }
210
-
211
- async setCronTime () {
212
- await this.db.executeSql(this.setCronTimeCommand)
213
- }
214
-
215
- async getCronTime () {
216
- const { rows } = await this.db.executeSql(this.getCronTimeCommand)
217
-
218
- let { cron_on: cronOn, seconds_ago: secondsAgo } = rows[0]
219
-
220
- secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 61
221
-
222
- return { cronOn, secondsAgo }
201
+ await this.db.executeSql(this.unscheduleCommand, [name])
223
202
  }
224
203
  }
225
204
 
package/types.d.ts CHANGED
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events'
2
2
 
3
3
  declare namespace PgBoss {
4
4
  interface Db {
5
- executeSql(text: string, values: any[]): Promise<{ rows: any[]; rowCount: number }>;
5
+ executeSql(text: string, values: any[]): Promise<{ rows: any[] }>;
6
6
  }
7
7
 
8
8
  interface DatabaseOptions {
@@ -91,10 +91,14 @@ declare namespace PgBoss {
91
91
  interface ConnectionOptions {
92
92
  db?: Db;
93
93
  }
94
-
94
+
95
95
  type InsertOptions = ConnectionOptions;
96
-
96
+
97
97
  type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & ConnectionOptions;
98
+
99
+ type QueuePolicy = 'standard' | 'short' | 'singleton' | 'stately'
100
+ type Queue = ExpirationOptions & RetentionOptions & RetryOptions & { policy: QueuePolicy }
101
+ type QueueUpdateOptions = ExpirationOptions & RetentionOptions & RetryOptions
98
102
 
99
103
  type ScheduleOptions = SendOptions & { tz?: string }
100
104
 
@@ -176,24 +180,26 @@ declare namespace PgBoss {
176
180
  data: T;
177
181
  }
178
182
 
179
- interface JobWithMetadata<T = object> extends Job<T> {
183
+ interface JobWithMetadata<T = object> {
184
+ id: string;
185
+ name: string;
186
+ data: T;
180
187
  priority: number;
181
188
  state: 'created' | 'retry' | 'active' | 'completed' | 'cancelled' | 'failed';
182
- retrylimit: number;
183
- retrycount: number;
184
- retrydelay: number;
185
- retrybackoff: boolean;
186
- startafter: Date;
187
- // This is nullable in the schema, but by the time this type is reified,
188
- // it will have been set.
189
- startedon: Date;
190
- singletonkey: string | null;
191
- singletonon: Date | null;
192
- expirein: PostgresInterval;
193
- createdon: Date;
194
- completedon: Date | null;
195
- keepuntil: Date;
196
- deadletter: string,
189
+ retryLimit: number;
190
+ retryCount: number;
191
+ retryDelay: number;
192
+ retryBackoff: boolean;
193
+ startAfter: Date;
194
+ startedOn: Date;
195
+ singletonKey: string | null;
196
+ singletonOn: Date | null;
197
+ expireIn: PostgresInterval;
198
+ createdOn: Date;
199
+ completedOn: Date | null;
200
+ keepUntil: Date;
201
+ deadLetter: string,
202
+ policy: QueuePolicy,
197
203
  output: object
198
204
  }
199
205
 
@@ -314,10 +320,6 @@ declare class PgBoss extends EventEmitter {
314
320
  offWork(name: string): Promise<void>;
315
321
  offWork(options: PgBoss.OffWorkOptions): Promise<void>;
316
322
 
317
- /**
318
- * Notify worker that something has changed
319
- * @param workerId
320
- */
321
323
  notifyWorker(workerId: string): void;
322
324
 
323
325
  subscribe(event: string, name: string): Promise<void>;
@@ -348,15 +350,19 @@ declare class PgBoss extends EventEmitter {
348
350
  getQueueSize(name: string, options?: object): Promise<number>;
349
351
  getJobById(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<PgBoss.JobWithMetadata | null>;
350
352
 
351
- createQueue(name: string, options?: { policy: 'standard' | 'short' | 'singleton' | 'stately' }): Promise<void>;
353
+ createQueue(name: string, options?: PgBoss.Queue): Promise<void>;
354
+ getQueue(name: string): Promise<PgBoss.Queue | null>;
355
+ updateQueue(name: string, options?: PgBoss.QueueUpdateOptions): Promise<void>;
352
356
  deleteQueue(name: string): Promise<void>;
353
357
  purgeQueue(name: string): Promise<void>;
354
- clearStorage(): Promise<void>;
355
358
 
359
+ clearStorage(): Promise<void>;
356
360
  archive(): Promise<void>;
357
361
  purge(): Promise<void>;
358
362
  expire(): Promise<void>;
359
363
  maintain(): Promise<void>;
364
+ isInstalled(): Promise<Boolean>;
365
+ schemaVersion(): Promise<Number>;
360
366
 
361
367
  schedule(name: string, cron: string, data?: object, options?: PgBoss.ScheduleOptions): Promise<void>;
362
368
  unschedule(name: string): Promise<void>;