pg-boss 10.0.0-beta2 → 10.0.0-beta21

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/timekeeper.js CHANGED
@@ -2,14 +2,12 @@ const EventEmitter = require('events')
2
2
  const plans = require('./plans')
3
3
  const cronParser = require('cron-parser')
4
4
  const Attorney = require('./attorney')
5
- const pMap = require('p-map')
6
5
 
7
- const queues = {
8
- CRON: '__pgboss__cron',
9
- SEND_IT: '__pgboss__send_it'
6
+ const QUEUES = {
7
+ SEND_IT: '__pgboss__send-it'
10
8
  }
11
9
 
12
- const events = {
10
+ const EVENTS = {
13
11
  error: 'error',
14
12
  schedule: 'schedule'
15
13
  }
@@ -25,14 +23,14 @@ class Timekeeper extends EventEmitter {
25
23
  this.cronMonitorIntervalMs = config.cronMonitorIntervalSeconds * 1000
26
24
  this.clockSkew = 0
27
25
 
28
- this.events = events
26
+ this.events = EVENTS
29
27
 
30
28
  this.getTimeCommand = plans.getTime(config.schema)
29
+ this.getQueueCommand = plans.getQueueByName(config.schema)
31
30
  this.getSchedulesCommand = plans.getSchedules(config.schema)
32
31
  this.scheduleCommand = plans.schedule(config.schema)
33
32
  this.unscheduleCommand = plans.unschedule(config.schema)
34
- this.getCronTimeCommand = plans.getCronTime(config.schema)
35
- this.setCronTimeCommand = plans.setCronTime(config.schema)
33
+ this.trySetCronTimeCommand = plans.trySetCronTime(config.schema)
36
34
 
37
35
  this.functions = [
38
36
  this.schedule,
@@ -49,24 +47,25 @@ class Timekeeper extends EventEmitter {
49
47
  return
50
48
  }
51
49
 
52
- // cache the clock skew from the db server
50
+ this.stopped = false
51
+
53
52
  await this.cacheClockSkew()
54
53
 
55
- await this.manager.createQueue(queues.CRON)
56
- await this.manager.createQueue(queues.SEND_IT)
54
+ try {
55
+ await this.manager.createQueue(QUEUES.SEND_IT)
56
+ } catch {}
57
57
 
58
- await this.manager.work(queues.CRON, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds }, (job) => this.onCron(job))
59
- await this.manager.work(queues.SEND_IT, { newJobCheckIntervalSeconds: this.config.cronWorkerIntervalSeconds, teamSize: 50, teamConcurrency: 5 }, (job) => this.onSendIt(job))
58
+ const options = {
59
+ pollingIntervalSeconds: this.config.cronWorkerIntervalSeconds,
60
+ batchSize: 50
61
+ }
60
62
 
61
- // uses sendDebounced() to enqueue a cron check
62
- await this.checkSchedulesAsync()
63
+ await this.manager.work(QUEUES.SEND_IT, options, (jobs) => this.manager.insert(jobs.map(i => i.data)))
63
64
 
64
- // create monitoring interval to make sure cron hasn't crashed
65
- this.cronMonitorInterval = setInterval(async () => await this.monitorCron(), this.cronMonitorIntervalMs)
66
- // create monitoring interval to measure and adjust for drift in clock skew
67
- this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
65
+ setImmediate(() => this.onCron())
68
66
 
69
- this.stopped = false
67
+ this.cronMonitorInterval = setInterval(async () => await this.onCron(), this.cronMonitorIntervalMs)
68
+ this.skewMonitorInterval = setInterval(async () => await this.cacheClockSkew(), this.skewMonitorIntervalMs)
70
69
  }
71
70
 
72
71
  async stop () {
@@ -76,8 +75,7 @@ class Timekeeper extends EventEmitter {
76
75
 
77
76
  this.stopped = true
78
77
 
79
- await this.manager.offWork(queues.CRON)
80
- await this.manager.offWork(queues.SEND_IT)
78
+ await this.manager.offWork(QUEUES.SEND_IT)
81
79
 
82
80
  if (this.skewMonitorInterval) {
83
81
  clearInterval(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,39 @@ 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
- if (this.stopped) return
148
-
149
120
  try {
150
- if (this.config.__test__throw_cron_processing) {
151
- throw new Error(this.config.__test__throw_cron_processing)
121
+ if (this.stopped || this.timekeeping) return
122
+
123
+ if (this.config.__test__force_cron_monitoring_error) {
124
+ throw new Error(this.config.__test__force_cron_monitoring_error)
152
125
  }
153
126
 
154
- const items = await this.getSchedules()
127
+ this.timekeeping = true
155
128
 
156
- const sending = items.filter(i => this.shouldSendIt(i.cron, i.timezone))
129
+ const { rows } = await this.db.executeSql(this.trySetCronTimeCommand, [this.config.cronMonitorIntervalSeconds])
157
130
 
158
- if (sending.length && !this.stopped) {
159
- await pMap(sending, it => this.send(it), { concurrency: 5 })
131
+ if (rows.length === 1 && !this.stopped) {
132
+ await this.cron()
160
133
  }
161
-
162
- if (this.stopped) return
163
-
164
- // set last time cron was evaluated for downstream usage in cron monitoring
165
- await this.setCronTime()
166
134
  } catch (err) {
167
135
  this.emit(this.events.error, err)
136
+ } finally {
137
+ this.timekeeping = false
168
138
  }
139
+ }
140
+
141
+ async cron () {
142
+ const schedules = await this.getSchedules()
169
143
 
170
- if (this.stopped) return
144
+ const scheduled = schedules
145
+ .filter(i => this.shouldSendIt(i.cron, i.timezone))
146
+ .map(({ name, data, options }) =>
147
+ ({ name: QUEUES.SEND_IT, data: { name, data, options }, options: { singletonKey: name, singletonSeconds: 60 } }))
171
148
 
172
- // uses sendDebounced() to enqueue a cron check
173
- await this.checkSchedulesAsync()
149
+ if (scheduled.length > 0 && !this.stopped) {
150
+ await this.manager.insert(scheduled)
151
+ }
174
152
  }
175
153
 
176
154
  shouldSendIt (cron, tz) {
@@ -185,21 +163,6 @@ class Timekeeper extends EventEmitter {
185
163
  return prevDiff < 60
186
164
  }
187
165
 
188
- async send (job) {
189
- const options = {
190
- singletonKey: job.name,
191
- singletonSeconds: 60
192
- }
193
-
194
- await this.manager.send(queues.SEND_IT, job, options)
195
- }
196
-
197
- async onSendIt (job) {
198
- if (this.stopped) return
199
- const { name, data, options } = job.data
200
- await this.manager.send(name, data, options)
201
- }
202
-
203
166
  async getSchedules () {
204
167
  const { rows } = await this.db.executeSql(this.getSchedulesCommand)
205
168
  return rows
@@ -210,35 +173,25 @@ class Timekeeper extends EventEmitter {
210
173
 
211
174
  cronParser.parseExpression(cron, { tz })
212
175
 
213
- // validation pre-check
214
176
  Attorney.checkSendArgs([name, data, options], this.config)
215
177
 
216
178
  const values = [name, cron, tz, data, options]
217
179
 
218
- const result = await this.db.executeSql(this.scheduleCommand, values)
180
+ try {
181
+ await this.db.executeSql(this.scheduleCommand, values)
182
+ } catch (err) {
183
+ if (err.message.includes('foreign key')) {
184
+ err.message = `Queue ${name} not found`
185
+ }
219
186
 
220
- return result ? result.rowCount : null
187
+ throw err
188
+ }
221
189
  }
222
190
 
223
191
  async unschedule (name) {
224
- const result = await this.db.executeSql(this.unscheduleCommand, [name])
225
- return result ? result.rowCount : null
226
- }
227
-
228
- async setCronTime () {
229
- await this.db.executeSql(this.setCronTimeCommand)
230
- }
231
-
232
- async getCronTime () {
233
- const { rows } = await this.db.executeSql(this.getCronTimeCommand)
234
-
235
- let { cron_on: cronOn, seconds_ago: secondsAgo } = rows[0]
236
-
237
- secondsAgo = secondsAgo !== null ? parseFloat(secondsAgo) : 61
238
-
239
- return { cronOn, secondsAgo }
192
+ await this.db.executeSql(this.unscheduleCommand, [name])
240
193
  }
241
194
  }
242
195
 
243
196
  module.exports = Timekeeper
244
- module.exports.QUEUES = queues
197
+ module.exports.QUEUES = QUEUES
package/types.d.ts CHANGED
@@ -1,8 +1,24 @@
1
1
  import { EventEmitter } from 'events'
2
2
 
3
3
  declare namespace PgBoss {
4
+
5
+ type JobStates = {
6
+ created : 'created',
7
+ retry: 'retry',
8
+ active: 'active',
9
+ completed: 'completed',
10
+ cancelled: 'cancelled',
11
+ failed: 'failed'
12
+ }
13
+
14
+ type QueuePolicies = {
15
+ standard: 'standard'
16
+ short: 'short',
17
+ singleton: 'singleton',
18
+ stately: 'stately'
19
+ }
4
20
  interface Db {
5
- executeSql(text: string, values: any[]): Promise<{ rows: any[]; rowCount: number }>;
21
+ executeSql(text: string, values: any[]): Promise<{ rows: any[] }>;
6
22
  }
7
23
 
8
24
  interface DatabaseOptions {
@@ -91,53 +107,35 @@ declare namespace PgBoss {
91
107
  interface ConnectionOptions {
92
108
  db?: Db;
93
109
  }
94
-
110
+
95
111
  type InsertOptions = ConnectionOptions;
96
-
112
+
97
113
  type SendOptions = JobOptions & ExpirationOptions & RetentionOptions & RetryOptions & ConnectionOptions;
98
-
114
+
115
+ type QueuePolicy = 'standard' | 'short' | 'singleton' | 'stately'
116
+
117
+ type Queue = RetryOptions & ExpirationOptions & RetentionOptions & { name: string, policy?: QueuePolicy, deadLetter?: string }
118
+ type QueueResult = Queue & { createdOn: Date, updatedOn: Date }
99
119
  type ScheduleOptions = SendOptions & { tz?: string }
100
120
 
101
121
  interface JobPollingOptions {
102
- newJobCheckInterval?: number;
103
- newJobCheckIntervalSeconds?: number;
122
+ pollingIntervalSeconds?: number;
104
123
  }
105
124
 
106
- interface CommonJobFetchOptions {
125
+ interface JobFetchOptions {
107
126
  includeMetadata?: boolean;
108
127
  priority?: boolean;
109
- }
110
-
111
- type JobFetchOptions = CommonJobFetchOptions & {
112
- teamSize?: number;
113
- teamConcurrency?: number;
114
- teamRefill?: boolean;
115
- }
116
-
117
- type BatchJobFetchOptions = CommonJobFetchOptions & {
118
- batchSize: number;
128
+ batchSize?: number;
119
129
  }
120
130
 
121
131
  type WorkOptions = JobFetchOptions & JobPollingOptions
122
- type BatchWorkOptions = BatchJobFetchOptions & JobPollingOptions
123
-
124
- type FetchOptions = {
125
- includeMetadata?: boolean;
126
- } & ConnectionOptions;
132
+ type FetchOptions = JobFetchOptions & ConnectionOptions;
127
133
 
128
134
  interface WorkHandler<ReqData> {
129
- (job: PgBoss.Job<ReqData>): Promise<any>;
130
- }
131
-
132
- interface BatchWorkHandler<ReqData> {
133
135
  (job: PgBoss.Job<ReqData>[]): Promise<any>;
134
136
  }
135
137
 
136
138
  interface WorkWithMetadataHandler<ReqData> {
137
- (job: PgBoss.JobWithMetadata<ReqData>): Promise<any>;
138
- }
139
-
140
- interface BatchWorkWithMetadataHandler<ReqData> {
141
139
  (job: PgBoss.JobWithMetadata<ReqData>[]): Promise<any>;
142
140
  }
143
141
 
@@ -176,24 +174,26 @@ declare namespace PgBoss {
176
174
  data: T;
177
175
  }
178
176
 
179
- interface JobWithMetadata<T = object> extends Job<T> {
177
+ interface JobWithMetadata<T = object> {
178
+ id: string;
179
+ name: string;
180
+ data: T;
180
181
  priority: number;
181
182
  state: 'created' | 'retry' | 'active' | 'completed' | 'cancelled' | 'failed';
182
- retrylimit: number;
183
- retrycount: number;
184
- retrydelay: number;
185
- retrybackoff: boolean;
186
- startafter: Date;
187
- // This is nullable in the schema, but by the time this type is reified,
188
- // it will have been set.
189
- startedon: Date;
190
- singletonkey: string | null;
191
- singletonon: Date | null;
192
- expirein: PostgresInterval;
193
- createdon: Date;
194
- completedon: Date | null;
195
- keepuntil: Date;
196
- deadletter: string,
183
+ retryLimit: number;
184
+ retryCount: number;
185
+ retryDelay: number;
186
+ retryBackoff: boolean;
187
+ startAfter: Date;
188
+ startedOn: Date;
189
+ singletonKey: string | null;
190
+ singletonOn: Date | null;
191
+ expireIn: PostgresInterval;
192
+ createdOn: Date;
193
+ completedOn: Date | null;
194
+ keepUntil: Date;
195
+ deadLetter: string,
196
+ policy: QueuePolicy,
197
197
  output: object
198
198
  }
199
199
 
@@ -242,7 +242,7 @@ declare namespace PgBoss {
242
242
  }
243
243
 
244
244
  interface StopOptions {
245
- destroy?: boolean,
245
+ close?: boolean,
246
246
  graceful?: boolean,
247
247
  timeout?: number,
248
248
  wait?: boolean
@@ -269,6 +269,9 @@ declare class PgBoss extends EventEmitter {
269
269
  static getRollbackPlans(schema: string): string;
270
270
  static getRollbackPlans(): string;
271
271
 
272
+ static states: PgBoss.JobStates
273
+ static policies: PgBoss.QueuePolicies
274
+
272
275
  on(event: "error", handler: (error: Error) => void): this;
273
276
  off(event: "error", handler: (error: Error) => void): this;
274
277
 
@@ -304,32 +307,24 @@ declare class PgBoss extends EventEmitter {
304
307
  insert(jobs: PgBoss.JobInsert[]): Promise<void>;
305
308
  insert(jobs: PgBoss.JobInsert[], options: PgBoss.InsertOptions): Promise<void>;
306
309
 
310
+ fetch<T>(name: string): Promise<PgBoss.Job<T>[]>;
311
+ fetch<T>(name: string, options: PgBoss.FetchOptions & { includeMetadata: true }): Promise<PgBoss.JobWithMetadata<T>[]>;
312
+ fetch<T>(name: string, options: PgBoss.FetchOptions): Promise<PgBoss.Job<T>[]>;
313
+
307
314
  work<ReqData>(name: string, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
308
315
  work<ReqData>(name: string, options: PgBoss.WorkOptions & { includeMetadata: true }, handler: PgBoss.WorkWithMetadataHandler<ReqData>): Promise<string>;
309
316
  work<ReqData>(name: string, options: PgBoss.WorkOptions, handler: PgBoss.WorkHandler<ReqData>): Promise<string>;
310
317
 
311
- work<ReqData>(name: string, options: PgBoss.BatchWorkOptions & { includeMetadata: true }, handler: PgBoss.BatchWorkWithMetadataHandler<ReqData>): Promise<string>;
312
- work<ReqData>(name: string, options: PgBoss.BatchWorkOptions, handler: PgBoss.BatchWorkHandler<ReqData>): Promise<string>;
313
-
314
318
  offWork(name: string): Promise<void>;
315
319
  offWork(options: PgBoss.OffWorkOptions): Promise<void>;
316
320
 
317
- /**
318
- * Notify worker that something has changed
319
- * @param workerId
320
- */
321
321
  notifyWorker(workerId: string): void;
322
322
 
323
323
  subscribe(event: string, name: string): Promise<void>;
324
324
  unsubscribe(event: string, name: string): Promise<void>;
325
- publish(event: string): Promise<string[]>;
326
- publish(event: string, data: object): Promise<string[]>;
327
- publish(event: string, data: object, options: PgBoss.SendOptions): Promise<string[]>;
328
-
329
- fetch<T>(name: string): Promise<PgBoss.Job<T> | null>;
330
- fetch<T>(name: string, batchSize: number): Promise<PgBoss.Job<T>[] | null>;
331
- fetch<T>(name: string, batchSize: number, options: PgBoss.FetchOptions & { includeMetadata: true }): Promise<PgBoss.JobWithMetadata<T>[] | null>;
332
- fetch<T>(name: string, batchSize: number, options: PgBoss.FetchOptions): Promise<PgBoss.Job<T>[] | null>;
325
+ publish(event: string): Promise<void>;
326
+ publish(event: string, data: object): Promise<void>;
327
+ publish(event: string, data: object, options: PgBoss.SendOptions): Promise<void>;
333
328
 
334
329
  cancel(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
335
330
  cancel(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
@@ -337,6 +332,9 @@ declare class PgBoss extends EventEmitter {
337
332
  resume(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
338
333
  resume(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
339
334
 
335
+ deleteJob(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
336
+ deleteJob(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
337
+
340
338
  complete(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<void>;
341
339
  complete(name: string, id: string, data: object, options?: PgBoss.ConnectionOptions): Promise<void>;
342
340
  complete(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
@@ -345,18 +343,23 @@ declare class PgBoss extends EventEmitter {
345
343
  fail(name: string, id: string, data: object, options?: PgBoss.ConnectionOptions): Promise<void>;
346
344
  fail(name: string, ids: string[], options?: PgBoss.ConnectionOptions): Promise<void>;
347
345
 
348
- getQueueSize(name: string, options?: object): Promise<number>;
349
- getJobById(name: string, id: string, options?: PgBoss.ConnectionOptions): Promise<PgBoss.JobWithMetadata | null>;
350
-
351
- createQueue(name: string, policy: 'standard' | 'short' | 'singleton' | 'stately'): Promise<void>;
346
+ getJobById(name: string, id: string, options?: PgBoss.ConnectionOptions & { includeArchive: bool }): Promise<PgBoss.JobWithMetadata | null>;
347
+
348
+ createQueue(name: string, options?: PgBoss.Queue): Promise<void>;
349
+ updateQueue(name: string, options?: PgBoss.Queue): Promise<void>;
352
350
  deleteQueue(name: string): Promise<void>;
353
351
  purgeQueue(name: string): Promise<void>;
354
- clearStorage(): Promise<void>;
352
+ getQueues(): Promise<PgBoss.QueueResult[]>;
353
+ getQueue(name: string): Promise<PgBoss.QueueResult | null>;
354
+ getQueueSize(name: string, options?: { before: 'retry' | 'active' | 'completed' | 'cancelled' | 'failed' }): Promise<number>;
355
355
 
356
+ clearStorage(): Promise<void>;
356
357
  archive(): Promise<void>;
357
358
  purge(): Promise<void>;
358
359
  expire(): Promise<void>;
359
360
  maintain(): Promise<void>;
361
+ isInstalled(): Promise<Boolean>;
362
+ schemaVersion(): Promise<Number>;
360
363
 
361
364
  schedule(name: string, cron: string, data?: object, options?: PgBoss.ScheduleOptions): Promise<void>;
362
365
  unschedule(name: string): Promise<void>;