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/plans.js CHANGED
@@ -5,19 +5,21 @@ const states = {
5
5
  retry: 'retry',
6
6
  active: 'active',
7
7
  completed: 'completed',
8
- expired: 'expired',
9
8
  cancelled: 'cancelled',
10
9
  failed: 'failed'
11
10
  }
12
11
 
13
12
  const DEFAULT_SCHEMA = 'pgboss'
14
- const COMPLETION_JOB_PREFIX = `__state__${states.completed}__`
15
- const SINGLETON_QUEUE_KEY = '__pgboss__singleton_queue'
16
- const SINGLETON_QUEUE_KEY_ESCAPED = SINGLETON_QUEUE_KEY.replace(/_/g, '\\_')
17
-
18
13
  const MIGRATE_RACE_MESSAGE = 'division by zero'
19
14
  const CREATE_RACE_MESSAGE = 'already exists'
20
15
 
16
+ const QUEUE_POLICY = {
17
+ standard: 'standard',
18
+ short: 'short',
19
+ singleton: 'singleton',
20
+ stately: 'stately'
21
+ }
22
+
21
23
  module.exports = {
22
24
  create,
23
25
  insertVersion,
@@ -28,7 +30,8 @@ module.exports = {
28
30
  completeJobs,
29
31
  cancelJobs,
30
32
  resumeJobs,
31
- failJobs,
33
+ failJobsById,
34
+ failJobsByTimeout,
32
35
  insertJob,
33
36
  insertJobs,
34
37
  getTime,
@@ -38,62 +41,64 @@ module.exports = {
38
41
  subscribe,
39
42
  unsubscribe,
40
43
  getQueuesForEvent,
41
- expire,
42
44
  archive,
43
- purge,
45
+ drop,
44
46
  countStates,
45
- deleteQueue,
46
- deleteAllQueues,
47
- clearStorage,
47
+ createQueue,
48
+ updateQueue,
49
+ partitionCreateJobName,
50
+ dropJobTablePartition,
51
+ deleteQueueRecords,
52
+ getQueueByName,
48
53
  getQueueSize,
54
+ purgeQueue,
55
+ clearStorage,
49
56
  getMaintenanceTime,
50
57
  setMaintenanceTime,
58
+ getMonitorTime,
59
+ setMonitorTime,
51
60
  getCronTime,
52
61
  setCronTime,
53
62
  locked,
63
+ advisoryLock,
54
64
  assertMigration,
55
65
  getArchivedJobById,
56
66
  getJobById,
67
+ QUEUE_POLICY,
57
68
  states: { ...states },
58
- COMPLETION_JOB_PREFIX,
59
- SINGLETON_QUEUE_KEY,
60
69
  MIGRATE_RACE_MESSAGE,
61
70
  CREATE_RACE_MESSAGE,
62
71
  DEFAULT_SCHEMA
63
72
  }
64
73
 
65
- function locked (schema, query) {
66
- if (Array.isArray(query)) {
67
- query = query.join(';\n')
68
- }
69
-
70
- return `
71
- BEGIN;
72
- SET LOCAL statement_timeout = '30s';
73
- ${advisoryLock(schema)};
74
- ${query};
75
- COMMIT;
76
- `
77
- }
78
-
79
74
  function create (schema, version) {
80
75
  const commands = [
81
76
  createSchema(schema),
82
- createVersionTable(schema),
83
- createJobStateEnum(schema),
84
- createJobTable(schema),
85
- cloneJobTableForArchive(schema),
86
- createScheduleTable(schema),
87
- createSubscriptionTable(schema),
88
- addIdIndexToArchive(schema),
89
- addArchivedOnToArchive(schema),
90
- addArchivedOnIndexToArchive(schema),
77
+ createEnumJobState(schema),
78
+
79
+ createTableJob(schema),
80
+ createTableJobDefault(schema),
81
+ attachPartitionJobDefault(schema),
91
82
  createIndexJobName(schema),
92
83
  createIndexJobFetch(schema),
93
- createIndexSingletonOn(schema),
94
- createIndexSingletonKeyOn(schema),
95
- createIndexSingletonKey(schema),
96
- createIndexSingletonQueue(schema),
84
+ createIndexJobPolicyStately(schema),
85
+ createIndexJobPolicyShort(schema),
86
+ createIndexJobPolicySingleton(schema),
87
+ createIndexJobThrottleOn(schema),
88
+ createIndexJobThrottleKey(schema),
89
+
90
+ createTableArchive(schema),
91
+ createPrimaryKeyArchive(schema),
92
+ createColumnArchiveArchivedOn(schema),
93
+ createIndexArchiveArchivedOn(schema),
94
+ createIndexArchiveName(schema),
95
+ createArchiveBackupTable(schema),
96
+
97
+ createTableVersion(schema),
98
+ createTableQueue(schema),
99
+ createTableSchedule(schema),
100
+ createTableSubscription(schema),
101
+
97
102
  insertVersion(schema, version)
98
103
  ]
99
104
 
@@ -106,17 +111,18 @@ function createSchema (schema) {
106
111
  `
107
112
  }
108
113
 
109
- function createVersionTable (schema) {
114
+ function createTableVersion (schema) {
110
115
  return `
111
116
  CREATE TABLE ${schema}.version (
112
117
  version int primary key,
113
118
  maintained_on timestamp with time zone,
114
- cron_on timestamp with time zone
119
+ cron_on timestamp with time zone,
120
+ monitored_on timestamp with time zone
115
121
  )
116
122
  `
117
123
  }
118
124
 
119
- function createJobStateEnum (schema) {
125
+ function createEnumJobState (schema) {
120
126
  // ENUM definition order is important
121
127
  // base type is numeric and first values are less than last values
122
128
  return `
@@ -125,17 +131,16 @@ function createJobStateEnum (schema) {
125
131
  '${states.retry}',
126
132
  '${states.active}',
127
133
  '${states.completed}',
128
- '${states.expired}',
129
134
  '${states.cancelled}',
130
135
  '${states.failed}'
131
136
  )
132
137
  `
133
138
  }
134
139
 
135
- function createJobTable (schema) {
140
+ function createTableJob (schema) {
136
141
  return `
137
142
  CREATE TABLE ${schema}.job (
138
- id uuid primary key not null default gen_random_uuid(),
143
+ id uuid not null default gen_random_uuid(),
139
144
  name text not null,
140
145
  priority integer not null default(0),
141
146
  data jsonb,
@@ -152,34 +157,100 @@ function createJobTable (schema) {
152
157
  createdOn timestamp with time zone not null default now(),
153
158
  completedOn timestamp with time zone,
154
159
  keepUntil timestamp with time zone NOT NULL default now() + interval '14 days',
155
- on_complete boolean not null default false,
156
- output jsonb
157
- )
160
+ output jsonb,
161
+ deadletter text,
162
+ policy text,
163
+ CONSTRAINT job_pkey PRIMARY KEY (name, id)
164
+ ) PARTITION BY LIST (name)
165
+ `
166
+ }
167
+
168
+ function createTableJobDefault (schema) {
169
+ return `CREATE TABLE ${schema}.job_default (LIKE ${schema}.job INCLUDING DEFAULTS INCLUDING CONSTRAINTS)`
170
+ }
171
+
172
+ function attachPartitionJobDefault (schema) {
173
+ return `ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.job_default DEFAULT`
174
+ }
175
+
176
+ function partitionCreateJobName (schema, name) {
177
+ return `
178
+ CREATE TABLE ${schema}.job_${name} (LIKE ${schema}.job INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
179
+ ALTER TABLE ${schema}.job_${name} ADD CONSTRAINT job_check_${name} CHECK (name='${name}');
180
+ ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.job_${name} FOR VALUES IN ('${name}');
158
181
  `
159
182
  }
160
183
 
161
- function cloneJobTableForArchive (schema) {
184
+ function dropJobTablePartition (schema, name) {
185
+ return `DROP TABLE IF EXISTS ${schema}.job_${name}`
186
+ }
187
+
188
+ function createPrimaryKeyArchive (schema) {
189
+ return `ALTER TABLE ${schema}.archive ADD CONSTRAINT archive_pkey PRIMARY KEY (name, id)`
190
+ }
191
+
192
+ function createIndexJobPolicyShort (schema) {
193
+ return `CREATE UNIQUE INDEX job_policy_short ON ${schema}.job (name) WHERE state = '${states.created}' AND policy = '${QUEUE_POLICY.short}'`
194
+ }
195
+
196
+ function createIndexJobPolicySingleton (schema) {
197
+ return `CREATE UNIQUE INDEX job_policy_singleton ON ${schema}.job (name) WHERE state = '${states.active}' AND policy = '${QUEUE_POLICY.singleton}'`
198
+ }
199
+
200
+ function createIndexJobPolicyStately (schema) {
201
+ return `CREATE UNIQUE INDEX job_policy_stately ON ${schema}.job (name, state) WHERE state <= '${states.active}' AND policy = '${QUEUE_POLICY.stately}'`
202
+ }
203
+
204
+ function createIndexJobThrottleOn (schema) {
205
+ return `CREATE UNIQUE INDEX job_throttle_on ON ${schema}.job (name, singletonOn, COALESCE(singletonKey, '')) WHERE state <= '${states.completed}' AND singletonOn IS NOT NULL`
206
+ }
207
+
208
+ function createIndexJobThrottleKey (schema) {
209
+ return `CREATE UNIQUE INDEX job_throttle_key ON ${schema}.job (name, singletonKey) WHERE state <= '${states.completed}' AND singletonOn IS NULL`
210
+ }
211
+
212
+ function createIndexJobName (schema) {
213
+ return `CREATE INDEX job_name ON ${schema}.job (name text_pattern_ops)`
214
+ }
215
+
216
+ function createIndexJobFetch (schema) {
217
+ return `CREATE INDEX job_fetch ON ${schema}.job (name text_pattern_ops, startAfter) INCLUDE (priority, createdOn, id) WHERE state < '${states.active}'`
218
+ }
219
+
220
+ function createTableArchive (schema) {
162
221
  return `CREATE TABLE ${schema}.archive (LIKE ${schema}.job)`
163
222
  }
164
223
 
165
- function addArchivedOnToArchive (schema) {
224
+ function createArchiveBackupTable (schema) {
225
+ return `CREATE TABLE ${schema}.archive_backup (LIKE ${schema}.job)`
226
+ }
227
+
228
+ function createColumnArchiveArchivedOn (schema) {
166
229
  return `ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
167
230
  }
168
231
 
169
- function addArchivedOnIndexToArchive (schema) {
232
+ function createIndexArchiveArchivedOn (schema) {
170
233
  return `CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
171
234
  }
172
235
 
173
- function addIdIndexToArchive (schema) {
174
- return `CREATE INDEX archive_id_idx ON ${schema}.archive(id)`
236
+ function createIndexArchiveName (schema) {
237
+ return `CREATE INDEX archive_name_idx ON ${schema}.archive(name)`
238
+ }
239
+
240
+ function getMaintenanceTime (schema) {
241
+ return `SELECT maintained_on, EXTRACT( EPOCH FROM (now() - maintained_on) ) seconds_ago FROM ${schema}.version`
175
242
  }
176
243
 
177
244
  function setMaintenanceTime (schema) {
178
245
  return `UPDATE ${schema}.version SET maintained_on = now()`
179
246
  }
180
247
 
181
- function getMaintenanceTime (schema) {
182
- return `SELECT maintained_on, EXTRACT( EPOCH FROM (now() - maintained_on) ) seconds_ago FROM ${schema}.version`
248
+ function getMonitorTime (schema) {
249
+ return `SELECT monitored_on, EXTRACT( EPOCH FROM (now() - monitored_on) ) seconds_ago FROM ${schema}.version`
250
+ }
251
+
252
+ function setMonitorTime (schema) {
253
+ return `UPDATE ${schema}.version SET monitored_on = now()`
183
254
  }
184
255
 
185
256
  function setCronTime (schema, time) {
@@ -191,69 +262,69 @@ function getCronTime (schema) {
191
262
  return `SELECT cron_on, EXTRACT( EPOCH FROM (now() - cron_on) ) seconds_ago FROM ${schema}.version`
192
263
  }
193
264
 
194
- function deleteQueue (schema, options = {}) {
195
- options.before = options.before || states.active
196
- assert(options.before in states, `${options.before} is not a valid state`)
197
- return `DELETE FROM ${schema}.job WHERE name = $1 and state < '${options.before}'`
198
- }
199
-
200
- function deleteAllQueues (schema, options = {}) {
201
- options.before = options.before || states.active
202
- assert(options.before in states, `${options.before} is not a valid state`)
203
- return `DELETE FROM ${schema}.job WHERE state < '${options.before}'`
204
- }
205
-
206
- function clearStorage (schema) {
207
- return `TRUNCATE ${schema}.job, ${schema}.archive`
208
- }
209
-
210
- function getQueueSize (schema, options = {}) {
211
- options.before = options.before || states.active
212
- assert(options.before in states, `${options.before} is not a valid state`)
213
- return `SELECT count(*) as count FROM ${schema}.job WHERE name = $1 AND state < '${options.before}'`
214
- }
215
-
216
- function createIndexSingletonKey (schema) {
217
- // anything with singletonKey means "only 1 job can be queued or active at a time"
265
+ function createQueue (schema) {
218
266
  return `
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}%'
267
+ INSERT INTO ${schema}.queue (name, policy, retry_limit, retry_delay, retry_backoff, expire_seconds, retention_minutes, dead_letter)
268
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
220
269
  `
221
270
  }
222
271
 
223
- function createIndexSingletonQueue (schema) {
224
- // "singleton queue" means "only 1 job can be queued at a time"
272
+ function updateQueue (schema) {
225
273
  return `
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}%'
274
+ UPDATE ${schema}.queue SET
275
+ retry_limit = COALESCE($2, retry_limit),
276
+ retry_delay = COALESCE($3, retry_delay),
277
+ retry_backoff = COALESCE($4, retry_backoff),
278
+ expire_seconds = COALESCE($5, expire_seconds),
279
+ retention_minutes = COALESCE($6, retention_minutes),
280
+ dead_letter = COALESCE($7, dead_letter)
281
+ WHERE name = $1
227
282
  `
228
283
  }
229
284
 
230
- function createIndexSingletonOn (schema) {
231
- // anything with singletonOn means "only 1 job within this time period, queued, active or completed"
232
- return `
233
- CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < '${states.expired}' AND singletonKey IS NULL
234
- `
285
+ function getQueueByName (schema) {
286
+ return `SELECT * FROM ${schema}.queue WHERE name = $1`
235
287
  }
236
288
 
237
- function createIndexSingletonKeyOn (schema) {
238
- // anything with both singletonOn and singletonKey means "only 1 job within this time period with this key, queued, active or completed"
239
- return `
240
- CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < '${states.expired}'
289
+ function deleteQueueRecords (schema) {
290
+ return `WITH dq AS (
291
+ DELETE FROM ${schema}.queue WHERE name = $1
292
+ )
293
+ DELETE FROM ${schema}.job WHERE name = $1
241
294
  `
242
295
  }
243
296
 
244
- function createIndexJobName (schema) {
245
- return `
246
- CREATE INDEX job_name ON ${schema}.job (name text_pattern_ops)
247
- `
297
+ function purgeQueue (schema) {
298
+ return `DELETE from ${schema}.job WHERE name = $1 and state < '${states.active}'`
248
299
  }
249
300
 
250
- function createIndexJobFetch (schema) {
301
+ function clearStorage (schema) {
302
+ return `TRUNCATE ${schema}.job, ${schema}.archive`
303
+ }
304
+
305
+ function getQueueSize (schema, options = {}) {
306
+ options.before = options.before || states.active
307
+ assert(options.before in states, `${options.before} is not a valid state`)
308
+ return `SELECT count(*) as count FROM ${schema}.job WHERE name = $1 AND state < '${options.before}'`
309
+ }
310
+
311
+ function createTableQueue (schema) {
251
312
  return `
252
- CREATE INDEX job_fetch ON ${schema}.job (name text_pattern_ops, startAfter) WHERE state < '${states.active}'
313
+ CREATE TABLE ${schema}.queue (
314
+ name text primary key,
315
+ policy text,
316
+ retry_limit int,
317
+ retry_delay int,
318
+ retry_backoff bool,
319
+ expire_seconds int,
320
+ retention_minutes int,
321
+ dead_letter text,
322
+ created_on timestamp with time zone not null default now()
323
+ )
253
324
  `
254
325
  }
255
326
 
256
- function createScheduleTable (schema) {
327
+ function createTableSchedule (schema) {
257
328
  return `
258
329
  CREATE TABLE ${schema}.schedule (
259
330
  name text primary key,
@@ -267,7 +338,7 @@ function createScheduleTable (schema) {
267
338
  `
268
339
  }
269
340
 
270
- function createSubscriptionTable (schema) {
341
+ function createTableSubscription (schema) {
271
342
  return `
272
343
  CREATE TABLE ${schema}.subscription (
273
344
  event text not null,
@@ -351,79 +422,28 @@ function insertVersion (schema, version) {
351
422
  }
352
423
 
353
424
  function fetchNextJob (schema) {
354
- return (includeMetadata, enforceSingletonQueueActiveLimit) => `
355
- WITH nextJob as (
425
+ return ({ includeMetadata, patternMatch, priority = true } = {}) => `
426
+ WITH next as (
356
427
  SELECT id
357
- FROM ${schema}.job j
428
+ FROM ${schema}.job
358
429
  WHERE state < '${states.active}'
359
- AND name LIKE $1
430
+ AND name ${patternMatch ? 'LIKE' : '='} $1
360
431
  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
- : ''}
379
- ORDER BY priority desc, createdOn, id
432
+ ORDER BY ${priority && 'priority desc, '} createdOn, id
380
433
  LIMIT $2
381
434
  FOR UPDATE SKIP LOCKED
382
435
  )
383
436
  UPDATE ${schema}.job j SET
384
437
  state = '${states.active}',
385
438
  startedOn = now(),
386
- retryCount = CASE WHEN state = '${states.retry}' THEN retryCount + 1 ELSE retryCount END
387
- FROM nextJob
388
- WHERE j.id = nextJob.id
389
- RETURNING ${includeMetadata ? 'j.*' : 'j.id, name, data'}, EXTRACT(epoch FROM expireIn) as expire_in_seconds
439
+ retryCount = CASE WHEN startedOn IS NOT NULL THEN retryCount + 1 ELSE retryCount END
440
+ FROM next
441
+ WHERE j.id = next.id
442
+ RETURNING ${includeMetadata ? 'j.*' : 'j.id, name, data'},
443
+ EXTRACT(epoch FROM expireIn) as expire_in_seconds
390
444
  `
391
445
  }
392
446
 
393
- function buildJsonCompletionObject (withResponse) {
394
- // job completion contract
395
- return `jsonb_build_object(
396
- 'request', jsonb_build_object('id', id, 'name', name, 'data', data),
397
- 'response', ${withResponse ? '$2::jsonb' : 'null'},
398
- 'state', state,
399
- 'retryCount', retryCount,
400
- 'createdOn', createdOn,
401
- 'startedOn', startedOn,
402
- 'completedOn', completedOn,
403
- 'failed', CASE WHEN state = '${states.completed}' THEN false ELSE true END
404
- )`
405
- }
406
-
407
- const retryCompletedOnCase = `CASE
408
- WHEN retryCount < retryLimit
409
- THEN NULL
410
- ELSE now()
411
- END`
412
-
413
- const retryStartAfterCase = `CASE
414
- WHEN retryCount = retryLimit THEN startAfter
415
- WHEN NOT retryBackoff THEN now() + retryDelay * interval '1'
416
- ELSE now() +
417
- (
418
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2
419
- +
420
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 * random()
421
- )
422
- * interval '1'
423
- END`
424
-
425
- const keepUntilInheritance = 'keepUntil + (keepUntil - startAfter)'
426
-
427
447
  function completeJobs (schema) {
428
448
  return `
429
449
  WITH results AS (
@@ -434,76 +454,64 @@ function completeJobs (schema) {
434
454
  WHERE id IN (SELECT UNNEST($1::uuid[]))
435
455
  AND state = '${states.active}'
436
456
  RETURNING *
437
- ), completion_jobs as (
438
- INSERT INTO ${schema}.job (name, data, keepUntil)
439
- SELECT
440
- '${COMPLETION_JOB_PREFIX}' || name,
441
- ${buildJsonCompletionObject(true)},
442
- ${keepUntilInheritance}
443
- FROM results
444
- WHERE NOT name LIKE '${COMPLETION_JOB_PREFIX}%'
445
- AND on_complete
446
457
  )
447
458
  SELECT COUNT(*) FROM results
448
459
  `
449
460
  }
450
461
 
451
- function failJobs (schema) {
462
+ function failJobsById (schema) {
463
+ const where = `id IN (SELECT UNNEST($1::uuid[])) AND state < '${states.completed}'`
464
+ const output = '$2::jsonb'
465
+
466
+ return failJobs(schema, where, output)
467
+ }
468
+
469
+ function failJobsByTimeout (schema) {
470
+ const where = `state = '${states.active}' AND (startedOn + expireIn) < now()`
471
+ const output = '\'{ "value": { "message": "job failed by timeout in active state" } }\'::jsonb'
472
+ return failJobs(schema, where, output)
473
+ }
474
+
475
+ function failJobs (schema, where, output) {
452
476
  return `
453
477
  WITH results AS (
454
- UPDATE ${schema}.job
455
- SET state = CASE
456
- WHEN retryCount < retryLimit
457
- THEN '${states.retry}'::${schema}.job_state
478
+ UPDATE ${schema}.job SET
479
+ state = CASE
480
+ WHEN retryCount < retryLimit THEN '${states.retry}'::${schema}.job_state
458
481
  ELSE '${states.failed}'::${schema}.job_state
459
482
  END,
460
- completedOn = ${retryCompletedOnCase},
461
- startAfter = ${retryStartAfterCase},
462
- output = $2::jsonb
463
- WHERE id IN (SELECT UNNEST($1::uuid[]))
464
- AND state < '${states.completed}'
483
+ completedOn = CASE
484
+ WHEN retryCount < retryLimit THEN NULL
485
+ ELSE now()
486
+ END,
487
+ startAfter = CASE
488
+ WHEN retryCount = retryLimit THEN startAfter
489
+ WHEN NOT retryBackoff THEN now() + retryDelay * interval '1'
490
+ ELSE now() + (
491
+ retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 +
492
+ retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 * random()
493
+ ) * interval '1'
494
+ END,
495
+ output = ${output}
496
+ WHERE ${where}
465
497
  RETURNING *
466
- ), completion_jobs as (
467
- INSERT INTO ${schema}.job (name, data, keepUntil)
498
+ ), dlq_jobs as (
499
+ INSERT INTO ${schema}.job (name, data, output, retryLimit, keepUntil)
468
500
  SELECT
469
- '${COMPLETION_JOB_PREFIX}' || name,
470
- ${buildJsonCompletionObject(true)},
471
- ${keepUntilInheritance}
501
+ deadletter,
502
+ data,
503
+ output,
504
+ retryLimit,
505
+ keepUntil + (keepUntil - startAfter)
472
506
  FROM results
473
507
  WHERE state = '${states.failed}'
474
- AND NOT name LIKE '${COMPLETION_JOB_PREFIX}%'
475
- AND on_complete
508
+ AND deadletter IS NOT NULL
509
+ AND NOT name = deadletter
476
510
  )
477
511
  SELECT COUNT(*) FROM results
478
512
  `
479
513
  }
480
514
 
481
- function expire (schema) {
482
- return `
483
- WITH results AS (
484
- UPDATE ${schema}.job
485
- SET state = CASE
486
- WHEN retryCount < retryLimit THEN '${states.retry}'::${schema}.job_state
487
- ELSE '${states.expired}'::${schema}.job_state
488
- END,
489
- completedOn = ${retryCompletedOnCase},
490
- startAfter = ${retryStartAfterCase}
491
- WHERE state = '${states.active}'
492
- AND (startedOn + expireIn) < now()
493
- RETURNING *
494
- )
495
- INSERT INTO ${schema}.job (name, data, keepUntil)
496
- SELECT
497
- '${COMPLETION_JOB_PREFIX}' || name,
498
- ${buildJsonCompletionObject()},
499
- ${keepUntilInheritance}
500
- FROM results
501
- WHERE state = '${states.expired}'
502
- AND NOT name LIKE '${COMPLETION_JOB_PREFIX}%'
503
- AND on_complete
504
- `
505
- }
506
-
507
515
  function cancelJobs (schema) {
508
516
  return `
509
517
  with results as (
@@ -536,68 +544,73 @@ function insertJob (schema) {
536
544
  INSERT INTO ${schema}.job (
537
545
  id,
538
546
  name,
547
+ data,
539
548
  priority,
540
- state,
541
- retryLimit,
542
549
  startAfter,
543
- expireIn,
544
- data,
545
550
  singletonKey,
546
551
  singletonOn,
552
+ deadletter,
553
+ expireIn,
554
+ keepUntil,
555
+ retryLimit,
547
556
  retryDelay,
548
557
  retryBackoff,
549
- keepUntil,
550
- on_complete
558
+ policy
551
559
  )
552
560
  SELECT
553
561
  id,
554
- name,
562
+ j.name,
563
+ data,
555
564
  priority,
556
- state,
557
- retryLimit,
558
565
  startAfter,
559
- expireIn,
560
- data,
561
566
  singletonKey,
562
567
  singletonOn,
563
- retryDelay,
564
- retryBackoff,
565
- keepUntil,
566
- on_complete
568
+ COALESCE(deadLetter, q.dead_letter) as deadletter,
569
+ CASE
570
+ WHEN expireIn IS NOT NULL THEN CAST(expireIn as interval)
571
+ WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
572
+ WHEN expireInDefault IS NOT NULL THEN CAST(expireInDefault as interval)
573
+ ELSE interval '15 minutes'
574
+ END as expireIn,
575
+ CASE
576
+ WHEN right(keepUntil, 1) = 'Z' THEN CAST(keepUntil as timestamp with time zone)
577
+ ELSE startAfter + CAST(COALESCE(keepUntil, (q.retention_minutes * 60)::text, keepUntilDefault, '14 days') as interval)
578
+ END as keepUntil,
579
+ COALESCE(retryLimit, q.retry_limit, retryLimitDefault, 2) as retryLimit,
580
+ CASE
581
+ WHEN COALESCE(retryBackoff, q.retry_backoff, retryBackoffDefault, false)
582
+ THEN GREATEST(COALESCE(retryDelay, q.retry_delay, retryDelayDefault), 1)
583
+ ELSE COALESCE(retryDelay, q.retry_delay, retryDelayDefault, 0)
584
+ END as retryDelay,
585
+ COALESCE(retryBackoff, q.retry_backoff, retryBackoffDefault, false) as retryBackoff,
586
+ q.policy
567
587
  FROM
568
- ( SELECT *,
569
- CASE
570
- WHEN right(keepUntilValue, 1) = 'Z' THEN CAST(keepUntilValue as timestamp with time zone)
571
- ELSE startAfter + CAST(COALESCE(keepUntilValue,'0') as interval)
572
- END as keepUntil
573
- FROM
574
- ( SELECT *,
588
+ ( SELECT
589
+ COALESCE($1::uuid, gen_random_uuid()) as id,
590
+ $2 as name,
591
+ $3::jsonb as data,
592
+ COALESCE($4::int, 0) as priority,
575
593
  CASE
576
- WHEN right(startAfterValue, 1) = 'Z' THEN CAST(startAfterValue as timestamp with time zone)
577
- ELSE now() + CAST(COALESCE(startAfterValue,'0') as interval)
578
- END as startAfter
579
- FROM
580
- ( SELECT
581
- $1::uuid as id,
582
- $2::text as name,
583
- $3::int as priority,
584
- '${states.created}'::${schema}.job_state as state,
585
- $4::int as retryLimit,
586
- $5::text as startAfterValue,
587
- CAST($6 as interval) as expireIn,
588
- $7::jsonb as data,
589
- $8::text as singletonKey,
590
- CASE
591
- WHEN $9::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($9 * floor((date_part('epoch', now()) + $10) / $9))
592
- ELSE NULL
593
- END as singletonOn,
594
- $11::int as retryDelay,
595
- $12::bool as retryBackoff,
596
- $13::text as keepUntilValue,
597
- $14::boolean as on_complete
598
- ) j1
599
- ) j2
600
- ) j3
594
+ WHEN right($5, 1) = 'Z' THEN CAST($5 as timestamp with time zone)
595
+ ELSE now() + CAST(COALESCE($5,'0') as interval)
596
+ END as startAfter,
597
+ $6 as singletonKey,
598
+ CASE
599
+ WHEN $7::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($7 * floor((date_part('epoch', now()) + $8) / $7))
600
+ ELSE NULL
601
+ END as singletonOn,
602
+ $9 as deadletter,
603
+ $10 as expireIn,
604
+ $11 as expireInDefault,
605
+ $12 as keepUntil,
606
+ $13 as keepUntilDefault,
607
+ $14::int as retryLimit,
608
+ $15::int as retryLimitDefault,
609
+ $16::int as retryDelay,
610
+ $17::int as retryDelayDefault,
611
+ $18::bool as retryBackoff,
612
+ $19::bool as retryBackoffDefault
613
+ ) j LEFT JOIN ${schema}.queue q ON j.name = q.name
601
614
  ON CONFLICT DO NOTHING
602
615
  RETURNING id
603
616
  `
@@ -605,52 +618,76 @@ function insertJob (schema) {
605
618
 
606
619
  function insertJobs (schema) {
607
620
  return `
621
+ WITH defaults as (
622
+ SELECT
623
+ $2 as expireIn,
624
+ $3 as keepUntil,
625
+ $4::int as retryLimit,
626
+ $5::int as retryDelay,
627
+ $6::bool as retryBackoff
628
+ )
608
629
  INSERT INTO ${schema}.job (
609
630
  id,
610
631
  name,
611
632
  data,
612
633
  priority,
613
634
  startAfter,
635
+ singletonKey,
636
+ deadletter,
614
637
  expireIn,
638
+ keepUntil,
615
639
  retryLimit,
616
640
  retryDelay,
617
641
  retryBackoff,
618
- singletonKey,
619
- keepUntil,
620
- on_complete
642
+ policy
621
643
  )
622
644
  SELECT
623
645
  COALESCE(id, gen_random_uuid()) as id,
624
- name,
646
+ j.name,
625
647
  data,
626
- COALESCE(priority, 0) as priority,
627
- COALESCE("startAfter", now()) as startAfter,
628
- COALESCE("expireInSeconds", 15 * 60) * interval '1s' as expireIn,
629
- COALESCE("retryLimit", 0) as retryLimit,
630
- COALESCE("retryDelay", 0) as retryDelay,
631
- COALESCE("retryBackoff", false) as retryBackoff,
648
+ COALESCE(priority, 0),
649
+ COALESCE("startAfter", now()),
632
650
  "singletonKey",
633
- COALESCE("keepUntil", now() + interval '14 days') as keepUntil,
634
- COALESCE("onComplete", false) as onComplete
635
- FROM json_to_recordset($1) as x(
651
+ COALESCE("deadLetter", q.dead_letter),
652
+ CASE
653
+ WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds" * interval '1s'
654
+ WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
655
+ WHEN defaults.expireIn IS NOT NULL THEN CAST(defaults.expireIn as interval)
656
+ ELSE interval '15 minutes'
657
+ END as expireIn,
658
+ CASE
659
+ WHEN "keepUntil" IS NOT NULL THEN "keepUntil"
660
+ ELSE COALESCE("startAfter", now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keepUntil, '14 days') as interval)
661
+ END as keepUntil,
662
+ COALESCE("retryLimit", q.retry_limit, defaults.retryLimit, 2),
663
+ CASE
664
+ WHEN COALESCE("retryBackoff", q.retry_backoff, defaults.retryBackoff, false)
665
+ THEN GREATEST(COALESCE("retryDelay", q.retry_delay, defaults.retryDelay), 1)
666
+ ELSE COALESCE("retryDelay", q.retry_delay, defaults.retryDelay, 0)
667
+ END as retryDelay,
668
+ COALESCE("retryBackoff", q.retry_backoff, defaults.retryBackoff, false) as retryBackoff,
669
+ q.policy
670
+ FROM json_to_recordset($1) as j (
636
671
  id uuid,
637
672
  name text,
638
673
  priority integer,
639
674
  data jsonb,
675
+ "startAfter" timestamp with time zone,
640
676
  "retryLimit" integer,
641
677
  "retryDelay" integer,
642
678
  "retryBackoff" boolean,
643
- "startAfter" timestamp with time zone,
644
679
  "singletonKey" text,
645
680
  "expireInSeconds" integer,
646
681
  "keepUntil" timestamp with time zone,
647
- "onComplete" boolean
682
+ "deadLetter" text
648
683
  )
684
+ LEFT JOIN ${schema}.queue q ON j.name = q.name,
685
+ defaults
649
686
  ON CONFLICT DO NOTHING
650
687
  `
651
688
  }
652
689
 
653
- function purge (schema, interval) {
690
+ function drop (schema, interval) {
654
691
  return `
655
692
  DELETE FROM ${schema}.archive
656
693
  WHERE archivedOn < (now() - interval '${interval}')
@@ -661,22 +698,16 @@ function archive (schema, completedInterval, failedInterval = completedInterval)
661
698
  return `
662
699
  WITH archived_rows AS (
663
700
  DELETE FROM ${schema}.job
664
- WHERE (
665
- state <> '${states.failed}' AND completedOn < (now() - interval '${completedInterval}')
666
- )
667
- OR (
668
- state = '${states.failed}' AND completedOn < (now() - interval '${failedInterval}')
669
- )
670
- OR (
671
- state < '${states.active}' AND keepUntil < now()
672
- )
701
+ WHERE (state <> '${states.failed}' AND completedOn < (now() - interval '${completedInterval}'))
702
+ OR (state = '${states.failed}' AND completedOn < (now() - interval '${failedInterval}'))
703
+ OR (state < '${states.active}' AND keepUntil < now())
673
704
  RETURNING *
674
705
  )
675
706
  INSERT INTO ${schema}.archive (
676
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, on_complete, output
707
+ id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
677
708
  )
678
709
  SELECT
679
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, on_complete, output
710
+ id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
680
711
  FROM archived_rows
681
712
  `
682
713
  }
@@ -689,9 +720,23 @@ function countStates (schema) {
689
720
  `
690
721
  }
691
722
 
692
- function advisoryLock (schema) {
723
+ function locked (schema, query) {
724
+ if (Array.isArray(query)) {
725
+ query = query.join(';\n')
726
+ }
727
+
728
+ return `
729
+ BEGIN;
730
+ SET LOCAL lock_timeout = '30s';
731
+ ${advisoryLock(schema)};
732
+ ${query};
733
+ COMMIT;
734
+ `
735
+ }
736
+
737
+ function advisoryLock (schema, key) {
693
738
  return `SELECT pg_advisory_xact_lock(
694
- ('x' || md5(current_database() || '.pgboss.${schema}'))::bit(64)::bigint
739
+ ('x' || md5(current_database() || '.pgboss.${schema}${key || ''}'))::bit(64)::bigint
695
740
  )`
696
741
  }
697
742
 
@@ -709,5 +754,5 @@ function getArchivedJobById (schema) {
709
754
  }
710
755
 
711
756
  function getJobByTableAndId (schema, table) {
712
- return `SELECT * From ${schema}.${table} WHERE id = $1`
757
+ return `SELECT * FROM ${schema}.${table} WHERE id = $1`
713
758
  }