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 +3 -3
- package/package.json +2 -2
- package/src/attorney.js +2 -0
- package/src/boss.js +2 -2
- package/src/db.js +8 -0
- package/src/manager.js +26 -19
- package/src/plans.js +20 -2
- package/types.d.ts +9 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Queueing jobs in Node.js using PostgreSQL like a boss.
|
|
2
2
|
|
|
3
|
-
[](http://www.postgresql.org)
|
|
4
4
|
[](https://badge.fury.io/js/pg-boss)
|
|
5
5
|
[](https://app.travis-ci.com/github/timgit/pg-boss)
|
|
6
6
|
[](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
|
|
54
|
-
* PostgreSQL
|
|
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": "
|
|
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": ">=
|
|
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.
|
|
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.
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
(job: PgBoss.
|
|
126
|
+
interface WorkHandler<ReqData> {
|
|
127
|
+
(job: PgBoss.Job<ReqData>): Promise<void>;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
interface WorkWithMetadataHandler<ReqData
|
|
129
|
-
(job: PgBoss.
|
|
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
|
|
312
|
-
work<ReqData
|
|
313
|
-
work<ReqData
|
|
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>;
|