pg-boss 10.0.0-beta3 → 10.0.0-beta5

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
@@ -9,24 +9,19 @@ async function readme() {
9
9
  const PgBoss = require('pg-boss');
10
10
  const boss = new PgBoss('postgres://user:pass@host/database');
11
11
 
12
- boss.on('error', error => console.error(error));
12
+ boss.on('error', console.error)
13
13
 
14
- await boss.start();
14
+ await boss.start()
15
15
 
16
- const queue = 'some-queue';
16
+ const queue = 'readme-queue'
17
17
 
18
- let jobId = await boss.send(queue, { param1: 'foo' })
18
+ const id = await boss.send(queue, { arg1: 'read me' })
19
19
 
20
- console.log(`created job in queue ${queue}: ${jobId}`);
20
+ console.log(`created job ${id} in queue ${queue}`)
21
21
 
22
- await boss.work(queue, someAsyncJobHandler);
23
- }
24
-
25
- async function someAsyncJobHandler(job) {
26
- console.log(`job ${job.id} received with data:`);
27
- console.log(JSON.stringify(job.data));
28
-
29
- await doSomethingAsyncWithThis(job.data);
22
+ await boss.work(queue, async job => {
23
+ console.log(`received job ${job.id} with data ${JSON.stringify(job.data)}`)
24
+ })
30
25
  }
31
26
  ```
32
27
 
@@ -48,7 +43,7 @@ This will likely cater the most to teams already familiar with the simplicity of
48
43
 
49
44
  ## Requirements
50
45
  * Node 20 or higher
51
- * PostgreSQL 12 or higher
46
+ * PostgreSQL 13 or higher
52
47
 
53
48
  ## Installation
54
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.0.0-beta3",
3
+ "version": "10.0.0-beta5",
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
@@ -368,21 +368,21 @@ function applyMonitoringConfig (config) {
368
368
  ? config.clockMonitorIntervalSeconds
369
369
  : TEN_MINUTES_IN_SECONDS
370
370
 
371
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 60),
372
- 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 60 seconds')
371
+ assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
372
+ 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
373
373
 
374
374
  config.cronMonitorIntervalSeconds =
375
375
  ('cronMonitorIntervalSeconds' in config)
376
376
  ? config.cronMonitorIntervalSeconds
377
- : 60
377
+ : 30
378
378
 
379
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 60),
380
- 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 60 seconds')
379
+ assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
380
+ 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
381
381
 
382
382
  config.cronWorkerIntervalSeconds =
383
383
  ('cronWorkerIntervalSeconds' in config)
384
384
  ? config.cronWorkerIntervalSeconds
385
- : 4
385
+ : 5
386
386
  }
387
387
 
388
388
  function warnClockSkew (message) {
package/src/boss.js CHANGED
@@ -77,10 +77,7 @@ class Boss extends EventEmitter {
77
77
  } catch (err) {
78
78
  this.emit(events.error, err)
79
79
  } finally {
80
- if (locker?.locked) {
81
- await locker.unlock()
82
- }
83
-
80
+ await locker?.unlock()
84
81
  this.monitoring = false
85
82
  }
86
83
  }
@@ -119,10 +116,7 @@ class Boss extends EventEmitter {
119
116
  } catch (err) {
120
117
  this.emit(events.error, err)
121
118
  } finally {
122
- if (locker?.locked) {
123
- await locker.unlock()
124
- }
125
-
119
+ await locker?.unlock()
126
120
  this.maintaining = false
127
121
  }
128
122
  }
package/src/db.js CHANGED
@@ -45,27 +45,23 @@ class Db extends EventEmitter {
45
45
  }
46
46
 
47
47
  async lock ({ timeout = 30, key } = {}) {
48
- // const lockedClient = new pg.Client(this.config)
49
- // await lockedClient.connect()
50
48
  const lockedClient = await this.pool.connect()
51
49
 
52
50
  const query = `
53
51
  BEGIN;
54
52
  SET LOCAL lock_timeout = '${timeout}s';
55
- SET LOCAL idle_in_transaction_session_timeout = '3600s';
53
+ SET LOCAL idle_in_transaction_session_timeout = '${timeout}s';
56
54
  ${advisoryLock(this.config.schema, key)};
57
55
  `
58
56
 
59
57
  await lockedClient.query(query)
60
58
 
61
59
  const locker = {
62
- locked: true,
63
60
  unlock: async function () {
64
61
  try {
65
62
  await lockedClient.query('COMMIT')
66
- await lockedClient.end()
67
63
  } finally {
68
- this.locked = false
64
+ lockedClient.release()
69
65
  }
70
66
  }
71
67
  }
package/src/index.js CHANGED
@@ -15,6 +15,8 @@ const events = {
15
15
  class PgBoss extends EventEmitter {
16
16
  #stoppingOn
17
17
  #stopped
18
+ #starting
19
+ #started
18
20
  #config
19
21
  #db
20
22
  #boss
@@ -102,11 +104,11 @@ class PgBoss extends EventEmitter {
102
104
  }
103
105
 
104
106
  async start () {
105
- if (this.starting || this.started) {
107
+ if (this.#starting || this.#started) {
106
108
  return
107
109
  }
108
110
 
109
- this.starting = true
111
+ this.#starting = true
110
112
 
111
113
  if (this.#db.isOurs && !this.#db.opened) {
112
114
  await this.#db.open()
@@ -132,8 +134,8 @@ class PgBoss extends EventEmitter {
132
134
  await this.#timekeeper.start()
133
135
  }
134
136
 
135
- this.starting = false
136
- this.started = true
137
+ this.#starting = false
138
+ this.#started = true
137
139
  this.#stopped = false
138
140
 
139
141
  return this
@@ -169,7 +171,7 @@ class PgBoss extends EventEmitter {
169
171
 
170
172
  this.#stopped = true
171
173
  this.#stoppingOn = null
172
- this.started = false
174
+ this.#started = false
173
175
 
174
176
  this.emit(events.stopped)
175
177
  resolve()
package/src/plans.js CHANGED
@@ -174,7 +174,7 @@ function getPartitionFunction (schema) {
174
174
  return `
175
175
  CREATE FUNCTION ${schema}.get_partition(queue_name text, out name text) AS
176
176
  $$
177
- SELECT '${schema}.j' || encode(digest(queue_name, 'sha1'), 'hex');
177
+ SELECT '${schema}.job_' || encode(sha224(queue_name::bytea), 'hex');
178
178
  $$
179
179
  LANGUAGE SQL
180
180
  IMMUTABLE
@@ -315,6 +315,8 @@ function getQueueByName (schema) {
315
315
  function deleteQueueRecords (schema) {
316
316
  return `WITH dq AS (
317
317
  DELETE FROM ${schema}.queue WHERE name = $1
318
+ ), ds AS (
319
+ DELETE FROM ${schema}.schedule WHERE name = $1
318
320
  )
319
321
  DELETE FROM ${schema}.job WHERE name = $1
320
322
  `
@@ -378,7 +380,9 @@ function createTableSubscription (schema) {
378
380
 
379
381
  function getSchedules (schema) {
380
382
  return `
381
- SELECT * FROM ${schema}.schedule
383
+ SELECT s.*
384
+ FROM ${schema}.schedule s
385
+ JOIN ${schema}.queue q on s.name = q.name
382
386
  `
383
387
  }
384
388
 
@@ -765,7 +769,7 @@ function locked (schema, query) {
765
769
 
766
770
  function advisoryLock (schema, key) {
767
771
  return `SELECT pg_advisory_xact_lock(
768
- ('x' || encode(digest(current_database() || '.pgboss.${schema}${key || ''}', 'sha256'), 'hex'))::bit(64)::bigint
772
+ ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ''}')::bytea), 'hex'))::bit(64)::bigint
769
773
  )`
770
774
  }
771
775
 
package/src/timekeeper.js CHANGED
@@ -5,7 +5,6 @@ const Attorney = require('./attorney')
5
5
  const pMap = require('p-map')
6
6
 
7
7
  const queues = {
8
- CRON: '__pgboss__cron',
9
8
  SEND_IT: '__pgboss__send-it'
10
9
  }
11
10
 
@@ -28,6 +27,7 @@ class Timekeeper extends EventEmitter {
28
27
  this.events = events
29
28
 
30
29
  this.getTimeCommand = plans.getTime(config.schema)
30
+ this.getQueueCommand = plans.getQueueByName(config.schema)
31
31
  this.getSchedulesCommand = plans.getSchedules(config.schema)
32
32
  this.scheduleCommand = plans.schedule(config.schema)
33
33
  this.unscheduleCommand = plans.unschedule(config.schema)
@@ -44,6 +44,8 @@ class Timekeeper extends EventEmitter {
44
44
  }
45
45
 
46
46
  async start () {
47
+ this.stopped = false
48
+
47
49
  // setting the archive config too low breaks the cron 60s debounce interval so don't even try
48
50
  if (this.config.archiveSeconds < 60 || this.config.archiveFailedSeconds < 60) {
49
51
  return
@@ -52,21 +54,18 @@ class Timekeeper extends EventEmitter {
52
54
  // cache the clock skew from the db server
53
55
  await this.cacheClockSkew()
54
56
 
55
- await this.manager.createQueue(queues.CRON)
56
- await this.manager.createQueue(queues.SEND_IT)
57
+ try {
58
+ await this.manager.createQueue(queues.SEND_IT)
59
+ } catch {}
57
60
 
58
- await this.manager.work(queues.CRON, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds }, (job) => this.onCron(job))
59
61
  await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
60
62
 
61
- // uses sendDebounced() to enqueue a cron check
62
- await this.checkSchedulesAsync()
63
+ setImmediate(() => this.onCron())
63
64
 
64
65
  // create monitoring interval to make sure cron hasn't crashed
65
- this.cronMonitorInterval = setInterval(async () => await this.monitorCron(), this.cronMonitorIntervalMs)
66
+ this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.cronMonitorIntervalMs)
66
67
  // create monitoring interval to measure and adjust for drift in clock skew
67
68
  this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
68
-
69
- this.stopped = false
70
69
  }
71
70
 
72
71
  async stop () {
@@ -76,7 +75,6 @@ class Timekeeper extends EventEmitter {
76
75
 
77
76
  this.stopped = true
78
77
 
79
- await this.manager.offWork(queues.CRON)
80
78
  await this.manager.offWork(queues.SEND_IT)
81
79
 
82
80
  if (this.skewMonitorInterval) {
@@ -90,22 +88,6 @@ class Timekeeper extends EventEmitter {
90
88
  }
91
89
  }
92
90
 
93
- async monitorCron () {
94
- try {
95
- if (this.config.__test__force_cron_monitoring_error) {
96
- throw new Error(this.config.__test__force_cron_monitoring_error)
97
- }
98
-
99
- const { secondsAgo } = await this.getCronTime()
100
-
101
- if (secondsAgo > 60) {
102
- await this.checkSchedulesAsync()
103
- }
104
- } catch (err) {
105
- this.emit(this.events.error, err)
106
- }
107
- }
108
-
109
91
  async cacheClockSkew () {
110
92
  let skew = 0
111
93
 
@@ -134,43 +116,42 @@ class Timekeeper extends EventEmitter {
134
116
  }
135
117
  }
136
118
 
137
- async checkSchedulesAsync () {
138
- const opts = {
139
- retryLimit: 2,
140
- retentionSeconds: 60
141
- }
142
-
143
- await this.manager.sendDebounced(queues.CRON, null, opts, 60)
144
- }
145
-
146
119
  async onCron () {
147
- if (this.stopped) return
120
+ let locker
148
121
 
149
122
  try {
150
- if (this.config.__test__throw_cron_processing) {
151
- throw new Error(this.config.__test__throw_cron_processing)
152
- }
123
+ if (this.stopped || this.timekeeping) return
153
124
 
154
- const items = await this.getSchedules()
125
+ if (this.config.__test__force_cron_monitoring_error) {
126
+ throw new Error(this.config.__test__force_cron_monitoring_error)
127
+ }
155
128
 
156
- const sending = items.filter(i => this.shouldSendIt(i.cron, i.timezone))
129
+ this.timekeeping = true
157
130
 
158
- if (sending.length && !this.stopped) {
159
- await pMap(sending, it => this.send(it), { concurrency: 5 })
160
- }
131
+ locker = await this.db.lock({ key: 'timekeeper' })
161
132
 
162
- if (this.stopped) return
133
+ const { secondsAgo } = await this.getCronTime()
163
134
 
164
- // set last time cron was evaluated for downstream usage in cron monitoring
165
- await this.setCronTime()
135
+ if (secondsAgo > this.config.cronMonitorIntervalSeconds) {
136
+ await this.cron()
137
+ await this.setCronTime()
138
+ }
166
139
  } catch (err) {
167
140
  this.emit(this.events.error, err)
141
+ } finally {
142
+ this.timekeeping = false
143
+ await locker?.unlock()
168
144
  }
145
+ }
169
146
 
170
- if (this.stopped) return
147
+ async cron () {
148
+ const items = await this.getSchedules()
171
149
 
172
- // uses sendDebounced() to enqueue a cron check
173
- await this.checkSchedulesAsync()
150
+ const sending = items.filter(i => this.shouldSendIt(i.cron, i.timezone))
151
+
152
+ if (sending.length && !this.stopped) {
153
+ await pMap(sending, it => this.send(it), { concurrency: 5 })
154
+ }
174
155
  }
175
156
 
176
157
  shouldSendIt (cron, tz) {
@@ -186,12 +167,7 @@ class Timekeeper extends EventEmitter {
186
167
  }
187
168
 
188
169
  async send (job) {
189
- const options = {
190
- singletonKey: job.name,
191
- singletonSeconds: 60
192
- }
193
-
194
- await this.manager.send(queues.SEND_IT, job, options)
170
+ await this.manager.send(queues.SEND_IT, job, { singletonKey: job.name, singletonSeconds: 60 })
195
171
  }
196
172
 
197
173
  async onSendIt (job) {
@@ -213,6 +189,13 @@ class Timekeeper extends EventEmitter {
213
189
  // validation pre-check
214
190
  Attorney.checkSendArgs([name, data, options], this.config)
215
191
 
192
+ // make sure queue exists before scheduling
193
+ const queue = await this.db.executeSql(this.getQueueCommand, [name])
194
+
195
+ if (!queue.rows.length === 0) {
196
+ throw new Error(`Queue '${name}' not found`)
197
+ }
198
+
216
199
  const values = [name, cron, tz, data, options]
217
200
 
218
201
  const result = await this.db.executeSql(this.scheduleCommand, values)