pg-boss 10.0.0-beta2 → 10.0.0-beta21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,50 +9,47 @@ 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
 
33
28
  pg-boss is a job queue built in Node.js on top of PostgreSQL in order to provide background processing and reliable asynchronous execution to Node.js applications.
34
29
 
35
- pg-boss relies on [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/), a feature added to postgres specifically for message queues, in order to resolve record locking challenges inherent with relational databases. This brings the safety of guaranteed atomic commits of a relational database to your asynchronous job processing.
30
+ pg-boss relies on [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/), a feature built specifically for message queues to resolve record locking challenges inherent with relational databases. This provides exactly-once delivery and the safety of guaranteed atomic commits to asynchronous job processing.
36
31
 
37
32
  This will likely cater the most to teams already familiar with the simplicity of relational database semantics and operations (SQL, querying, and backups). It will be especially useful to those already relying on PostgreSQL that want to limit how many systems are required to monitor and support in their architecture.
38
33
 
39
- ## Features
34
+
35
+ ## Summary
40
36
  * Exactly-once job delivery
41
37
  * Backpressure-compatible polling workers
42
38
  * Cron scheduling
39
+ * Queue storage policies to support a variety of rate limiting, debouncing, and concurrency use cases
40
+ * Priority queues, dead letter queues, job deferral, automatic retries with exponential backoff
43
41
  * Pub/sub API for fan-out queue relationships
44
- * Priority queues, deferral, retries (with exponential backoff), rate limiting, debouncing
45
- * Table operations via SQL for bulk loads via COPY or INSERT
42
+ * Raw SQL support for non-Node.js runtimes via INSERT or COPY
43
+ * Serverless function compatible
46
44
  * Multi-master compatible (for example, in a Kubernetes ReplicaSet)
47
- * Dead letter queues
48
45
 
49
46
  ## Requirements
50
47
  * Node 20 or higher
51
- * PostgreSQL 12 or higher
48
+ * PostgreSQL 13 or higher
52
49
 
53
50
  ## Installation
54
51
 
55
- ``` bash
52
+ ```bash
56
53
  # npm
57
54
  npm install pg-boss
58
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.0.0-beta2",
3
+ "version": "10.0.0-beta21",
4
4
  "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
@@ -8,8 +8,6 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
11
- "lodash.debounce": "^4.0.8",
12
- "p-map": "^4.0.0",
13
11
  "pg": "^8.5.1",
14
12
  "serialize-error": "^8.1.0"
15
13
  },
package/src/attorney.js CHANGED
@@ -8,7 +8,8 @@ module.exports = {
8
8
  checkWorkArgs,
9
9
  checkFetchArgs,
10
10
  warnClockSkew,
11
- assertPostgresObjectName
11
+ assertPostgresObjectName,
12
+ assertQueueName
12
13
  }
13
14
 
14
15
  const MAX_INTERVAL_HOURS = 24
@@ -29,8 +30,6 @@ const WARNINGS = {
29
30
  }
30
31
 
31
32
  function checkQueueArgs (name, options = {}) {
32
- assertPostgresObjectName(name)
33
-
34
33
  assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
35
34
 
36
35
  applyRetryConfig(options)
@@ -123,26 +122,25 @@ function checkWorkArgs (name, args, defaults) {
123
122
 
124
123
  options = { ...options }
125
124
 
126
- applyNewJobCheckInterval(options, defaults)
127
-
128
- assert(!('teamConcurrency' in options) ||
129
- (Number.isInteger(options.teamConcurrency) && options.teamConcurrency >= 1 && options.teamConcurrency <= 1000),
130
- 'teamConcurrency must be an integer between 1 and 1000')
125
+ applyPollingInterval(options, defaults)
131
126
 
132
- assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
133
127
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
134
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
135
132
 
136
133
  return { options, callback }
137
134
  }
138
135
 
139
- function checkFetchArgs (name, batchSize, options) {
136
+ function checkFetchArgs (name, options) {
140
137
  assert(name, 'missing queue name')
141
138
 
142
- 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')
143
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')
144
142
 
145
- return { name }
143
+ options.batchSize = options.batchSize || 1
146
144
  }
147
145
 
148
146
  function getConfig (value) {
@@ -153,6 +151,10 @@ function getConfig (value) {
153
151
  ? { connectionString: value }
154
152
  : { ...value }
155
153
 
154
+ config.schedule = ('schedule' in config) ? config.schedule : true
155
+ config.supervise = ('supervise' in config) ? config.supervise : true
156
+ config.migrate = ('migrate' in config) ? config.migrate : true
157
+
156
158
  applySchemaConfig(config)
157
159
  applyMaintenanceConfig(config)
158
160
  applyArchiveConfig(config)
@@ -160,7 +162,7 @@ function getConfig (value) {
160
162
  applyDeleteConfig(config)
161
163
  applyMonitoringConfig(config)
162
164
 
163
- applyNewJobCheckInterval(config)
165
+ applyPollingInterval(config)
164
166
  applyExpirationConfig(config)
165
167
  applyRetentionConfig(config)
166
168
 
@@ -178,8 +180,14 @@ function applySchemaConfig (config) {
178
180
  function assertPostgresObjectName (name) {
179
181
  assert(typeof name === 'string', 'Name must be a string')
180
182
  assert(name.length <= 50, 'Name cannot exceed 50 characters')
181
- assert(!/\W/.test(name), 'Name can only contain alphanumeric characters and underscores')
182
- assert(!/^d/.test(name), 'Name cannot start with a number')
183
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters or underscores')
184
+ assert(!/^\d/.test(name), 'Name cannot start with a number')
185
+ }
186
+
187
+ function assertQueueName (name) {
188
+ assert(name, 'Name is required')
189
+ assert(typeof name === 'string', 'Name must be a string')
190
+ assert(/[\w-]/.test(name), 'Name can only contain alphanumeric characters, underscores, or hyphens')
183
191
  }
184
192
 
185
193
  function applyArchiveConfig (config) {
@@ -270,18 +278,13 @@ function applyRetryConfig (config, defaults) {
270
278
  config.retryBackoffDefault = defaults?.retryBackoff
271
279
  }
272
280
 
273
- function applyNewJobCheckInterval (config, defaults) {
274
- assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 500,
275
- 'configuration assert: newJobCheckInterval must be at least every 500ms')
276
-
277
- assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
278
- 'configuration assert: newJobCheckIntervalSeconds must be at least every second')
281
+ function applyPollingInterval (config, defaults) {
282
+ assert(!('pollingIntervalSeconds' in config) || config.pollingIntervalSeconds >= 0.5,
283
+ 'configuration assert: pollingIntervalSeconds must be at least every 500ms')
279
284
 
280
- config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
281
- ? config.newJobCheckIntervalSeconds * 1000
282
- : ('newJobCheckInterval' in config)
283
- ? config.newJobCheckInterval
284
- : defaults?.newJobCheckInterval || 2000
285
+ config.pollingInterval = ('pollingIntervalSeconds' in config)
286
+ ? config.pollingIntervalSeconds * 1000
287
+ : defaults?.pollingInterval || 2000
285
288
  }
286
289
 
287
290
  function applyMaintenanceConfig (config) {
@@ -298,10 +301,6 @@ function applyMaintenanceConfig (config) {
298
301
  : 120
299
302
 
300
303
  assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
301
-
302
- config.schedule = ('schedule' in config) ? config.schedule : true
303
- config.supervise = ('supervise' in config) ? config.supervise : true
304
- config.migrate = ('migrate' in config) ? config.migrate : true
305
304
  }
306
305
 
307
306
  function applyDeleteConfig (config) {
@@ -363,21 +362,21 @@ function applyMonitoringConfig (config) {
363
362
  ? config.clockMonitorIntervalSeconds
364
363
  : TEN_MINUTES_IN_SECONDS
365
364
 
366
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 60),
367
- 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 60 seconds')
365
+ assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
366
+ 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
368
367
 
369
368
  config.cronMonitorIntervalSeconds =
370
369
  ('cronMonitorIntervalSeconds' in config)
371
370
  ? config.cronMonitorIntervalSeconds
372
- : 60
371
+ : 30
373
372
 
374
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 60),
375
- 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 60 seconds')
373
+ assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
374
+ 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
376
375
 
377
376
  config.cronWorkerIntervalSeconds =
378
377
  ('cronWorkerIntervalSeconds' in config)
379
378
  ? config.cronWorkerIntervalSeconds
380
- : 4
379
+ : 5
381
380
  }
382
381
 
383
382
  function warnClockSkew (message) {
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,29 +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
- if (locker?.locked) {
81
- await locker.unlock()
82
- }
83
-
84
77
  this.monitoring = false
85
78
  }
86
79
  }
87
80
 
88
81
  async onSupervise () {
89
- let locker
90
-
91
82
  try {
92
83
  if (this.maintaining) {
93
84
  return
@@ -108,21 +99,15 @@ class Boss extends EventEmitter {
108
99
  return
109
100
  }
110
101
 
111
- locker = await this.db.lock({ key: 'maintenance' })
102
+ const { rows } = await this.db.executeSql(this.trySetMaintenanceTimeCommand, [this.config.maintenanceIntervalSeconds])
112
103
 
113
- const { secondsAgo } = await this.getMaintenanceTime()
114
-
115
- if (secondsAgo > this.maintenanceIntervalSeconds) {
104
+ if (rows.length === 1 && !this.stopped) {
116
105
  const result = await this.maintain()
117
106
  this.emit(events.maintenance, result)
118
107
  }
119
108
  } catch (err) {
120
109
  this.emit(events.error, err)
121
110
  } finally {
122
- if (locker?.locked) {
123
- await locker.unlock()
124
- }
125
-
126
111
  this.maintaining = false
127
112
  }
128
113
  }
@@ -136,8 +121,6 @@ class Boss extends EventEmitter {
136
121
 
137
122
  const ended = Date.now()
138
123
 
139
- await this.setMaintenanceTime()
140
-
141
124
  return { ms: ended - started }
142
125
  }
143
126
 
@@ -152,10 +135,11 @@ class Boss extends EventEmitter {
152
135
  }
153
136
 
154
137
  async countStates () {
155
- const stateCountDefault = { ...plans.states }
138
+ const stateCountDefault = { ...plans.JOB_STATES }
156
139
 
157
- Object.keys(stateCountDefault)
158
- .forEach(key => { stateCountDefault[key] = 0 })
140
+ for (const key of Object.keys(stateCountDefault)) {
141
+ stateCountDefault[key] = 0
142
+ }
159
143
 
160
144
  const counts = await this.db.executeSql(this.countStatesCommand)
161
145
 
@@ -187,34 +171,6 @@ class Boss extends EventEmitter {
187
171
  async drop () {
188
172
  await this.db.executeSql(this.dropCommand)
189
173
  }
190
-
191
- async setMaintenanceTime () {
192
- await this.db.executeSql(this.setMaintenanceTimeCommand)
193
- }
194
-
195
- async getMaintenanceTime () {
196
- const { rows } = await this.db.executeSql(this.getMaintenanceTimeCommand)
197
-
198
- let { maintained_on: maintainedOn, seconds_ago: secondsAgo } = rows[0]
199
-
200
- secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 999_999_999
201
-
202
- return { maintainedOn, secondsAgo }
203
- }
204
-
205
- async setMonitorTime () {
206
- await this.db.executeSql(this.setMonitorTimeCommand)
207
- }
208
-
209
- async getMonitorTime () {
210
- const { rows } = await this.db.executeSql(this.getMonitorTimeCommand)
211
-
212
- let { monitored_on: monitoredOn, seconds_ago: secondsAgo } = rows[0]
213
-
214
- secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 999_999_999
215
-
216
- return { monitoredOn, secondsAgo }
217
- }
218
174
  }
219
175
 
220
176
  module.exports = Boss
package/src/contractor.js CHANGED
@@ -21,23 +21,29 @@ class Contractor {
21
21
  this.config = config
22
22
  this.db = db
23
23
  this.migrations = this.config.migrations || migrationStore.getAll(this.config.schema)
24
+
25
+ // exported api to index
26
+ this.functions = [
27
+ this.schemaVersion,
28
+ this.isInstalled
29
+ ]
24
30
  }
25
31
 
26
- async version () {
32
+ async schemaVersion () {
27
33
  const result = await this.db.executeSql(plans.getVersion(this.config.schema))
28
34
  return result.rows.length ? parseInt(result.rows[0].version) : null
29
35
  }
30
36
 
31
37
  async isInstalled () {
32
38
  const result = await this.db.executeSql(plans.versionTableExists(this.config.schema))
33
- return result.rows.length ? result.rows[0].name : null
39
+ return !!result.rows[0].name
34
40
  }
35
41
 
36
42
  async start () {
37
43
  const installed = await this.isInstalled()
38
44
 
39
45
  if (installed) {
40
- const version = await this.version()
46
+ const version = await this.schemaVersion()
41
47
 
42
48
  if (schemaVersion > version) {
43
49
  throw new Error('Migrations are not supported to v10')
@@ -55,7 +61,7 @@ class Contractor {
55
61
  throw new Error('pg-boss is not installed')
56
62
  }
57
63
 
58
- const version = await this.version()
64
+ const version = await this.schemaVersion()
59
65
 
60
66
  if (schemaVersion !== version) {
61
67
  throw new Error('pg-boss database requires migrations')
@@ -80,15 +86,15 @@ class Contractor {
80
86
  }
81
87
  }
82
88
 
83
- async next (version) {
84
- const commands = migrationStore.next(this.config.schema, version, this.migrations)
85
- await this.db.executeSql(commands)
86
- }
89
+ // async next (version) {
90
+ // const commands = migrationStore.next(this.config.schema, version, this.migrations)
91
+ // await this.db.executeSql(commands)
92
+ // }
87
93
 
88
- async rollback (version) {
89
- const commands = migrationStore.rollback(this.config.schema, version, this.migrations)
90
- await this.db.executeSql(commands)
91
- }
94
+ // async rollback (version) {
95
+ // const commands = migrationStore.rollback(this.config.schema, version, this.migrations)
96
+ // await this.db.executeSql(commands)
97
+ // }
92
98
  }
93
99
 
94
100
  module.exports = Contractor
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) {
@@ -11,6 +10,10 @@ class Db extends EventEmitter {
11
10
  this.config = config
12
11
  }
13
12
 
13
+ events = {
14
+ error: 'error'
15
+ }
16
+
14
17
  async open () {
15
18
  this.pool = new pg.Pool(this.config)
16
19
  this.pool.on('error', error => this.emit('error', error))
@@ -26,48 +29,19 @@ class Db extends EventEmitter {
26
29
 
27
30
  async executeSql (text, values) {
28
31
  if (this.opened) {
29
- if (this.config.debug === true) {
30
- console.log(`${new Date().toISOString()}: DEBUG SQL`)
31
- console.log(text)
32
+ // if (this.config.debug === true) {
33
+ // console.log(`${new Date().toISOString()}: DEBUG SQL`)
34
+ // console.log(text)
32
35
 
33
- if (values) {
34
- console.log(`${new Date().toISOString()}: DEBUG VALUES`)
35
- console.log(values)
36
- }
37
- }
36
+ // if (values) {
37
+ // console.log(`${new Date().toISOString()}: DEBUG VALUES`)
38
+ // console.log(values)
39
+ // }
40
+ // }
38
41
 
39
42
  return await this.pool.query(text, values)
40
43
  }
41
44
  }
42
-
43
- async lock ({ timeout = 30, key } = {}) {
44
- // const lockedClient = new pg.Client(this.config)
45
- // await lockedClient.connect()
46
- const lockedClient = await this.pool.connect()
47
-
48
- const query = `
49
- BEGIN;
50
- SET LOCAL lock_timeout = '${timeout}s';
51
- SET LOCAL idle_in_transaction_session_timeout = '3600s';
52
- ${advisoryLock(this.config.schema, key)};
53
- `
54
-
55
- await lockedClient.query(query)
56
-
57
- const locker = {
58
- locked: true,
59
- unlock: async function () {
60
- try {
61
- await lockedClient.query('COMMIT')
62
- await lockedClient.end()
63
- } finally {
64
- this.locked = false
65
- }
66
- }
67
- }
68
-
69
- return locker
70
- }
71
45
  }
72
46
 
73
47
  module.exports = Db