pg-boss 10.0.0-beta1 → 10.0.0-beta11

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-beta11",
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,13 @@ 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(/[\w-]/.test(name), 'Name can only contain alphanumeric characters, underscores, or hyphens')
196
191
  }
197
192
 
198
193
  function applyArchiveConfig (config) {
@@ -283,18 +278,13 @@ function applyRetryConfig (config, defaults) {
283
278
  config.retryBackoffDefault = defaults?.retryBackoff
284
279
  }
285
280
 
286
- function applyNewJobCheckInterval (config, defaults) {
287
- assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 500,
288
- 'configuration assert: newJobCheckInterval must be at least every 500ms')
289
-
290
- assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
291
- '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')
292
284
 
293
- config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
294
- ? config.newJobCheckIntervalSeconds * 1000
295
- : ('newJobCheckInterval' in config)
296
- ? config.newJobCheckInterval
297
- : defaults?.newJobCheckInterval || 2000
285
+ config.pollingInterval = ('pollingIntervalSeconds' in config)
286
+ ? config.pollingIntervalSeconds * 1000
287
+ : defaults?.pollingInterval || 2000
298
288
  }
299
289
 
300
290
  function applyMaintenanceConfig (config) {
@@ -311,10 +301,6 @@ function applyMaintenanceConfig (config) {
311
301
  : 120
312
302
 
313
303
  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
304
  }
319
305
 
320
306
  function applyDeleteConfig (config) {
@@ -376,21 +362,21 @@ function applyMonitoringConfig (config) {
376
362
  ? config.clockMonitorIntervalSeconds
377
363
  : TEN_MINUTES_IN_SECONDS
378
364
 
379
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 60),
380
- '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')
381
367
 
382
368
  config.cronMonitorIntervalSeconds =
383
369
  ('cronMonitorIntervalSeconds' in config)
384
370
  ? config.cronMonitorIntervalSeconds
385
- : 60
371
+ : 30
386
372
 
387
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 60),
388
- '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')
389
375
 
390
376
  config.cronWorkerIntervalSeconds =
391
377
  ('cronWorkerIntervalSeconds' in config)
392
378
  ? config.cronWorkerIntervalSeconds
393
- : 4
379
+ : 5
394
380
  }
395
381
 
396
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' })
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