pg-boss 7.2.1 → 8.0.0
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 +1 -1
- package/package.json +11 -8
- package/src/attorney.js +19 -1
- package/src/index.js +3 -1
- package/src/manager.js +27 -17
- package/src/migrationStore.js +18 -216
- package/src/plans.js +4 -2
- package/src/timekeeper.js +13 -5
- package/src/worker.js +11 -1
- package/types.d.ts +17 -3
- package/version.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ 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
|
|
53
|
+
* Node 14 or higher
|
|
54
54
|
* PostgreSQL 9.5 or higher
|
|
55
55
|
|
|
56
56
|
## Installation
|
package/package.json
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pg-boss",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
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": ">=14"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"cron-parser": "^4.0.0",
|
|
11
11
|
"delay": "^5.0.0",
|
|
12
12
|
"lodash.debounce": "^4.0.8",
|
|
13
|
-
"p-map": "^
|
|
13
|
+
"p-map": "^5.3.0",
|
|
14
14
|
"pg": "^8.5.1",
|
|
15
|
+
"serialize-error": "^11.0.0",
|
|
15
16
|
"uuid": "^8.3.2"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@types/node": "^
|
|
19
|
+
"@types/node": "^18.0.0",
|
|
19
20
|
"coveralls": "^3.1.0",
|
|
20
|
-
"luxon": "^
|
|
21
|
-
"mocha": "^
|
|
21
|
+
"luxon": "^3.0.1",
|
|
22
|
+
"mocha": "^10.0.0",
|
|
22
23
|
"nyc": "^15.1.0",
|
|
23
|
-
"standard": "^
|
|
24
|
+
"standard": "^17.0.0"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"test": "standard && mocha",
|
|
27
28
|
"cover": "nyc --reporter=text npm test",
|
|
28
|
-
"
|
|
29
|
+
"travis-cover": "nyc --reporter=text npm run travis-test",
|
|
30
|
+
"travis-test": "standard && mocha --jobs 0 --exit",
|
|
31
|
+
"forcover": "npm run travis-cover && nyc report --reporter=text-lcov | coveralls",
|
|
29
32
|
"export-schema": "node ./scripts/construct.js",
|
|
30
33
|
"export-migration": "node ./scripts/migrate.js",
|
|
31
34
|
"export-rollback": "node ./scripts/rollback.js",
|
package/src/attorney.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const assert = require('assert')
|
|
2
|
-
const { DEFAULT_SCHEMA } = require('./plans')
|
|
2
|
+
const { DEFAULT_SCHEMA, SINGLETON_QUEUE_KEY } = require('./plans')
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
getConfig,
|
|
6
6
|
checkSendArgs,
|
|
7
|
+
checkInsertArgs,
|
|
7
8
|
checkWorkArgs,
|
|
8
9
|
checkFetchArgs,
|
|
9
10
|
warnClockSkew
|
|
@@ -60,6 +61,7 @@ function checkSendArgs (args, defaults) {
|
|
|
60
61
|
applyExpirationConfig(options, defaults)
|
|
61
62
|
applyRetentionConfig(options, defaults)
|
|
62
63
|
applyCompletionConfig(options, defaults)
|
|
64
|
+
applySingletonKeyConfig(options)
|
|
63
65
|
|
|
64
66
|
const { startAfter, singletonSeconds, singletonMinutes, singletonHours } = options
|
|
65
67
|
|
|
@@ -84,6 +86,22 @@ function checkSendArgs (args, defaults) {
|
|
|
84
86
|
return { name, data, options }
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
function checkInsertArgs (jobs) {
|
|
90
|
+
assert(Array.isArray(jobs), `jobs argument should be an array. Received '${typeof jobs}'`)
|
|
91
|
+
return jobs.map(job => {
|
|
92
|
+
job = { ...job }
|
|
93
|
+
applySingletonKeyConfig(job)
|
|
94
|
+
return job
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applySingletonKeyConfig (options) {
|
|
99
|
+
if (options.singletonKey && options.useSingletonQueue && options.singletonKey !== SINGLETON_QUEUE_KEY) {
|
|
100
|
+
options.singletonKey = SINGLETON_QUEUE_KEY + options.singletonKey
|
|
101
|
+
}
|
|
102
|
+
delete options.useSingletonQueue
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
function checkWorkArgs (name, args, defaults) {
|
|
88
106
|
let options, callback
|
|
89
107
|
|
package/src/index.js
CHANGED
|
@@ -90,6 +90,8 @@ class PgBoss extends EventEmitter {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async start () {
|
|
93
|
+
const { serializeError } = await import('serialize-error')
|
|
94
|
+
|
|
93
95
|
if (!this.stopped) {
|
|
94
96
|
return this
|
|
95
97
|
}
|
|
@@ -104,7 +106,7 @@ class PgBoss extends EventEmitter {
|
|
|
104
106
|
|
|
105
107
|
this.started = true
|
|
106
108
|
|
|
107
|
-
this.manager.start()
|
|
109
|
+
this.manager.start({ stringify: serializeError })
|
|
108
110
|
|
|
109
111
|
if (!this.config.noSupervisor) {
|
|
110
112
|
await this.boss.supervise()
|
package/src/manager.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const assert = require('assert')
|
|
2
2
|
const EventEmitter = require('events')
|
|
3
|
-
const pMap = require('p-map')
|
|
4
3
|
const delay = require('delay')
|
|
5
4
|
const uuid = require('uuid')
|
|
6
5
|
const debounce = require('lodash.debounce')
|
|
@@ -45,6 +44,8 @@ class Manager extends EventEmitter {
|
|
|
45
44
|
constructor (db, config) {
|
|
46
45
|
super()
|
|
47
46
|
|
|
47
|
+
this.stringify = null
|
|
48
|
+
|
|
48
49
|
this.config = config
|
|
49
50
|
this.db = db
|
|
50
51
|
|
|
@@ -74,6 +75,7 @@ class Manager extends EventEmitter {
|
|
|
74
75
|
this.fetchCompleted,
|
|
75
76
|
this.work,
|
|
76
77
|
this.offWork,
|
|
78
|
+
this.notifyWorker,
|
|
77
79
|
this.onComplete,
|
|
78
80
|
this.offComplete,
|
|
79
81
|
this.publish,
|
|
@@ -96,7 +98,8 @@ class Manager extends EventEmitter {
|
|
|
96
98
|
this.emitWipThrottled = debounce(() => this.emit(events.wip, this.getWipData()), WIP_EVENT_INTERVAL, WIP_DEBOUNCE_OPTIONS)
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
start () {
|
|
101
|
+
start ({ stringify }) {
|
|
102
|
+
this.stringify = stringify
|
|
100
103
|
this.stopping = false
|
|
101
104
|
}
|
|
102
105
|
|
|
@@ -210,6 +213,8 @@ class Manager extends EventEmitter {
|
|
|
210
213
|
const fetch = () => this.fetch(name, batchSize || (teamSize - queueSize), { includeMetadata })
|
|
211
214
|
|
|
212
215
|
const onFetch = async (jobs) => {
|
|
216
|
+
const { default: pMap } = await import('p-map')
|
|
217
|
+
|
|
213
218
|
if (this.config.__test__throw_worker) {
|
|
214
219
|
throw new Error('__test__throw_worker')
|
|
215
220
|
}
|
|
@@ -294,6 +299,12 @@ class Manager extends EventEmitter {
|
|
|
294
299
|
})
|
|
295
300
|
}
|
|
296
301
|
|
|
302
|
+
notifyWorker (workerId) {
|
|
303
|
+
if (this.workers.has(workerId)) {
|
|
304
|
+
this.workers.get(workerId).notify()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
297
308
|
async subscribe (event, name) {
|
|
298
309
|
assert(event, 'Missing required argument')
|
|
299
310
|
assert(name, 'Missing required argument')
|
|
@@ -386,6 +397,7 @@ class Manager extends EventEmitter {
|
|
|
386
397
|
|
|
387
398
|
async createJob (name, data, options, singletonOffset = 0) {
|
|
388
399
|
const {
|
|
400
|
+
db: wrapper,
|
|
389
401
|
expireIn,
|
|
390
402
|
priority,
|
|
391
403
|
startAfter,
|
|
@@ -416,8 +428,8 @@ class Manager extends EventEmitter {
|
|
|
416
428
|
keepUntil, // 13
|
|
417
429
|
onComplete // 14
|
|
418
430
|
]
|
|
419
|
-
|
|
420
|
-
const result = await
|
|
431
|
+
const db = wrapper || this.db
|
|
432
|
+
const result = await db.executeSql(this.insertJobCommand, values)
|
|
421
433
|
|
|
422
434
|
if (result && result.rowCount === 1) {
|
|
423
435
|
return result.rows[0].id
|
|
@@ -438,10 +450,12 @@ class Manager extends EventEmitter {
|
|
|
438
450
|
return await this.createJob(name, data, options, singletonOffset)
|
|
439
451
|
}
|
|
440
452
|
|
|
441
|
-
async insert (jobs) {
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
453
|
+
async insert (jobs, options = {}) {
|
|
454
|
+
const { db: wrapper } = options
|
|
455
|
+
const db = wrapper || this.db
|
|
456
|
+
const checkedJobs = Attorney.checkInsertArgs(jobs)
|
|
457
|
+
const data = JSON.stringify(checkedJobs)
|
|
458
|
+
return await db.executeSql(this.insertJobsCommand, [data])
|
|
445
459
|
}
|
|
446
460
|
|
|
447
461
|
getDebounceStartAfter (singletonSeconds, clockOffset) {
|
|
@@ -463,8 +477,8 @@ class Manager extends EventEmitter {
|
|
|
463
477
|
|
|
464
478
|
async fetch (name, batchSize, options = {}) {
|
|
465
479
|
const values = Attorney.checkFetchArgs(name, batchSize, options)
|
|
466
|
-
|
|
467
|
-
const result = await
|
|
480
|
+
const db = options.db || this.db
|
|
481
|
+
const result = await db.executeSql(
|
|
468
482
|
this.nextJobCommand(options.includeMetadata || false),
|
|
469
483
|
[values.name, batchSize || 1]
|
|
470
484
|
)
|
|
@@ -506,15 +520,11 @@ class Manager extends EventEmitter {
|
|
|
506
520
|
mapCompletionDataArg (data) {
|
|
507
521
|
if (data === null || typeof data === 'undefined' || typeof data === 'function') { return null }
|
|
508
522
|
|
|
509
|
-
|
|
510
|
-
const newData = {}
|
|
511
|
-
Object.getOwnPropertyNames(data).forEach(key => { newData[key] = data[key] })
|
|
512
|
-
data = newData
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return (typeof data === 'object' && !Array.isArray(data))
|
|
523
|
+
const result = (typeof data === 'object' && !Array.isArray(data))
|
|
516
524
|
? data
|
|
517
525
|
: { value: data }
|
|
526
|
+
|
|
527
|
+
return this.stringify(result)
|
|
518
528
|
}
|
|
519
529
|
|
|
520
530
|
mapCompletionResponse (ids, result) {
|
package/src/migrationStore.js
CHANGED
|
@@ -62,11 +62,25 @@ function migrate (value, version, migrations) {
|
|
|
62
62
|
return flatten(schema, result.install, result.version)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function getAll (schema
|
|
66
|
-
const DEFAULT_RETENTION = '30 days'
|
|
67
|
-
const keepUntil = config ? config.keepUntil : DEFAULT_RETENTION
|
|
68
|
-
|
|
65
|
+
function getAll (schema) {
|
|
69
66
|
return [
|
|
67
|
+
{
|
|
68
|
+
release: '7.4.0',
|
|
69
|
+
version: 20,
|
|
70
|
+
previous: 19,
|
|
71
|
+
install: [
|
|
72
|
+
`DROP INDEX ${schema}.job_singletonKey`,
|
|
73
|
+
`DROP INDEX ${schema}.job_singleton_queue`,
|
|
74
|
+
`CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL AND NOT singletonKey LIKE '\\_\\_pgboss\\_\\_singleton\\_queue%'`,
|
|
75
|
+
`CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < 'active' AND singletonOn IS NULL AND singletonKey LIKE '\\_\\_pgboss\\_\\_singleton\\_queue%'`
|
|
76
|
+
],
|
|
77
|
+
uninstall: [
|
|
78
|
+
`DROP INDEX ${schema}.job_singletonKey`,
|
|
79
|
+
`DROP INDEX ${schema}.job_singleton_queue`,
|
|
80
|
+
`CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL AND NOT singletonKey = '__pgboss__singleton_queue'`,
|
|
81
|
+
`CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < 'active' AND singletonOn IS NULL AND singletonKey = '__pgboss__singleton_queue'`
|
|
82
|
+
]
|
|
83
|
+
},
|
|
70
84
|
{
|
|
71
85
|
release: '7.0.0',
|
|
72
86
|
version: 19,
|
|
@@ -154,218 +168,6 @@ function getAll (schema, config) {
|
|
|
154
168
|
uninstall: [
|
|
155
169
|
`ALTER TABLE ${schema}.version DROP COLUMN maintained_on`
|
|
156
170
|
]
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
release: '5.0.0-beta1',
|
|
160
|
-
version: 13,
|
|
161
|
-
previous: 12,
|
|
162
|
-
install: [
|
|
163
|
-
`CREATE TABLE ${schema}.schedule (
|
|
164
|
-
name text primary key,
|
|
165
|
-
cron text not null,
|
|
166
|
-
timezone text,
|
|
167
|
-
data jsonb,
|
|
168
|
-
options jsonb,
|
|
169
|
-
created_on timestamp with time zone not null default now(),
|
|
170
|
-
updated_on timestamp with time zone not null default now()
|
|
171
|
-
)`
|
|
172
|
-
],
|
|
173
|
-
uninstall: [
|
|
174
|
-
`DROP TABLE ${schema}.schedule`
|
|
175
|
-
]
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
release: '4.0.0',
|
|
179
|
-
version: 12,
|
|
180
|
-
previous: 11,
|
|
181
|
-
install: [
|
|
182
|
-
`ALTER TABLE ${schema}.version ALTER COLUMN version TYPE int USING version::int`,
|
|
183
|
-
`ALTER TABLE ${schema}.job ADD COLUMN keepUntil timestamptz`,
|
|
184
|
-
`ALTER TABLE ${schema}.archive ADD COLUMN keepUntil timestamptz`,
|
|
185
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN keepUntil SET DEFAULT now() + interval '${DEFAULT_RETENTION}'`,
|
|
186
|
-
`UPDATE ${schema}.job SET keepUntil = startAfter + interval '${keepUntil}'`,
|
|
187
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN keepUntil SET NOT NULL`
|
|
188
|
-
],
|
|
189
|
-
uninstall: [
|
|
190
|
-
`ALTER TABLE ${schema}.version ALTER COLUMN version TYPE text USING version::text`,
|
|
191
|
-
`ALTER TABLE ${schema}.job DROP COLUMN keepUntil`,
|
|
192
|
-
`ALTER TABLE ${schema}.archive DROP COLUMN keepUntil`
|
|
193
|
-
]
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
release: '3.2.0',
|
|
197
|
-
version: 11,
|
|
198
|
-
previous: 10,
|
|
199
|
-
install: [
|
|
200
|
-
`CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
|
|
201
|
-
],
|
|
202
|
-
uninstall: [
|
|
203
|
-
`DROP INDEX ${schema}.archive_archivedon_idx`
|
|
204
|
-
]
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
release: '3.1.3',
|
|
208
|
-
version: 10,
|
|
209
|
-
previous: 9,
|
|
210
|
-
install: [
|
|
211
|
-
`CREATE INDEX archive_id_idx ON ${schema}.archive(id)`
|
|
212
|
-
],
|
|
213
|
-
uninstall: [
|
|
214
|
-
`DROP INDEX ${schema}.archive_id_idx`
|
|
215
|
-
]
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
release: '3.1.0',
|
|
219
|
-
version: 9,
|
|
220
|
-
previous: 8,
|
|
221
|
-
install: [
|
|
222
|
-
`DROP INDEX ${schema}.job_fetch`,
|
|
223
|
-
`DROP INDEX ${schema}.job_name`,
|
|
224
|
-
`CREATE INDEX job_name ON ${schema}.job (name text_pattern_ops)`,
|
|
225
|
-
`UPDATE ${schema}.job set name = '__state__completed__' || substr(name, 1, position('__state__completed' in name) - 1) WHERE name LIKE '%__state__completed'`
|
|
226
|
-
],
|
|
227
|
-
uninstall: [
|
|
228
|
-
`UPDATE ${schema}.job set name = substr(name, 21) || '__state__completed' WHERE name LIKE '__state__completed__%'`,
|
|
229
|
-
`CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
|
|
230
|
-
`DROP INDEX ${schema}.job_name`,
|
|
231
|
-
`CREATE INDEX job_name ON ${schema}.job (name) WHERE state < 'active'`
|
|
232
|
-
]
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
release: '3.0.0',
|
|
236
|
-
version: 8,
|
|
237
|
-
previous: 7,
|
|
238
|
-
install: [
|
|
239
|
-
'CREATE EXTENSION IF NOT EXISTS pgcrypto',
|
|
240
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN id SET DEFAULT gen_random_uuid()`,
|
|
241
|
-
`ALTER TABLE ${schema}.job ADD retryDelay integer not null DEFAULT (0)`,
|
|
242
|
-
`ALTER TABLE ${schema}.job ADD retryBackoff boolean not null DEFAULT false`,
|
|
243
|
-
`ALTER TABLE ${schema}.job ADD startAfter timestamp with time zone not null default now()`,
|
|
244
|
-
`UPDATE ${schema}.job SET startAfter = createdOn + startIn`,
|
|
245
|
-
`ALTER TABLE ${schema}.job DROP COLUMN startIn`,
|
|
246
|
-
`UPDATE ${schema}.job SET expireIn = interval '15 minutes' WHERE expireIn IS NULL`,
|
|
247
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN expireIn SET NOT NULL`,
|
|
248
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN expireIn SET DEFAULT interval '15 minutes'`,
|
|
249
|
-
// archive table schema changes
|
|
250
|
-
`ALTER TABLE ${schema}.archive ADD retryDelay integer not null DEFAULT (0)`,
|
|
251
|
-
`ALTER TABLE ${schema}.archive ADD retryBackoff boolean not null DEFAULT false`,
|
|
252
|
-
`ALTER TABLE ${schema}.archive ADD startAfter timestamp with time zone`,
|
|
253
|
-
`UPDATE ${schema}.archive SET startAfter = createdOn + startIn`,
|
|
254
|
-
`ALTER TABLE ${schema}.archive DROP COLUMN startIn`,
|
|
255
|
-
// rename complete to completed for state enum - can't use ALTER TYPE :(
|
|
256
|
-
`DROP INDEX ${schema}.job_fetch`,
|
|
257
|
-
`DROP INDEX ${schema}.job_singletonOn`,
|
|
258
|
-
`DROP INDEX ${schema}.job_singletonKeyOn`,
|
|
259
|
-
`DROP INDEX ${schema}.job_singletonKey`,
|
|
260
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
|
|
261
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE text USING state::text`,
|
|
262
|
-
`ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE text USING state::text`,
|
|
263
|
-
`DROP TYPE ${schema}.job_state`,
|
|
264
|
-
`CREATE TYPE ${schema}.job_state AS ENUM ('created', 'retry', 'active', 'completed', 'expired', 'cancelled', 'failed')`,
|
|
265
|
-
`UPDATE ${schema}.job SET state = 'completed' WHERE state = 'complete'`,
|
|
266
|
-
`UPDATE ${schema}.archive SET state = 'completed' WHERE state = 'complete'`,
|
|
267
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
|
|
268
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT 'created'`,
|
|
269
|
-
`ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
|
|
270
|
-
`CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
|
|
271
|
-
`CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL`,
|
|
272
|
-
`CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'`,
|
|
273
|
-
`CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'completed' AND singletonOn IS NULL`,
|
|
274
|
-
// add new job name index
|
|
275
|
-
`CREATE INDEX job_name ON ${schema}.job (name) WHERE state < 'active'`
|
|
276
|
-
],
|
|
277
|
-
uninstall: [
|
|
278
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN id DROP DEFAULT`,
|
|
279
|
-
// won't know if we should drop pgcrypto extension so it stays
|
|
280
|
-
`ALTER TABLE ${schema}.job DROP COLUMN retryDelay`,
|
|
281
|
-
`ALTER TABLE ${schema}.job DROP COLUMN retryBackoff`,
|
|
282
|
-
`ALTER TABLE ${schema}.job DROP COLUMN startAfter`,
|
|
283
|
-
`ALTER TABLE ${schema}.job ADD COLUMN startIn interval not null default(interval '0')`,
|
|
284
|
-
// leaving migrated default data for expireIn
|
|
285
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN expireIn DROP NOT NULL`,
|
|
286
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN expireIn DROP DEFAULT`,
|
|
287
|
-
// archive table restore
|
|
288
|
-
`ALTER TABLE ${schema}.archive DROP COLUMN retryDelay`,
|
|
289
|
-
`ALTER TABLE ${schema}.archive DROP COLUMN retryBackoff`,
|
|
290
|
-
`ALTER TABLE ${schema}.archive DROP COLUMN startAfter`,
|
|
291
|
-
`ALTER TABLE ${schema}.archive ADD COLUMN startIn interval`,
|
|
292
|
-
// drop new job name index
|
|
293
|
-
`DROP INDEX ${schema}.job_name`,
|
|
294
|
-
// roll back to old enum def
|
|
295
|
-
`DROP INDEX ${schema}.job_fetch`,
|
|
296
|
-
`DROP INDEX ${schema}.job_singletonOn`,
|
|
297
|
-
`DROP INDEX ${schema}.job_singletonKeyOn`,
|
|
298
|
-
`DROP INDEX ${schema}.job_singletonKey`,
|
|
299
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
|
|
300
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE text USING state::text`,
|
|
301
|
-
`ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE text USING state::text`,
|
|
302
|
-
`DROP TYPE ${schema}.job_state`,
|
|
303
|
-
`CREATE TYPE ${schema}.job_state AS ENUM ('created', 'retry', 'active', 'complete', 'expired', 'cancelled', 'failed')`,
|
|
304
|
-
`UPDATE ${schema}.job SET state = 'completed' WHERE state = 'complete'`,
|
|
305
|
-
`UPDATE ${schema}.archive SET state = 'complete' WHERE state = 'completed'`,
|
|
306
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
|
|
307
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT 'created'`,
|
|
308
|
-
`ALTER TABLE ${schema}.archive ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`,
|
|
309
|
-
`CREATE INDEX job_fetch ON ${schema}.job (name, priority desc, createdOn, id) WHERE state < 'active'`,
|
|
310
|
-
`CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL`,
|
|
311
|
-
`CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'`,
|
|
312
|
-
`CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL`
|
|
313
|
-
]
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
release: '2.5.0',
|
|
317
|
-
version: 7,
|
|
318
|
-
previous: 6,
|
|
319
|
-
install: [
|
|
320
|
-
`CREATE TABLE IF NOT EXISTS ${schema}.archive (LIKE ${schema}.job)`,
|
|
321
|
-
`ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
|
|
322
|
-
],
|
|
323
|
-
uninstall: [
|
|
324
|
-
`DROP TABLE ${schema}.archive`
|
|
325
|
-
]
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
release: '2.0.0',
|
|
329
|
-
version: 6,
|
|
330
|
-
previous: 5,
|
|
331
|
-
install: [
|
|
332
|
-
`CREATE INDEX job_fetch ON ${schema}.job (priority desc, createdOn, id) WHERE state < 'active'`
|
|
333
|
-
],
|
|
334
|
-
uninstall: [
|
|
335
|
-
`DROP INDEX ${schema}.job_fetch`
|
|
336
|
-
]
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
release: '2.0.0',
|
|
340
|
-
version: 5,
|
|
341
|
-
previous: 4,
|
|
342
|
-
install: [
|
|
343
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN startIn SET DEFAULT (interval '0')`,
|
|
344
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state SET DEFAULT ('created')`,
|
|
345
|
-
`UPDATE ${schema}.job SET name = left(name, -9) || '__state__expired' WHERE name LIKE '%__expired'`
|
|
346
|
-
],
|
|
347
|
-
uninstall: [
|
|
348
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN startIn DROP DEFAULT`,
|
|
349
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN state DROP DEFAULT`,
|
|
350
|
-
`UPDATE ${schema}.job SET name = left(name, -16) || '__expired' WHERE name LIKE '%__state__expired'`
|
|
351
|
-
]
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
release: '1.1.0',
|
|
355
|
-
version: 4,
|
|
356
|
-
previous: 3,
|
|
357
|
-
install: [
|
|
358
|
-
`ALTER TABLE ${schema}.job ADD COLUMN priority integer not null default(0)`,
|
|
359
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN createdOn SET DATA TYPE timestamptz`,
|
|
360
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN startedOn SET DATA TYPE timestamptz`,
|
|
361
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN completedOn SET DATA TYPE timestamptz`
|
|
362
|
-
],
|
|
363
|
-
uninstall: [
|
|
364
|
-
`ALTER TABLE ${schema}.job DROP COLUMN priority`,
|
|
365
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN createdOn SET DATA TYPE timestamp`,
|
|
366
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN startedOn SET DATA TYPE timestamp`,
|
|
367
|
-
`ALTER TABLE ${schema}.job ALTER COLUMN completedOn SET DATA TYPE timestamp`
|
|
368
|
-
]
|
|
369
171
|
}
|
|
370
172
|
]
|
|
371
173
|
}
|
package/src/plans.js
CHANGED
|
@@ -13,6 +13,7 @@ const states = {
|
|
|
13
13
|
const DEFAULT_SCHEMA = 'pgboss'
|
|
14
14
|
const COMPLETION_JOB_PREFIX = `__state__${states.completed}__`
|
|
15
15
|
const SINGLETON_QUEUE_KEY = '__pgboss__singleton_queue'
|
|
16
|
+
const SINGLETON_QUEUE_KEY_ESCAPED = SINGLETON_QUEUE_KEY.replace(/_/g, '\\_')
|
|
16
17
|
|
|
17
18
|
const MIGRATE_RACE_MESSAGE = 'division by zero'
|
|
18
19
|
const CREATE_RACE_MESSAGE = 'already exists'
|
|
@@ -68,6 +69,7 @@ function locked (schema, query) {
|
|
|
68
69
|
|
|
69
70
|
return `
|
|
70
71
|
BEGIN;
|
|
72
|
+
SET LOCAL statement_timeout = '30s';
|
|
71
73
|
${advisoryLock(schema)};
|
|
72
74
|
${query};
|
|
73
75
|
COMMIT;
|
|
@@ -214,14 +216,14 @@ function getQueueSize (schema, options = {}) {
|
|
|
214
216
|
function createIndexSingletonKey (schema) {
|
|
215
217
|
// anything with singletonKey means "only 1 job can be queued or active at a time"
|
|
216
218
|
return `
|
|
217
|
-
CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < '${states.completed}' AND singletonOn IS NULL AND NOT singletonKey
|
|
219
|
+
CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < '${states.completed}' AND singletonOn IS NULL AND NOT singletonKey LIKE '${SINGLETON_QUEUE_KEY_ESCAPED}%'
|
|
218
220
|
`
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
function createIndexSingletonQueue (schema) {
|
|
222
224
|
// "singleton queue" means "only 1 job can be queued at a time"
|
|
223
225
|
return `
|
|
224
|
-
CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < '${states.active}' AND singletonOn IS NULL AND singletonKey
|
|
226
|
+
CREATE UNIQUE INDEX job_singleton_queue ON ${schema}.job (name, singletonKey) WHERE state < '${states.active}' AND singletonOn IS NULL AND singletonKey LIKE '${SINGLETON_QUEUE_KEY_ESCAPED}%'
|
|
225
227
|
`
|
|
226
228
|
}
|
|
227
229
|
|
package/src/timekeeper.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const pMap = require('p-map')
|
|
2
1
|
const EventEmitter = require('events')
|
|
3
2
|
const plans = require('./plans')
|
|
4
3
|
const cronParser = require('cron-parser')
|
|
@@ -44,18 +43,23 @@ class Timekeeper extends EventEmitter {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
async start () {
|
|
46
|
+
// setting the archive config too low breaks the cron 60s debounce interval so don't even try
|
|
47
47
|
if (this.config.archiveSeconds < 60) {
|
|
48
48
|
return
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// cache the clock skew from the db server
|
|
51
52
|
await this.cacheClockSkew()
|
|
52
53
|
|
|
53
54
|
await this.manager.work(queues.CRON, { newJobCheckIntervalSeconds: 4 }, (job) => this.onCron(job))
|
|
54
55
|
await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: 4, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
// uses sendDebounced() to enqueue a cron check
|
|
58
|
+
await this.checkSchedulesAsync()
|
|
57
59
|
|
|
60
|
+
// create monitoring interval to make sure cron hasn't crashed
|
|
58
61
|
this.cronMonitorInterval = setInterval(async () => await this.monitorCron(), this.cronMonitorIntervalMs)
|
|
62
|
+
// create monitoring interval to measure and adjust for drift in clock skew
|
|
59
63
|
this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
|
|
60
64
|
|
|
61
65
|
this.stopped = false
|
|
@@ -86,7 +90,7 @@ class Timekeeper extends EventEmitter {
|
|
|
86
90
|
const { secondsAgo } = await this.getCronTime()
|
|
87
91
|
|
|
88
92
|
if (secondsAgo > 60) {
|
|
89
|
-
await this.
|
|
93
|
+
await this.checkSchedulesAsync()
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -108,7 +112,7 @@ class Timekeeper extends EventEmitter {
|
|
|
108
112
|
this.clockSkew = skew
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
async
|
|
115
|
+
async checkSchedulesAsync () {
|
|
112
116
|
const opts = {
|
|
113
117
|
retryLimit: 2,
|
|
114
118
|
retentionSeconds: 60,
|
|
@@ -121,6 +125,8 @@ class Timekeeper extends EventEmitter {
|
|
|
121
125
|
async onCron () {
|
|
122
126
|
if (this.stopped) return
|
|
123
127
|
|
|
128
|
+
const { default: pMap } = await import('p-map')
|
|
129
|
+
|
|
124
130
|
try {
|
|
125
131
|
if (this.config.__test__throw_clock_monitoring) {
|
|
126
132
|
throw new Error('clock monitoring error')
|
|
@@ -136,6 +142,7 @@ class Timekeeper extends EventEmitter {
|
|
|
136
142
|
|
|
137
143
|
if (this.stopped) return
|
|
138
144
|
|
|
145
|
+
// set last time cron was evaluated for downstream usage in cron monitoring
|
|
139
146
|
await this.setCronTime()
|
|
140
147
|
} catch (err) {
|
|
141
148
|
this.emit(this.events.error, err)
|
|
@@ -143,7 +150,8 @@ class Timekeeper extends EventEmitter {
|
|
|
143
150
|
|
|
144
151
|
if (this.stopped) return
|
|
145
152
|
|
|
146
|
-
|
|
153
|
+
// uses sendDebounced() to enqueue a cron check
|
|
154
|
+
await this.checkSchedulesAsync()
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
shouldSendIt (cron, tz) {
|
package/src/worker.js
CHANGED
|
@@ -27,6 +27,15 @@ class Worker {
|
|
|
27
27
|
this.stopping = false
|
|
28
28
|
this.stopped = false
|
|
29
29
|
this.loopDelayPromise = null
|
|
30
|
+
this.beenNotified = false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
notify () {
|
|
34
|
+
this.beenNotified = true
|
|
35
|
+
|
|
36
|
+
if (this.loopDelayPromise) {
|
|
37
|
+
this.loopDelayPromise.clear()
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
async start () {
|
|
@@ -36,6 +45,7 @@ class Worker {
|
|
|
36
45
|
const started = Date.now()
|
|
37
46
|
|
|
38
47
|
try {
|
|
48
|
+
this.beenNotified = false
|
|
39
49
|
const jobs = await this.fetch()
|
|
40
50
|
|
|
41
51
|
this.lastFetchedOn = Date.now()
|
|
@@ -64,7 +74,7 @@ class Worker {
|
|
|
64
74
|
|
|
65
75
|
this.lastJobDuration = duration
|
|
66
76
|
|
|
67
|
-
if (!this.stopping && duration < this.interval) {
|
|
77
|
+
if (!this.stopping && !this.beenNotified && duration < this.interval) {
|
|
68
78
|
this.loopDelayPromise = delay(this.interval - duration)
|
|
69
79
|
await this.loopDelayPromise
|
|
70
80
|
this.loopDelayPromise = null
|
package/types.d.ts
CHANGED
|
@@ -84,13 +84,20 @@ declare namespace PgBoss {
|
|
|
84
84
|
priority?: number;
|
|
85
85
|
startAfter?: number | string | Date;
|
|
86
86
|
singletonKey?: string;
|
|
87
|
+
useSingletonQueue?: boolean;
|
|
87
88
|
singletonSeconds?: number;
|
|
88
89
|
singletonMinutes?: number;
|
|
89
90
|
singletonHours?: number;
|
|
90
91
|
singletonNextSlot?: boolean;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
interface ConnectionOptions {
|
|
95
|
+
db?: Db;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type InsertOptions = ConnectionOptions;
|
|
99
|
+
|
|
100
|
+
type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & CompletionOptions & ConnectionOptions;
|
|
94
101
|
|
|
95
102
|
type ScheduleOptions = SendOptions & { tz?: string }
|
|
96
103
|
|
|
@@ -111,7 +118,7 @@ declare namespace PgBoss {
|
|
|
111
118
|
|
|
112
119
|
type FetchOptions = {
|
|
113
120
|
includeMetadata?: boolean;
|
|
114
|
-
}
|
|
121
|
+
} & ConnectionOptions;
|
|
115
122
|
|
|
116
123
|
interface WorkHandler<ReqData, ResData> {
|
|
117
124
|
(job: PgBoss.JobWithDoneCallback<ReqData, ResData>): Promise<ResData> | void;
|
|
@@ -189,7 +196,7 @@ declare namespace PgBoss {
|
|
|
189
196
|
retryLimit?: number;
|
|
190
197
|
retryDelay?: number;
|
|
191
198
|
retryBackoff?: boolean;
|
|
192
|
-
startAfter?: Date | string;
|
|
199
|
+
startAfter?: Date | string;
|
|
193
200
|
singletonKey?: string;
|
|
194
201
|
expireInSeconds?: number;
|
|
195
202
|
keepUntil?: Date | string;
|
|
@@ -297,6 +304,7 @@ declare class PgBoss extends EventEmitter {
|
|
|
297
304
|
sendDebounced(name: string, data: object, options: PgBoss.SendOptions, seconds: number, key: string): Promise<string | null>;
|
|
298
305
|
|
|
299
306
|
insert(jobs: PgBoss.JobInsert[]): Promise<void>;
|
|
307
|
+
insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
|
|
300
308
|
|
|
301
309
|
work<ReqData, ResData>(name: string, handler: PgBoss.WorkHandler<ReqData, ResData>): Promise<string>;
|
|
302
310
|
work<ReqData, ResData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData, ResData>): Promise<string>;
|
|
@@ -308,6 +316,12 @@ declare class PgBoss extends EventEmitter {
|
|
|
308
316
|
offWork(name: string): Promise<void>;
|
|
309
317
|
offWork(options: PgBoss.OffWorkOptions): Promise<void>;
|
|
310
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Notify worker that something has changed
|
|
321
|
+
* @param workerId
|
|
322
|
+
*/
|
|
323
|
+
notifyWorker(workerId: string): void;
|
|
324
|
+
|
|
311
325
|
subscribe(event: string, name: string): Promise<void>;
|
|
312
326
|
unsubscribe(event: string, name: string): Promise<void>;
|
|
313
327
|
publish(event: string): Promise<string[]>;
|
package/version.json
CHANGED