pg-boss 10.0.0-beta1 → 10.0.0-beta10

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
@@ -1,4 +1,4 @@
1
- Queueing jobs in Node.js using PostgreSQL like a boss.
1
+ Queueing jobs in Postgres from Node.js like a boss.
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/pg-boss.svg)](https://badge.fury.io/js/pg-boss)
4
4
  [![Build](https://github.com/timgit/pg-boss/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/timgit/pg-boss/actions/workflows/ci.yml)
@@ -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
 
@@ -41,16 +36,14 @@ This will likely cater the most to teams already familiar with the simplicity of
41
36
  * Backpressure-compatible polling workers
42
37
  * Cron scheduling
43
38
  * Pub/sub API for fan-out queue relationships
44
- * Priority, deferral, retries (with exponential backoff), rate limiting, debouncing
45
- * Direct table access for bulk loads via COPY or INSERT
39
+ * Priority queues, deferral, retries (with exponential backoff), rate limiting, debouncing
40
+ * Table operations via SQL for bulk loads via COPY or INSERT
46
41
  * Multi-master compatible (for example, in a Kubernetes ReplicaSet)
47
42
  * Dead letter queues
48
- * Automatic creation and migration of storage tables
49
- * Automatic maintenance operations to manage table growth
50
43
 
51
44
  ## Requirements
52
- * Node 18 or higher
53
- * PostgreSQL 12 or higher
45
+ * Node 20 or higher
46
+ * PostgreSQL 13 or higher
54
47
 
55
48
  ## Installation
56
49
 
@@ -66,7 +59,6 @@ yarn add pg-boss
66
59
  * [Docs](docs/readme.md)
67
60
 
68
61
  ## Contributing
69
-
70
62
  To setup a development environment for this library:
71
63
 
72
64
  ```bash
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "10.0.0-beta1",
4
- "description": "Queueing jobs in Node.js using PostgreSQL like a boss",
3
+ "version": "10.0.0-beta10",
4
+ "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
7
- "node": ">=18"
7
+ "node": ">=20"
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
@@ -17,14 +17,15 @@
17
17
  "@types/node": "^20.3.3",
18
18
  "luxon": "^3.0.1",
19
19
  "mocha": "^10.0.0",
20
- "nyc": "^15.1.0",
20
+ "nyc": "^17.0.0",
21
21
  "standard": "^17.0.0"
22
22
  },
23
23
  "scripts": {
24
24
  "test": "standard && mocha",
25
25
  "cover": "nyc npm test",
26
26
  "tsc": "tsc --noEmit types.d.ts",
27
- "readme": "node ./test/readme.js"
27
+ "readme": "node ./test/readme.js",
28
+ "migrate": "node -e 'console.log(require(\"./src\").getMigrationPlans())'"
28
29
  },
29
30
  "mocha": {
30
31
  "timeout": 10000,
package/src/attorney.js CHANGED
@@ -8,8 +8,8 @@ module.exports = {
8
8
  checkWorkArgs,
9
9
  checkFetchArgs,
10
10
  warnClockSkew,
11
- queueNameHasPatternMatch,
12
- assertPostgresObjectName
11
+ assertPostgresObjectName,
12
+ assertQueueName
13
13
  }
14
14
 
15
15
  const MAX_INTERVAL_HOURS = 24
@@ -30,8 +30,6 @@ const WARNINGS = {
30
30
  }
31
31
 
32
32
  function checkQueueArgs (name, options = {}) {
33
- assertPostgresObjectName(name)
34
-
35
33
  assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
36
34
 
37
35
  applyRetryConfig(options)
@@ -124,7 +122,7 @@ function checkWorkArgs (name, args, defaults) {
124
122
 
125
123
  options = { ...options }
126
124
 
127
- applyNewJobCheckInterval(options, defaults)
125
+ applyPollingInterval(options, defaults)
128
126
 
129
127
  assert(!('teamConcurrency' in options) ||
130
128
  (Number.isInteger(options.teamConcurrency) && options.teamConcurrency >= 1 && options.teamConcurrency <= 1000),
@@ -140,24 +138,12 @@ function checkWorkArgs (name, args, defaults) {
140
138
  function checkFetchArgs (name, batchSize, options) {
141
139
  assert(name, 'missing queue name')
142
140
 
143
- if (queueNameHasPatternMatch(name)) {
144
- name = sanitizeQueueNameForFetch(name)
145
- }
146
-
147
141
  assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
148
142
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
149
143
 
150
144
  return { name }
151
145
  }
152
146
 
153
- function sanitizeQueueNameForFetch (name) {
154
- return name.replace(/[%_*]/g, match => match === '*' ? '%' : '\\' + match)
155
- }
156
-
157
- function queueNameHasPatternMatch (name) {
158
- return name.includes('*')
159
- }
160
-
161
147
  function getConfig (value) {
162
148
  assert(value && (typeof value === 'object' || typeof value === 'string'),
163
149
  'configuration assert: string or config object is required to connect to postgres')
@@ -166,6 +152,10 @@ function getConfig (value) {
166
152
  ? { connectionString: value }
167
153
  : { ...value }
168
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
+
169
159
  applySchemaConfig(config)
170
160
  applyMaintenanceConfig(config)
171
161
  applyArchiveConfig(config)
@@ -173,7 +163,7 @@ function getConfig (value) {
173
163
  applyDeleteConfig(config)
174
164
  applyMonitoringConfig(config)
175
165
 
176
- applyNewJobCheckInterval(config)
166
+ applyPollingInterval(config)
177
167
  applyExpirationConfig(config)
178
168
  applyRetentionConfig(config)
179
169
 
@@ -191,8 +181,14 @@ function applySchemaConfig (config) {
191
181
  function assertPostgresObjectName (name) {
192
182
  assert(typeof name === 'string', 'Name must be a string')
193
183
  assert(name.length <= 50, 'Name cannot exceed 50 characters')
194
- assert(!/\W/.test(name), 'Name can only contain alphanumeric characters and underscores')
195
- assert(!/^d/.test(name), 'Name cannot start with a number')
184
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters or underscores')
185
+ assert(!/^\d/.test(name), 'Name cannot start with a number')
186
+ }
187
+
188
+ function assertQueueName (name) {
189
+ assert(typeof name === 'string', 'Name must be a string')
190
+ assert(name.length <= 50, 'Name cannot exceed 50 characters')
191
+ assert(/[\w-]/.test(name), 'Name can only contain alphanumeric characters, underscores, or hyphens')
196
192
  }
197
193
 
198
194
  function applyArchiveConfig (config) {
@@ -283,18 +279,13 @@ function applyRetryConfig (config, defaults) {
283
279
  config.retryBackoffDefault = defaults?.retryBackoff
284
280
  }
285
281
 
286
- function applyNewJobCheckInterval (config, defaults) {
287
- assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 500,
288
- 'configuration assert: newJobCheckInterval must be at least every 500ms')
282
+ function applyPollingInterval (config, defaults) {
283
+ assert(!('pollingIntervalSeconds' in config) || config.pollingIntervalSeconds >= 0.5,
284
+ 'configuration assert: pollingIntervalSeconds must be at least every 500ms')
289
285
 
290
- assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
291
- 'configuration assert: newJobCheckIntervalSeconds must be at least every second')
292
-
293
- config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
294
- ? config.newJobCheckIntervalSeconds * 1000
295
- : ('newJobCheckInterval' in config)
296
- ? config.newJobCheckInterval
297
- : defaults?.newJobCheckInterval || 2000
286
+ config.pollingInterval = ('pollingIntervalSeconds' in config)
287
+ ? config.pollingIntervalSeconds * 1000
288
+ : defaults?.pollingInterval || 2000
298
289
  }
299
290
 
300
291
  function applyMaintenanceConfig (config) {
@@ -311,10 +302,6 @@ function applyMaintenanceConfig (config) {
311
302
  : 120
312
303
 
313
304
  assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
314
-
315
- config.schedule = ('schedule' in config) ? config.schedule : true
316
- config.supervise = ('supervise' in config) ? config.supervise : true
317
- config.migrate = ('migrate' in config) ? config.migrate : true
318
305
  }
319
306
 
320
307
  function applyDeleteConfig (config) {
@@ -376,21 +363,21 @@ function applyMonitoringConfig (config) {
376
363
  ? config.clockMonitorIntervalSeconds
377
364
  : TEN_MINUTES_IN_SECONDS
378
365
 
379
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 60),
380
- 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 60 seconds')
366
+ assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
367
+ 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
381
368
 
382
369
  config.cronMonitorIntervalSeconds =
383
370
  ('cronMonitorIntervalSeconds' in config)
384
371
  ? config.cronMonitorIntervalSeconds
385
- : 60
372
+ : 30
386
373
 
387
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 60),
388
- 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 60 seconds')
374
+ assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
375
+ 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
389
376
 
390
377
  config.cronWorkerIntervalSeconds =
391
378
  ('cronWorkerIntervalSeconds' in config)
392
379
  ? config.cronWorkerIntervalSeconds
393
- : 4
380
+ : 5
394
381
  }
395
382
 
396
383
  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' })
112
-
113
- const { secondsAgo } = await this.getMaintenanceTime()
102
+ const { rows } = await this.db.executeSql(this.trySetMaintenanceTimeCommand, [this.config.maintenanceIntervalSeconds])
114
103
 
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
 
package/src/contractor.js CHANGED
@@ -21,26 +21,33 @@ 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
- await this.migrate(version)
49
+ throw new Error('Migrations are not supported to v10')
50
+ // await this.migrate(version)
44
51
  }
45
52
  } else {
46
53
  await this.create()
@@ -54,7 +61,7 @@ class Contractor {
54
61
  throw new Error('pg-boss is not installed')
55
62
  }
56
63
 
57
- const version = await this.version()
64
+ const version = await this.schemaVersion()
58
65
 
59
66
  if (schemaVersion !== version) {
60
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) {
@@ -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,55 +29,18 @@ 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
-
33
- // if (values) {
34
- // console.log(`${new Date().toISOString()}: DEBUG VALUES`)
35
- // console.log(values)
36
- // }
37
- // }
38
-
39
- return await this.pool.query(text, values)
40
- }
41
- }
32
+ if (this.config.debug === true) {
33
+ console.log(`${new Date().toISOString()}: DEBUG SQL`)
34
+ console.log(text)
42
35
 
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
36
+ if (values) {
37
+ console.log(`${new Date().toISOString()}: DEBUG VALUES`)
38
+ console.log(values)
65
39
  }
66
40
  }
67
- }
68
41
 
69
- return locker
70
- }
71
-
72
- static quotePostgresStr (str) {
73
- const delimeter = '$sanitize$'
74
- if (str.includes(delimeter)) {
75
- throw new Error(`Attempted to quote string that contains reserved Postgres delimeter: ${str}`)
42
+ return await this.pool.query(text, values)
76
43
  }
77
- return `${delimeter}${str}${delimeter}`
78
44
  }
79
45
  }
80
46