pg-boss 9.0.2 → 10.0.0-beta1

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/src/boss.js CHANGED
@@ -1,12 +1,6 @@
1
1
  const EventEmitter = require('events')
2
2
  const plans = require('./plans')
3
- const { states } = require('./plans')
4
- const { COMPLETION_JOB_PREFIX } = plans
5
-
6
- const queues = {
7
- MAINTENANCE: '__pgboss__maintenance',
8
- MONITOR_STATES: '__pgboss__monitor-states'
9
- }
3
+ const { delay } = require('./tools')
10
4
 
11
5
  const events = {
12
6
  error: 'error',
@@ -23,165 +17,135 @@ class Boss extends EventEmitter {
23
17
  this.manager = config.manager
24
18
 
25
19
  this.maintenanceIntervalSeconds = config.maintenanceIntervalSeconds
26
-
27
- this.monitorStates = config.monitorStateIntervalSeconds !== null
28
-
29
- if (this.monitorStates) {
30
- this.monitorIntervalSeconds = config.monitorStateIntervalSeconds
31
- }
20
+ this.monitorStateIntervalSeconds = config.monitorStateIntervalSeconds
32
21
 
33
22
  this.events = events
34
23
 
35
- this.expireCommand = plans.locked(config.schema, plans.expire(config.schema))
24
+ this.failJobsByTimeoutCommand = plans.locked(config.schema, plans.failJobsByTimeout(config.schema))
36
25
  this.archiveCommand = plans.locked(config.schema, plans.archive(config.schema, config.archiveInterval, config.archiveFailedInterval))
37
- this.purgeCommand = plans.locked(config.schema, plans.purge(config.schema, config.deleteAfter))
26
+ this.dropCommand = plans.locked(config.schema, plans.drop(config.schema, config.deleteAfter))
38
27
  this.getMaintenanceTimeCommand = plans.getMaintenanceTime(config.schema)
39
28
  this.setMaintenanceTimeCommand = plans.setMaintenanceTime(config.schema)
29
+ this.getMonitorTimeCommand = plans.getMonitorTime(config.schema)
30
+ this.setMonitorTimeCommand = plans.setMonitorTime(config.schema)
40
31
  this.countStatesCommand = plans.countStates(config.schema)
41
32
 
42
33
  this.functions = [
43
34
  this.expire,
44
35
  this.archive,
45
- this.purge,
36
+ this.drop,
46
37
  this.countStates,
47
- this.getQueueNames
38
+ this.maintain
48
39
  ]
49
40
  }
50
41
 
51
42
  async supervise () {
52
- this.metaMonitor()
43
+ this.maintenanceInterval = setInterval(() => this.onSupervise(), this.maintenanceIntervalSeconds * 1000)
44
+ }
53
45
 
54
- await this.manager.deleteQueue(COMPLETION_JOB_PREFIX + queues.MAINTENANCE)
55
- await this.manager.deleteQueue(queues.MAINTENANCE)
46
+ async monitor () {
47
+ this.monitorInterval = setInterval(() => this.onMonitor(), this.monitorStateIntervalSeconds * 1000)
48
+ }
56
49
 
57
- await this.maintenanceAsync()
50
+ async onMonitor () {
51
+ let locker
58
52
 
59
- const maintenanceWorkOptions = {
60
- newJobCheckIntervalSeconds: Math.max(1, this.maintenanceIntervalSeconds / 2)
61
- }
62
-
63
- await this.manager.work(queues.MAINTENANCE, maintenanceWorkOptions, (job) => this.onMaintenance(job))
53
+ try {
54
+ if (this.monitoring) {
55
+ return
56
+ }
64
57
 
65
- if (this.monitorStates) {
66
- await this.manager.deleteQueue(COMPLETION_JOB_PREFIX + queues.MONITOR_STATES)
67
- await this.manager.deleteQueue(queues.MONITOR_STATES)
58
+ this.monitoring = true
68
59
 
69
- await this.monitorStatesAsync()
60
+ if (this.config.__test__delay_monitor) {
61
+ await delay(this.config.__test__delay_monitor)
62
+ }
70
63
 
71
- const monitorStatesWorkOptions = {
72
- newJobCheckIntervalSeconds: Math.max(1, this.monitorIntervalSeconds / 2)
64
+ if (this.config.__test__throw_monitor) {
65
+ throw new Error(this.config.__test__throw_monitor)
73
66
  }
74
67
 
75
- await this.manager.work(queues.MONITOR_STATES, monitorStatesWorkOptions, (job) => this.onMonitorStates(job))
76
- }
77
- }
68
+ locker = await this.db.lock({ key: 'monitor' })
78
69
 
79
- metaMonitor () {
80
- this.metaMonitorInterval = setInterval(async () => {
81
- try {
82
- if (this.config.__test__throw_meta_monitor) {
83
- throw new Error(this.config.__test__throw_meta_monitor)
84
- }
85
-
86
- const { secondsAgo } = await this.getMaintenanceTime()
87
-
88
- if (secondsAgo > this.maintenanceIntervalSeconds * 2) {
89
- await this.manager.deleteQueue(queues.MAINTENANCE, { before: states.completed })
90
- await this.maintenanceAsync()
91
- }
92
- } catch (err) {
93
- this.emit(events.error, err)
94
- }
95
- }, this.maintenanceIntervalSeconds * 2 * 1000)
96
- }
70
+ const { secondsAgo } = await this.getMonitorTime()
97
71
 
98
- async maintenanceAsync (options = {}) {
99
- const { startAfter } = options
72
+ if (secondsAgo > this.monitorStateIntervalSeconds && !this.stopped) {
73
+ const states = await this.countStates()
74
+ this.setMonitorTime()
75
+ this.emit(events.monitorStates, states)
76
+ }
77
+ } catch (err) {
78
+ this.emit(events.error, err)
79
+ } finally {
80
+ if (locker?.locked) {
81
+ await locker.unlock()
82
+ }
100
83
 
101
- options = {
102
- startAfter,
103
- retentionSeconds: this.maintenanceIntervalSeconds * 4,
104
- singletonKey: queues.MAINTENANCE,
105
- onComplete: false
84
+ this.monitoring = false
106
85
  }
107
-
108
- await this.manager.send(queues.MAINTENANCE, null, options)
109
86
  }
110
87
 
111
- async monitorStatesAsync (options = {}) {
112
- const { startAfter } = options
88
+ async onSupervise () {
89
+ let locker
113
90
 
114
- options = {
115
- startAfter,
116
- retentionSeconds: this.monitorIntervalSeconds * 4,
117
- singletonKey: queues.MONITOR_STATES,
118
- onComplete: false
119
- }
91
+ try {
92
+ if (this.maintaining) {
93
+ return
94
+ }
120
95
 
121
- await this.manager.send(queues.MONITOR_STATES, null, options)
122
- }
96
+ this.maintaining = true
97
+
98
+ if (this.config.__test__delay_maintenance && !this.stopped) {
99
+ this.__testDelayPromise = delay(this.config.__test__delay_maintenance)
100
+ await this.__testDelayPromise
101
+ }
123
102
 
124
- async onMaintenance (job) {
125
- try {
126
103
  if (this.config.__test__throw_maint) {
127
104
  throw new Error(this.config.__test__throw_maint)
128
105
  }
129
106
 
130
- const started = Date.now()
131
-
132
- await this.expire()
133
- await this.archive()
134
- await this.purge()
135
-
136
- const ended = Date.now()
107
+ if (this.stopped) {
108
+ return
109
+ }
137
110
 
138
- await this.setMaintenanceTime()
111
+ locker = await this.db.lock({ key: 'maintenance' })
139
112
 
140
- this.emit('maintenance', { ms: ended - started })
113
+ const { secondsAgo } = await this.getMaintenanceTime()
141
114
 
142
- if (!this.stopped) {
143
- await this.manager.complete(job.id) // pre-complete to bypass throttling
144
- await this.maintenanceAsync({ startAfter: this.maintenanceIntervalSeconds })
115
+ if (secondsAgo > this.maintenanceIntervalSeconds) {
116
+ const result = await this.maintain()
117
+ this.emit(events.maintenance, result)
145
118
  }
146
119
  } catch (err) {
147
120
  this.emit(events.error, err)
121
+ } finally {
122
+ if (locker?.locked) {
123
+ await locker.unlock()
124
+ }
125
+
126
+ this.maintaining = false
148
127
  }
149
128
  }
150
129
 
151
- async onMonitorStates (job) {
152
- try {
153
- if (this.config.__test__throw_monitor) {
154
- throw new Error(this.config.__test__throw_monitor)
155
- }
130
+ async maintain () {
131
+ const started = Date.now()
156
132
 
157
- const states = await this.countStates()
133
+ !this.stopped && await this.expire()
134
+ !this.stopped && await this.archive()
135
+ !this.stopped && await this.drop()
158
136
 
159
- this.emit(events.monitorStates, states)
137
+ const ended = Date.now()
160
138
 
161
- if (!this.stopped && this.monitorStates) {
162
- await this.manager.complete(job.id) // pre-complete to bypass throttling
163
- await this.monitorStatesAsync({ startAfter: this.monitorIntervalSeconds })
164
- }
165
- } catch (err) {
166
- this.emit(events.error, err)
167
- }
139
+ await this.setMaintenanceTime()
140
+
141
+ return { ms: ended - started }
168
142
  }
169
143
 
170
144
  async stop () {
171
- if (this.config.__test__throw_stop) {
172
- throw new Error(this.config.__test__throw_stop)
173
- }
174
-
175
145
  if (!this.stopped) {
176
- if (this.metaMonitorInterval) {
177
- clearInterval(this.metaMonitorInterval)
178
- }
179
-
180
- await this.manager.offWork(queues.MAINTENANCE)
181
-
182
- if (this.monitorStates) {
183
- await this.manager.offWork(queues.MONITOR_STATES)
184
- }
146
+ if (this.__testDelayPromise) this.__testDelayPromise.abort()
147
+ if (this.maintenanceInterval) clearInterval(this.maintenanceInterval)
148
+ if (this.monitorInterval) clearInterval(this.monitorInterval)
185
149
 
186
150
  this.stopped = true
187
151
  }
@@ -193,7 +157,7 @@ class Boss extends EventEmitter {
193
157
  Object.keys(stateCountDefault)
194
158
  .forEach(key => { stateCountDefault[key] = 0 })
195
159
 
196
- const counts = await this.executeSql(this.countStatesCommand)
160
+ const counts = await this.db.executeSql(this.countStatesCommand)
197
161
 
198
162
  const states = counts.rows.reduce((acc, item) => {
199
163
  if (item.name) {
@@ -213,43 +177,44 @@ class Boss extends EventEmitter {
213
177
  }
214
178
 
215
179
  async expire () {
216
- await this.executeSql(this.expireCommand)
180
+ await this.db.executeSql(this.failJobsByTimeoutCommand)
217
181
  }
218
182
 
219
183
  async archive () {
220
- await this.executeSql(this.archiveCommand)
184
+ await this.db.executeSql(this.archiveCommand)
221
185
  }
222
186
 
223
- async purge () {
224
- await this.executeSql(this.purgeCommand)
187
+ async drop () {
188
+ await this.db.executeSql(this.dropCommand)
225
189
  }
226
190
 
227
191
  async setMaintenanceTime () {
228
- await this.executeSql(this.setMaintenanceTimeCommand)
192
+ await this.db.executeSql(this.setMaintenanceTimeCommand)
229
193
  }
230
194
 
231
195
  async getMaintenanceTime () {
232
- if (!this.stopped) {
233
- const { rows } = await this.db.executeSql(this.getMaintenanceTimeCommand)
196
+ const { rows } = await this.db.executeSql(this.getMaintenanceTimeCommand)
234
197
 
235
- let { maintained_on: maintainedOn, seconds_ago: secondsAgo } = rows[0]
198
+ let { maintained_on: maintainedOn, seconds_ago: secondsAgo } = rows[0]
236
199
 
237
- secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : this.maintenanceIntervalSeconds * 10
200
+ secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 999_999_999
238
201
 
239
- return { maintainedOn, secondsAgo }
240
- }
202
+ return { maintainedOn, secondsAgo }
241
203
  }
242
204
 
243
- getQueueNames () {
244
- return queues
205
+ async setMonitorTime () {
206
+ await this.db.executeSql(this.setMonitorTimeCommand)
245
207
  }
246
208
 
247
- async executeSql (sql, params) {
248
- if (!this.stopped) {
249
- return await this.db.executeSql(sql, params)
250
- }
209
+ async getMonitorTime () {
210
+ const { rows } = await this.db.executeSql(this.getMonitorTimeCommand)
211
+
212
+ let { monitored_on: monitoredOn, seconds_ago: secondsAgo } = rows[0]
213
+
214
+ secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 999_999_999
215
+
216
+ return { monitoredOn, secondsAgo }
251
217
  }
252
218
  }
253
219
 
254
220
  module.exports = Boss
255
- module.exports.QUEUES = queues
package/src/contractor.js CHANGED
@@ -47,6 +47,20 @@ class Contractor {
47
47
  }
48
48
  }
49
49
 
50
+ async check () {
51
+ const installed = await this.isInstalled()
52
+
53
+ if (!installed) {
54
+ throw new Error('pg-boss is not installed')
55
+ }
56
+
57
+ const version = await this.version()
58
+
59
+ if (schemaVersion !== version) {
60
+ throw new Error('pg-boss database requires migrations')
61
+ }
62
+ }
63
+
50
64
  async create () {
51
65
  try {
52
66
  const commands = plans.create(this.config.schema, schemaVersion)
package/src/db.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const EventEmitter = require('events')
2
2
  const pg = require('pg')
3
+ const { advisoryLock } = require('./plans')
3
4
 
4
5
  class Db extends EventEmitter {
5
6
  constructor (config) {
@@ -25,10 +26,49 @@ class Db extends EventEmitter {
25
26
 
26
27
  async executeSql (text, values) {
27
28
  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
+
28
39
  return await this.pool.query(text, values)
29
40
  }
30
41
  }
31
42
 
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
65
+ }
66
+ }
67
+ }
68
+
69
+ return locker
70
+ }
71
+
32
72
  static quotePostgresStr (str) {
33
73
  const delimeter = '$sanitize$'
34
74
  if (str.includes(delimeter)) {
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ const Manager = require('./manager')
6
6
  const Timekeeper = require('./timekeeper')
7
7
  const Boss = require('./boss')
8
8
  const Db = require('./db')
9
- const delay = require('delay')
9
+ const { delay } = require('./tools')
10
10
 
11
11
  const events = {
12
12
  error: 'error',
@@ -72,16 +72,7 @@ class PgBoss extends EventEmitter {
72
72
  }
73
73
 
74
74
  function promoteFunction (obj, func) {
75
- this[func.name] = (...args) => {
76
- const shouldRun = !this.started || !((func.name === 'work' || func.name === 'onComplete') && (this.stopped || this.stoppingOn))
77
-
78
- if (shouldRun) {
79
- return func.apply(obj, args)
80
- } else {
81
- const state = this.stoppingOn ? 'stopping' : this.stopped ? 'stopped' : !this.started ? 'not started' : 'started'
82
- return Promise.reject(new Error(`pg-boss is ${state}.`))
83
- }
84
- }
75
+ this[func.name] = (...args) => func.apply(obj, args)
85
76
  }
86
77
 
87
78
  function promoteEvent (emitter, event) {
@@ -90,42 +81,49 @@ class PgBoss extends EventEmitter {
90
81
  }
91
82
 
92
83
  async start () {
93
- if (!this.stopped) {
94
- return this
84
+ if (this.starting || this.started) {
85
+ return
95
86
  }
96
87
 
88
+ this.starting = true
89
+
97
90
  if (this.db.isOurs && !this.db.opened) {
98
91
  await this.db.open()
99
92
  }
100
93
 
101
- await this.contractor.start()
102
-
103
- this.stopped = false
104
- this.started = true
94
+ if (this.config.migrate) {
95
+ await this.contractor.start()
96
+ } else {
97
+ await this.contractor.check()
98
+ }
105
99
 
106
100
  this.manager.start()
107
101
 
108
- if (!this.config.noSupervisor) {
102
+ if (this.config.supervise) {
109
103
  await this.boss.supervise()
110
104
  }
111
105
 
112
- if (!this.config.noScheduling) {
106
+ if (this.config.monitorStateIntervalSeconds) {
107
+ await this.boss.monitor()
108
+ }
109
+
110
+ if (this.config.schedule) {
113
111
  await this.timekeeper.start()
114
112
  }
115
113
 
114
+ this.starting = false
115
+ this.started = true
116
+ this.stopped = false
117
+
116
118
  return this
117
119
  }
118
120
 
119
121
  async stop (options = {}) {
120
- if (this.stoppingOn) {
122
+ if (this.stoppingOn || this.stopped) {
121
123
  return
122
124
  }
123
125
 
124
- if (this.stopped) {
125
- this.emit(events.stopped)
126
- }
127
-
128
- let { destroy = false, graceful = true, timeout = 30000 } = options
126
+ let { destroy = false, graceful = true, timeout = 30000, wait = true } = options
129
127
 
130
128
  timeout = Math.max(timeout, 1000)
131
129
 
@@ -133,47 +131,59 @@ class PgBoss extends EventEmitter {
133
131
 
134
132
  await this.manager.stop()
135
133
  await this.timekeeper.stop()
134
+ await this.boss.stop()
136
135
 
137
- const shutdown = async () => {
138
- this.stopped = true
139
- this.stoppingOn = null
136
+ await new Promise((resolve, reject) => {
137
+ const shutdown = async () => {
138
+ try {
139
+ if (this.config.__test__throw_shutdown) {
140
+ throw new Error(this.config.__test__throw_shutdown)
141
+ }
140
142
 
141
- if (this.db.isOurs && this.db.opened && destroy) {
142
- await this.db.close()
143
- }
143
+ await this.manager.failWip()
144
144
 
145
- this.emit(events.stopped)
146
- }
145
+ if (this.db.isOurs && this.db.opened && destroy) {
146
+ await this.db.close()
147
+ }
147
148
 
148
- if (!graceful) {
149
- await this.boss.stop()
150
- await shutdown()
151
- return
152
- }
149
+ this.stopped = true
150
+ this.stoppingOn = null
151
+ this.started = false
153
152
 
154
- setImmediate(async () => {
155
- let closing = false
153
+ this.emit(events.stopped)
154
+ resolve()
155
+ } catch (err) {
156
+ this.emit(events.error, err)
157
+ reject(err)
158
+ }
159
+ }
156
160
 
157
- try {
158
- while (Date.now() - this.stoppingOn < timeout) {
159
- if (this.manager.getWipData({ includeInternal: closing }).length === 0) {
160
- if (closing) {
161
- break
162
- }
161
+ if (!graceful) {
162
+ return shutdown()
163
+ }
163
164
 
164
- closing = true
165
+ if (!wait) {
166
+ resolve()
167
+ }
165
168
 
166
- await this.boss.stop()
169
+ setImmediate(async () => {
170
+ try {
171
+ if (this.config.__test__throw_stop_monitor) {
172
+ throw new Error(this.config.__test__throw_stop_monitor)
167
173
  }
168
174
 
169
- await delay(1000)
170
- }
175
+ const isWip = () => this.manager.getWipData({ includeInternal: false }).length > 0
171
176
 
172
- await this.boss.stop()
173
- await shutdown()
174
- } catch (err) {
175
- this.emit(events.error, err)
176
- }
177
+ while ((Date.now() - this.stoppingOn) < timeout && isWip()) {
178
+ await delay(500)
179
+ }
180
+
181
+ await shutdown()
182
+ } catch (err) {
183
+ reject(err)
184
+ this.emit(events.error, err)
185
+ }
186
+ })
177
187
  })
178
188
  }
179
189
  }