pg-boss 10.0.0-beta3 → 10.0.0-beta5
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 +9 -14
- package/package.json +1 -1
- package/src/attorney.js +6 -6
- package/src/boss.js +2 -8
- package/src/db.js +2 -6
- package/src/index.js +7 -5
- package/src/plans.js +7 -3
- package/src/timekeeper.js +39 -56
package/README.md
CHANGED
|
@@ -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',
|
|
12
|
+
boss.on('error', console.error)
|
|
13
13
|
|
|
14
|
-
await boss.start()
|
|
14
|
+
await boss.start()
|
|
15
15
|
|
|
16
|
-
const queue = '
|
|
16
|
+
const queue = 'readme-queue'
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const id = await boss.send(queue, { arg1: 'read me' })
|
|
19
19
|
|
|
20
|
-
console.log(`created job in queue ${queue}
|
|
20
|
+
console.log(`created job ${id} in queue ${queue}`)
|
|
21
21
|
|
|
22
|
-
await boss.work(queue,
|
|
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
|
|
|
@@ -48,7 +43,7 @@ This will likely cater the most to teams already familiar with the simplicity of
|
|
|
48
43
|
|
|
49
44
|
## Requirements
|
|
50
45
|
* Node 20 or higher
|
|
51
|
-
* PostgreSQL
|
|
46
|
+
* PostgreSQL 13 or higher
|
|
52
47
|
|
|
53
48
|
## Installation
|
|
54
49
|
|
package/package.json
CHANGED
package/src/attorney.js
CHANGED
|
@@ -368,21 +368,21 @@ function applyMonitoringConfig (config) {
|
|
|
368
368
|
? config.clockMonitorIntervalSeconds
|
|
369
369
|
: TEN_MINUTES_IN_SECONDS
|
|
370
370
|
|
|
371
|
-
assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <=
|
|
372
|
-
'configuration assert: cronMonitorIntervalSeconds must be between 1 and
|
|
371
|
+
assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
|
|
372
|
+
'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
|
|
373
373
|
|
|
374
374
|
config.cronMonitorIntervalSeconds =
|
|
375
375
|
('cronMonitorIntervalSeconds' in config)
|
|
376
376
|
? config.cronMonitorIntervalSeconds
|
|
377
|
-
:
|
|
377
|
+
: 30
|
|
378
378
|
|
|
379
|
-
assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <=
|
|
380
|
-
'configuration assert: cronWorkerIntervalSeconds must be between 1 and
|
|
379
|
+
assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
|
|
380
|
+
'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
|
|
381
381
|
|
|
382
382
|
config.cronWorkerIntervalSeconds =
|
|
383
383
|
('cronWorkerIntervalSeconds' in config)
|
|
384
384
|
? config.cronWorkerIntervalSeconds
|
|
385
|
-
:
|
|
385
|
+
: 5
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
function warnClockSkew (message) {
|
package/src/boss.js
CHANGED
|
@@ -77,10 +77,7 @@ class Boss extends EventEmitter {
|
|
|
77
77
|
} catch (err) {
|
|
78
78
|
this.emit(events.error, err)
|
|
79
79
|
} finally {
|
|
80
|
-
|
|
81
|
-
await locker.unlock()
|
|
82
|
-
}
|
|
83
|
-
|
|
80
|
+
await locker?.unlock()
|
|
84
81
|
this.monitoring = false
|
|
85
82
|
}
|
|
86
83
|
}
|
|
@@ -119,10 +116,7 @@ class Boss extends EventEmitter {
|
|
|
119
116
|
} catch (err) {
|
|
120
117
|
this.emit(events.error, err)
|
|
121
118
|
} finally {
|
|
122
|
-
|
|
123
|
-
await locker.unlock()
|
|
124
|
-
}
|
|
125
|
-
|
|
119
|
+
await locker?.unlock()
|
|
126
120
|
this.maintaining = false
|
|
127
121
|
}
|
|
128
122
|
}
|
package/src/db.js
CHANGED
|
@@ -45,27 +45,23 @@ class Db extends EventEmitter {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
async lock ({ timeout = 30, key } = {}) {
|
|
48
|
-
// const lockedClient = new pg.Client(this.config)
|
|
49
|
-
// await lockedClient.connect()
|
|
50
48
|
const lockedClient = await this.pool.connect()
|
|
51
49
|
|
|
52
50
|
const query = `
|
|
53
51
|
BEGIN;
|
|
54
52
|
SET LOCAL lock_timeout = '${timeout}s';
|
|
55
|
-
SET LOCAL idle_in_transaction_session_timeout = '
|
|
53
|
+
SET LOCAL idle_in_transaction_session_timeout = '${timeout}s';
|
|
56
54
|
${advisoryLock(this.config.schema, key)};
|
|
57
55
|
`
|
|
58
56
|
|
|
59
57
|
await lockedClient.query(query)
|
|
60
58
|
|
|
61
59
|
const locker = {
|
|
62
|
-
locked: true,
|
|
63
60
|
unlock: async function () {
|
|
64
61
|
try {
|
|
65
62
|
await lockedClient.query('COMMIT')
|
|
66
|
-
await lockedClient.end()
|
|
67
63
|
} finally {
|
|
68
|
-
|
|
64
|
+
lockedClient.release()
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
67
|
}
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,8 @@ const events = {
|
|
|
15
15
|
class PgBoss extends EventEmitter {
|
|
16
16
|
#stoppingOn
|
|
17
17
|
#stopped
|
|
18
|
+
#starting
|
|
19
|
+
#started
|
|
18
20
|
#config
|
|
19
21
|
#db
|
|
20
22
|
#boss
|
|
@@ -102,11 +104,11 @@ class PgBoss extends EventEmitter {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
async start () {
|
|
105
|
-
if (this
|
|
107
|
+
if (this.#starting || this.#started) {
|
|
106
108
|
return
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
this
|
|
111
|
+
this.#starting = true
|
|
110
112
|
|
|
111
113
|
if (this.#db.isOurs && !this.#db.opened) {
|
|
112
114
|
await this.#db.open()
|
|
@@ -132,8 +134,8 @@ class PgBoss extends EventEmitter {
|
|
|
132
134
|
await this.#timekeeper.start()
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
this
|
|
136
|
-
this
|
|
137
|
+
this.#starting = false
|
|
138
|
+
this.#started = true
|
|
137
139
|
this.#stopped = false
|
|
138
140
|
|
|
139
141
|
return this
|
|
@@ -169,7 +171,7 @@ class PgBoss extends EventEmitter {
|
|
|
169
171
|
|
|
170
172
|
this.#stopped = true
|
|
171
173
|
this.#stoppingOn = null
|
|
172
|
-
this
|
|
174
|
+
this.#started = false
|
|
173
175
|
|
|
174
176
|
this.emit(events.stopped)
|
|
175
177
|
resolve()
|
package/src/plans.js
CHANGED
|
@@ -174,7 +174,7 @@ function getPartitionFunction (schema) {
|
|
|
174
174
|
return `
|
|
175
175
|
CREATE FUNCTION ${schema}.get_partition(queue_name text, out name text) AS
|
|
176
176
|
$$
|
|
177
|
-
SELECT '${schema}.
|
|
177
|
+
SELECT '${schema}.job_' || encode(sha224(queue_name::bytea), 'hex');
|
|
178
178
|
$$
|
|
179
179
|
LANGUAGE SQL
|
|
180
180
|
IMMUTABLE
|
|
@@ -315,6 +315,8 @@ function getQueueByName (schema) {
|
|
|
315
315
|
function deleteQueueRecords (schema) {
|
|
316
316
|
return `WITH dq AS (
|
|
317
317
|
DELETE FROM ${schema}.queue WHERE name = $1
|
|
318
|
+
), ds AS (
|
|
319
|
+
DELETE FROM ${schema}.schedule WHERE name = $1
|
|
318
320
|
)
|
|
319
321
|
DELETE FROM ${schema}.job WHERE name = $1
|
|
320
322
|
`
|
|
@@ -378,7 +380,9 @@ function createTableSubscription (schema) {
|
|
|
378
380
|
|
|
379
381
|
function getSchedules (schema) {
|
|
380
382
|
return `
|
|
381
|
-
SELECT
|
|
383
|
+
SELECT s.*
|
|
384
|
+
FROM ${schema}.schedule s
|
|
385
|
+
JOIN ${schema}.queue q on s.name = q.name
|
|
382
386
|
`
|
|
383
387
|
}
|
|
384
388
|
|
|
@@ -765,7 +769,7 @@ function locked (schema, query) {
|
|
|
765
769
|
|
|
766
770
|
function advisoryLock (schema, key) {
|
|
767
771
|
return `SELECT pg_advisory_xact_lock(
|
|
768
|
-
('x' || encode(
|
|
772
|
+
('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ''}')::bytea), 'hex'))::bit(64)::bigint
|
|
769
773
|
)`
|
|
770
774
|
}
|
|
771
775
|
|
package/src/timekeeper.js
CHANGED
|
@@ -5,7 +5,6 @@ const Attorney = require('./attorney')
|
|
|
5
5
|
const pMap = require('p-map')
|
|
6
6
|
|
|
7
7
|
const queues = {
|
|
8
|
-
CRON: '__pgboss__cron',
|
|
9
8
|
SEND_IT: '__pgboss__send-it'
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -28,6 +27,7 @@ class Timekeeper extends EventEmitter {
|
|
|
28
27
|
this.events = events
|
|
29
28
|
|
|
30
29
|
this.getTimeCommand = plans.getTime(config.schema)
|
|
30
|
+
this.getQueueCommand = plans.getQueueByName(config.schema)
|
|
31
31
|
this.getSchedulesCommand = plans.getSchedules(config.schema)
|
|
32
32
|
this.scheduleCommand = plans.schedule(config.schema)
|
|
33
33
|
this.unscheduleCommand = plans.unschedule(config.schema)
|
|
@@ -44,6 +44,8 @@ class Timekeeper extends EventEmitter {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async start () {
|
|
47
|
+
this.stopped = false
|
|
48
|
+
|
|
47
49
|
// setting the archive config too low breaks the cron 60s debounce interval so don't even try
|
|
48
50
|
if (this.config.archiveSeconds < 60 || this.config.archiveFailedSeconds < 60) {
|
|
49
51
|
return
|
|
@@ -52,21 +54,18 @@ class Timekeeper extends EventEmitter {
|
|
|
52
54
|
// cache the clock skew from the db server
|
|
53
55
|
await this.cacheClockSkew()
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
try {
|
|
58
|
+
await this.manager.createQueue(queues.SEND_IT)
|
|
59
|
+
} catch {}
|
|
57
60
|
|
|
58
|
-
await this.manager.work(queues.CRON, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds }, (job) => this.onCron(job))
|
|
59
61
|
await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
await this.checkSchedulesAsync()
|
|
63
|
+
setImmediate(() => this.onCron())
|
|
63
64
|
|
|
64
65
|
// create monitoring interval to make sure cron hasn't crashed
|
|
65
|
-
this.cronMonitorInterval = setInterval(async () => await this.
|
|
66
|
+
this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.cronMonitorIntervalMs)
|
|
66
67
|
// create monitoring interval to measure and adjust for drift in clock skew
|
|
67
68
|
this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
|
|
68
|
-
|
|
69
|
-
this.stopped = false
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
async stop () {
|
|
@@ -76,7 +75,6 @@ class Timekeeper extends EventEmitter {
|
|
|
76
75
|
|
|
77
76
|
this.stopped = true
|
|
78
77
|
|
|
79
|
-
await this.manager.offWork(queues.CRON)
|
|
80
78
|
await this.manager.offWork(queues.SEND_IT)
|
|
81
79
|
|
|
82
80
|
if (this.skewMonitorInterval) {
|
|
@@ -90,22 +88,6 @@ class Timekeeper extends EventEmitter {
|
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
async monitorCron () {
|
|
94
|
-
try {
|
|
95
|
-
if (this.config.__test__force_cron_monitoring_error) {
|
|
96
|
-
throw new Error(this.config.__test__force_cron_monitoring_error)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const { secondsAgo } = await this.getCronTime()
|
|
100
|
-
|
|
101
|
-
if (secondsAgo > 60) {
|
|
102
|
-
await this.checkSchedulesAsync()
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
this.emit(this.events.error, err)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
91
|
async cacheClockSkew () {
|
|
110
92
|
let skew = 0
|
|
111
93
|
|
|
@@ -134,43 +116,42 @@ class Timekeeper extends EventEmitter {
|
|
|
134
116
|
}
|
|
135
117
|
}
|
|
136
118
|
|
|
137
|
-
async checkSchedulesAsync () {
|
|
138
|
-
const opts = {
|
|
139
|
-
retryLimit: 2,
|
|
140
|
-
retentionSeconds: 60
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
await this.manager.sendDebounced(queues.CRON, null, opts, 60)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
119
|
async onCron () {
|
|
147
|
-
|
|
120
|
+
let locker
|
|
148
121
|
|
|
149
122
|
try {
|
|
150
|
-
if (this.
|
|
151
|
-
throw new Error(this.config.__test__throw_cron_processing)
|
|
152
|
-
}
|
|
123
|
+
if (this.stopped || this.timekeeping) return
|
|
153
124
|
|
|
154
|
-
|
|
125
|
+
if (this.config.__test__force_cron_monitoring_error) {
|
|
126
|
+
throw new Error(this.config.__test__force_cron_monitoring_error)
|
|
127
|
+
}
|
|
155
128
|
|
|
156
|
-
|
|
129
|
+
this.timekeeping = true
|
|
157
130
|
|
|
158
|
-
|
|
159
|
-
await pMap(sending, it => this.send(it), { concurrency: 5 })
|
|
160
|
-
}
|
|
131
|
+
locker = await this.db.lock({ key: 'timekeeper' })
|
|
161
132
|
|
|
162
|
-
|
|
133
|
+
const { secondsAgo } = await this.getCronTime()
|
|
163
134
|
|
|
164
|
-
|
|
165
|
-
|
|
135
|
+
if (secondsAgo > this.config.cronMonitorIntervalSeconds) {
|
|
136
|
+
await this.cron()
|
|
137
|
+
await this.setCronTime()
|
|
138
|
+
}
|
|
166
139
|
} catch (err) {
|
|
167
140
|
this.emit(this.events.error, err)
|
|
141
|
+
} finally {
|
|
142
|
+
this.timekeeping = false
|
|
143
|
+
await locker?.unlock()
|
|
168
144
|
}
|
|
145
|
+
}
|
|
169
146
|
|
|
170
|
-
|
|
147
|
+
async cron () {
|
|
148
|
+
const items = await this.getSchedules()
|
|
171
149
|
|
|
172
|
-
|
|
173
|
-
|
|
150
|
+
const sending = items.filter(i => this.shouldSendIt(i.cron, i.timezone))
|
|
151
|
+
|
|
152
|
+
if (sending.length && !this.stopped) {
|
|
153
|
+
await pMap(sending, it => this.send(it), { concurrency: 5 })
|
|
154
|
+
}
|
|
174
155
|
}
|
|
175
156
|
|
|
176
157
|
shouldSendIt (cron, tz) {
|
|
@@ -186,12 +167,7 @@ class Timekeeper extends EventEmitter {
|
|
|
186
167
|
}
|
|
187
168
|
|
|
188
169
|
async send (job) {
|
|
189
|
-
|
|
190
|
-
singletonKey: job.name,
|
|
191
|
-
singletonSeconds: 60
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
await this.manager.send(queues.SEND_IT, job, options)
|
|
170
|
+
await this.manager.send(queues.SEND_IT, job, { singletonKey: job.name, singletonSeconds: 60 })
|
|
195
171
|
}
|
|
196
172
|
|
|
197
173
|
async onSendIt (job) {
|
|
@@ -213,6 +189,13 @@ class Timekeeper extends EventEmitter {
|
|
|
213
189
|
// validation pre-check
|
|
214
190
|
Attorney.checkSendArgs([name, data, options], this.config)
|
|
215
191
|
|
|
192
|
+
// make sure queue exists before scheduling
|
|
193
|
+
const queue = await this.db.executeSql(this.getQueueCommand, [name])
|
|
194
|
+
|
|
195
|
+
if (!queue.rows.length === 0) {
|
|
196
|
+
throw new Error(`Queue '${name}' not found`)
|
|
197
|
+
}
|
|
198
|
+
|
|
216
199
|
const values = [name, cron, tz, data, options]
|
|
217
200
|
|
|
218
201
|
const result = await this.db.executeSql(this.scheduleCommand, values)
|