pg-boss 9.0.3 → 10.0.0-beta2

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