pg-boss 8.4.2 → 9.0.1

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,6 +1,6 @@
1
1
  Queueing jobs in Node.js using PostgreSQL like a boss.
2
2
 
3
- [![PostgreSql Version](https://img.shields.io/badge/PostgreSQL-9.5+-blue.svg?maxAge=2592000)](http://www.postgresql.org)
3
+ [![PostgreSql Version](https://img.shields.io/badge/PostgreSQL-11+-blue.svg?maxAge=2592000)](http://www.postgresql.org)
4
4
  [![npm version](https://badge.fury.io/js/pg-boss.svg)](https://badge.fury.io/js/pg-boss)
5
5
  [![Build Status](https://app.travis-ci.com/timgit/pg-boss.svg?branch=master)](https://app.travis-ci.com/github/timgit/pg-boss)
6
6
  [![Coverage Status](https://coveralls.io/repos/github/timgit/pg-boss/badge.svg?branch=master)](https://coveralls.io/github/timgit/pg-boss?branch=master)
@@ -50,8 +50,8 @@ This will likely cater the most to teams already familiar with the simplicity of
50
50
  * Automatic maintenance operations to manage table growth
51
51
 
52
52
  ## Requirements
53
- * Node 14 or higher
54
- * PostgreSQL 9.5 or higher
53
+ * Node 16 or higher
54
+ * PostgreSQL 11 or higher
55
55
 
56
56
  ## Installation
57
57
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "8.4.2",
3
+ "version": "9.0.1",
4
4
  "description": "Queueing jobs in Node.js using PostgreSQL like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
7
- "node": ">=14"
7
+ "node": ">=16"
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
package/src/attorney.js CHANGED
@@ -129,6 +129,7 @@ function checkWorkArgs (name, args, defaults) {
129
129
  assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
130
130
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
131
131
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
132
+ assert(!('enforceSingletonQueueActiveLimit' in options) || typeof options.enforceSingletonQueueActiveLimit === 'boolean', 'enforceSingletonQueueActiveLimit must be a boolean')
132
133
 
133
134
  return { options, callback }
134
135
  }
@@ -140,6 +141,7 @@ function checkFetchArgs (name, batchSize, options) {
140
141
 
141
142
  assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
142
143
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
144
+ assert(!('enforceSingletonQueueActiveLimit' in options) || typeof options.enforceSingletonQueueActiveLimit === 'boolean', 'enforceSingletonQueueActiveLimit must be a boolean')
143
145
 
144
146
  return { name }
145
147
  }
package/src/boss.js CHANGED
@@ -140,7 +140,7 @@ class Boss extends EventEmitter {
140
140
  this.emit('maintenance', { ms: ended - started })
141
141
 
142
142
  if (!this.stopped) {
143
- await job.done() // pre-complete to bypass throttling
143
+ await this.manager.complete(job.id) // pre-complete to bypass throttling
144
144
  await this.maintenanceAsync({ startAfter: this.maintenanceIntervalSeconds })
145
145
  }
146
146
  } catch (err) {
@@ -159,7 +159,7 @@ class Boss extends EventEmitter {
159
159
  this.emit(events.monitorStates, states)
160
160
 
161
161
  if (!this.stopped && this.monitorStates) {
162
- await job.done() // pre-complete to bypass throttling
162
+ await this.manager.complete(job.id) // pre-complete to bypass throttling
163
163
  await this.monitorStatesAsync({ startAfter: this.monitorIntervalSeconds })
164
164
  }
165
165
  } catch (err) {
package/src/db.js CHANGED
@@ -28,6 +28,14 @@ class Db extends EventEmitter {
28
28
  return await this.pool.query(text, values)
29
29
  }
30
30
  }
31
+
32
+ static quotePostgresStr (str) {
33
+ const delimeter = '$sanitize$'
34
+ if (str.includes(delimeter)) {
35
+ throw new Error(`Attempted to quote string that contains reserved Postgres delimeter: ${str}`)
36
+ }
37
+ return `${delimeter}${str}${delimeter}`
38
+ }
31
39
  }
32
40
 
33
41
  module.exports = Db
package/src/manager.js CHANGED
@@ -6,6 +6,7 @@ const debounce = require('lodash.debounce')
6
6
  const { serializeError: stringify } = require('serialize-error')
7
7
  const Attorney = require('./attorney')
8
8
  const Worker = require('./worker')
9
+ const Db = require('./db')
9
10
  const pMap = require('p-map')
10
11
 
11
12
  const { QUEUES: BOSS_QUEUES } = require('./boss')
@@ -184,7 +185,8 @@ class Manager extends EventEmitter {
184
185
  teamSize = 1,
185
186
  teamConcurrency = 1,
186
187
  teamRefill: refill = false,
187
- includeMetadata = false
188
+ includeMetadata = false,
189
+ enforceSingletonQueueActiveLimit = false
188
190
  } = options
189
191
 
190
192
  const id = uuid.v4()
@@ -208,7 +210,7 @@ class Manager extends EventEmitter {
208
210
  createTeamRefillPromise()
209
211
  }
210
212
 
211
- const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata })
213
+ const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata, enforceSingletonQueueActiveLimit })
212
214
 
213
215
  const onFetch = async (jobs) => {
214
216
  if (this.config.__test__throw_worker) {
@@ -220,8 +222,8 @@ class Manager extends EventEmitter {
220
222
  if (batchSize) {
221
223
  const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expire_in_seconds), 0)
222
224
 
223
- // Failing will fail all fetched jobs
224
225
  await resolveWithinSeconds(Promise.all([callback(jobs)]), maxExpiration)
226
+ .then(() => this.complete(jobs.map(job => job.id)))
225
227
  .catch(err => this.fail(jobs.map(job => job.id), err))
226
228
  } else {
227
229
  if (refill) {
@@ -474,27 +476,32 @@ class Manager extends EventEmitter {
474
476
  async fetch (name, batchSize, options = {}) {
475
477
  const values = Attorney.checkFetchArgs(name, batchSize, options)
476
478
  const db = options.db || this.db
477
- const result = await db.executeSql(
478
- this.nextJobCommand(options.includeMetadata || false),
479
- [values.name, batchSize || 1]
480
- )
479
+ const preparedStatement = this.nextJobCommand(options.includeMetadata || false, options.enforceSingletonQueueActiveLimit || false)
480
+ const statementValues = [values.name, batchSize || 1]
481
+
482
+ let result
483
+ if (options.enforceSingletonQueueActiveLimit && !options.db) {
484
+ // Prepare/format now and send multi-statement transaction
485
+ const fetchQuery = preparedStatement
486
+ .replace('$1', Db.quotePostgresStr(statementValues[0]))
487
+ .replace('$2', statementValues[1].toString())
488
+ // eslint-disable-next-line no-unused-vars
489
+ const [_begin, _setLocal, fetchResult, _commit] = await db.executeSql([
490
+ 'BEGIN',
491
+ 'SET LOCAL jit = OFF', // JIT can slow things down significantly
492
+ fetchQuery,
493
+ 'COMMIT'
494
+ ].join(';\n'))
495
+ result = fetchResult
496
+ } else {
497
+ result = await db.executeSql(preparedStatement, statementValues)
498
+ }
481
499
 
482
500
  if (!result || result.rows.length === 0) {
483
501
  return null
484
502
  }
485
503
 
486
- const jobs = result.rows.map(job => {
487
- job.done = async (error, response) => {
488
- if (error) {
489
- await this.fail(job.id, error)
490
- } else {
491
- await this.complete(job.id, response)
492
- }
493
- }
494
- return job
495
- })
496
-
497
- return jobs.length === 1 && !batchSize ? jobs[0] : jobs
504
+ return result.rows.length === 1 && !batchSize ? result.rows[0] : result.rows
498
505
  }
499
506
 
500
507
  async fetchCompleted (name, batchSize, options = {}) {
package/src/plans.js CHANGED
@@ -351,13 +351,31 @@ function insertVersion (schema, version) {
351
351
  }
352
352
 
353
353
  function fetchNextJob (schema) {
354
- return (includeMetadata) => `
354
+ return (includeMetadata, enforceSingletonQueueActiveLimit) => `
355
355
  WITH nextJob as (
356
356
  SELECT id
357
- FROM ${schema}.job
357
+ FROM ${schema}.job j
358
358
  WHERE state < '${states.active}'
359
359
  AND name LIKE $1
360
360
  AND startAfter < now()
361
+ ${enforceSingletonQueueActiveLimit
362
+ ? `AND (
363
+ CASE
364
+ WHEN singletonKey IS NOT NULL
365
+ AND singletonKey LIKE '${SINGLETON_QUEUE_KEY_ESCAPED}%'
366
+ THEN NOT EXISTS (
367
+ SELECT 1
368
+ FROM ${schema}.job active_job
369
+ WHERE active_job.state = '${states.active}'
370
+ AND active_job.name = j.name
371
+ AND active_job.singletonKey = j.singletonKey
372
+ LIMIT 1
373
+ )
374
+ ELSE
375
+ true
376
+ END
377
+ )`
378
+ : ''}
361
379
  ORDER BY priority desc, createdOn, id
362
380
  LIMIT $2
363
381
  FOR UPDATE SKIP LOCKED
package/types.d.ts CHANGED
@@ -113,20 +113,22 @@ declare namespace PgBoss {
113
113
  teamRefill?: boolean;
114
114
  batchSize?: number;
115
115
  includeMetadata?: boolean;
116
+ enforceSingletonQueueActiveLimit?: boolean;
116
117
  }
117
118
 
118
119
  type WorkOptions = JobFetchOptions & JobPollingOptions
119
120
 
120
121
  type FetchOptions = {
121
122
  includeMetadata?: boolean;
123
+ enforceSingletonQueueActiveLimit?: boolean;
122
124
  } & ConnectionOptions;
123
125
 
124
- interface WorkHandler<ReqData, ResData> {
125
- (job: PgBoss.JobWithDoneCallback<ReqData, ResData>): Promise<ResData> | void;
126
+ interface WorkHandler<ReqData> {
127
+ (job: PgBoss.Job<ReqData>): Promise<void>;
126
128
  }
127
129
 
128
- interface WorkWithMetadataHandler<ReqData, ResData> {
129
- (job: PgBoss.JobWithMetadataDoneCallback<ReqData, ResData>): Promise<ResData> | void;
130
+ interface WorkWithMetadataHandler<ReqData> {
131
+ (job: PgBoss.JobWithMetadata<ReqData>): Promise<void>;
130
132
  }
131
133
 
132
134
  interface Request {
@@ -142,10 +144,6 @@ declare namespace PgBoss {
142
144
  options?: ScheduleOptions;
143
145
  }
144
146
 
145
- interface JobDoneCallback<T> {
146
- (err?: Error | null, data?: T): void;
147
- }
148
-
149
147
  // source (for now): https://github.com/bendrucker/postgres-interval/blob/master/index.d.ts
150
148
  interface PostgresInterval {
151
149
  years?: number;
@@ -204,14 +202,6 @@ declare namespace PgBoss {
204
202
  onComplete?: boolean
205
203
  }
206
204
 
207
- interface JobWithDoneCallback<ReqData, ResData> extends Job<ReqData> {
208
- done: JobDoneCallback<ResData>;
209
- }
210
-
211
- interface JobWithMetadataDoneCallback<ReqData, ResData> extends JobWithMetadata<ReqData> {
212
- done: JobDoneCallback<ResData>;
213
- }
214
-
215
205
  interface MonitorState {
216
206
  all: number;
217
207
  created: number;
@@ -308,9 +298,9 @@ declare class PgBoss extends EventEmitter {
308
298
  insert(jobs: PgBoss.JobInsert[]): Promise<void>;
309
299
  insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
310
300
 
311
- work<ReqData, ResData>(name: string, handler: PgBoss.WorkHandler<ReqData, ResData>): Promise<string>;
312
- work<ReqData, ResData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData, ResData>): Promise<string>;
313
- work<ReqData, ResData>(name: string, options: PgBoss.WorkOptions, handler: PgBoss.WorkHandler<ReqData, ResData>): Promise<string>;
301
+ work<ReqData>(name: string, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
302
+ work<ReqData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData>): Promise<string>;
303
+ work<ReqData>(name: string, options: PgBoss.WorkOptions, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
314
304
 
315
305
  onComplete(name: string, handler: Function): Promise<string>;
316
306
  onComplete(name: string, options: PgBoss.WorkOptions, handler: Function): Promise<string>;