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/plans.js CHANGED
@@ -1,24 +1,22 @@
1
- const assert = require('assert')
1
+ const DEFAULT_SCHEMA = 'pgboss'
2
+ const MIGRATE_RACE_MESSAGE = 'division by zero'
3
+ const CREATE_RACE_MESSAGE = 'already exists'
2
4
 
3
- const states = {
5
+ const JOB_STATES = Object.freeze({
4
6
  created: 'created',
5
7
  retry: 'retry',
6
8
  active: 'active',
7
9
  completed: 'completed',
8
10
  cancelled: 'cancelled',
9
11
  failed: 'failed'
10
- }
12
+ })
11
13
 
12
- const DEFAULT_SCHEMA = 'pgboss'
13
- const MIGRATE_RACE_MESSAGE = 'division by zero'
14
- const CREATE_RACE_MESSAGE = 'already exists'
15
-
16
- const QUEUE_POLICY = {
14
+ const QUEUE_POLICIES = Object.freeze({
17
15
  standard: 'standard',
18
16
  short: 'short',
19
17
  singleton: 'singleton',
20
18
  stately: 'stately'
21
- }
19
+ })
22
20
 
23
21
  module.exports = {
24
22
  create,
@@ -30,6 +28,7 @@ module.exports = {
30
28
  completeJobs,
31
29
  cancelJobs,
32
30
  resumeJobs,
31
+ deleteJobs,
33
32
  failJobsById,
34
33
  failJobsByTimeout,
35
34
  insertJob,
@@ -44,57 +43,49 @@ module.exports = {
44
43
  archive,
45
44
  drop,
46
45
  countStates,
47
- createQueue,
48
46
  updateQueue,
49
- partitionCreateJobName,
50
- dropJobTablePartition,
51
- deleteQueueRecords,
47
+ createQueue,
48
+ deleteQueue,
49
+ getQueues,
52
50
  getQueueByName,
53
51
  getQueueSize,
54
52
  purgeQueue,
55
53
  clearStorage,
56
- getMaintenanceTime,
57
- setMaintenanceTime,
58
- getMonitorTime,
59
- setMonitorTime,
60
- getCronTime,
61
- setCronTime,
54
+ trySetMaintenanceTime,
55
+ trySetMonitorTime,
56
+ trySetCronTime,
62
57
  locked,
63
- advisoryLock,
64
58
  assertMigration,
65
59
  getArchivedJobById,
66
60
  getJobById,
67
- QUEUE_POLICY,
68
- states: { ...states },
61
+ QUEUE_POLICIES,
62
+ JOB_STATES,
69
63
  MIGRATE_RACE_MESSAGE,
70
64
  CREATE_RACE_MESSAGE,
71
65
  DEFAULT_SCHEMA
72
66
  }
73
67
 
68
+ const assert = require('assert')
69
+
74
70
  function create (schema, version) {
75
71
  const commands = [
76
72
  createSchema(schema),
77
73
  createEnumJobState(schema),
78
74
 
75
+ createTableVersion(schema),
76
+ createTableQueue(schema),
77
+ createTableSchedule(schema),
78
+ createTableSubscription(schema),
79
+
79
80
  createTableJob(schema),
80
- createIndexJobName(schema),
81
- createIndexJobFetch(schema),
82
- createIndexJobPolicyStately(schema),
83
- createIndexJobPolicyShort(schema),
84
- createIndexJobPolicySingleton(schema),
85
- createIndexJobThrottleOn(schema),
86
- createIndexJobThrottleKey(schema),
87
81
 
88
82
  createTableArchive(schema),
89
83
  createPrimaryKeyArchive(schema),
90
84
  createColumnArchiveArchivedOn(schema),
91
85
  createIndexArchiveArchivedOn(schema),
92
- createIndexArchiveName(schema),
93
86
 
94
- createTableVersion(schema),
95
- createTableQueue(schema),
96
- createTableSchedule(schema),
97
- createTableSubscription(schema),
87
+ createQueueFunction(schema),
88
+ deleteQueueFunction(schema),
98
89
 
99
90
  insertVersion(schema, version)
100
91
  ]
@@ -108,6 +99,21 @@ function createSchema (schema) {
108
99
  `
109
100
  }
110
101
 
102
+ function createEnumJobState (schema) {
103
+ // ENUM definition order is important
104
+ // base type is numeric and first values are less than last values
105
+ return `
106
+ CREATE TYPE ${schema}.job_state AS ENUM (
107
+ '${JOB_STATES.created}',
108
+ '${JOB_STATES.retry}',
109
+ '${JOB_STATES.active}',
110
+ '${JOB_STATES.completed}',
111
+ '${JOB_STATES.cancelled}',
112
+ '${JOB_STATES.failed}'
113
+ )
114
+ `
115
+ }
116
+
111
117
  function createTableVersion (schema) {
112
118
  return `
113
119
  CREATE TABLE ${schema}.version (
@@ -119,17 +125,48 @@ function createTableVersion (schema) {
119
125
  `
120
126
  }
121
127
 
122
- function createEnumJobState (schema) {
123
- // ENUM definition order is important
124
- // base type is numeric and first values are less than last values
128
+ function createTableQueue (schema) {
125
129
  return `
126
- CREATE TYPE ${schema}.job_state AS ENUM (
127
- '${states.created}',
128
- '${states.retry}',
129
- '${states.active}',
130
- '${states.completed}',
131
- '${states.cancelled}',
132
- '${states.failed}'
130
+ CREATE TABLE ${schema}.queue (
131
+ name text,
132
+ policy text,
133
+ retry_limit int,
134
+ retry_delay int,
135
+ retry_backoff bool,
136
+ expire_seconds int,
137
+ retention_minutes int,
138
+ dead_letter text REFERENCES ${schema}.queue (name),
139
+ partition_name text,
140
+ created_on timestamp with time zone not null default now(),
141
+ updated_on timestamp with time zone not null default now(),
142
+ PRIMARY KEY (name)
143
+ )
144
+ `
145
+ }
146
+
147
+ function createTableSchedule (schema) {
148
+ return `
149
+ CREATE TABLE ${schema}.schedule (
150
+ name text REFERENCES ${schema}.queue ON DELETE CASCADE,
151
+ cron text not null,
152
+ timezone text,
153
+ data jsonb,
154
+ options jsonb,
155
+ created_on timestamp with time zone not null default now(),
156
+ updated_on timestamp with time zone not null default now(),
157
+ PRIMARY KEY (name)
158
+ )
159
+ `
160
+ }
161
+
162
+ function createTableSubscription (schema) {
163
+ return `
164
+ CREATE TABLE ${schema}.subscription (
165
+ event text not null,
166
+ name text not null REFERENCES ${schema}.queue ON DELETE CASCADE,
167
+ created_on timestamp with time zone not null default now(),
168
+ updated_on timestamp with time zone not null default now(),
169
+ PRIMARY KEY(event, name)
133
170
  )
134
171
  `
135
172
  }
@@ -141,146 +178,238 @@ function createTableJob (schema) {
141
178
  name text not null,
142
179
  priority integer not null default(0),
143
180
  data jsonb,
144
- state ${schema}.job_state not null default('${states.created}'),
145
- retryLimit integer not null default(0),
146
- retryCount integer not null default(0),
147
- retryDelay integer not null default(0),
148
- retryBackoff boolean not null default false,
149
- startAfter timestamp with time zone not null default now(),
150
- startedOn timestamp with time zone,
151
- singletonKey text,
152
- singletonOn timestamp without time zone,
153
- expireIn interval not null default interval '15 minutes',
154
- createdOn timestamp with time zone not null default now(),
155
- completedOn timestamp with time zone,
156
- keepUntil timestamp with time zone NOT NULL default now() + interval '14 days',
181
+ state ${schema}.job_state not null default('${JOB_STATES.created}'),
182
+ retry_limit integer not null default(0),
183
+ retry_count integer not null default(0),
184
+ retry_delay integer not null default(0),
185
+ retry_backoff boolean not null default false,
186
+ start_after timestamp with time zone not null default now(),
187
+ started_on timestamp with time zone,
188
+ singleton_key text,
189
+ singleton_on timestamp without time zone,
190
+ expire_in interval not null default interval '15 minutes',
191
+ created_on timestamp with time zone not null default now(),
192
+ completed_on timestamp with time zone,
193
+ keep_until timestamp with time zone NOT NULL default now() + interval '14 days',
157
194
  output jsonb,
158
- deadletter text,
159
- policy text,
160
- CONSTRAINT job_pkey PRIMARY KEY (name, id)
195
+ dead_letter text,
196
+ policy text
161
197
  ) PARTITION BY LIST (name)
162
198
  `
163
199
  }
164
200
 
165
- function partitionCreateJobName (schema, name) {
201
+ const baseJobColumns = 'id, name, data, EXTRACT(epoch FROM expire_in) as "expireInSeconds"'
202
+ const allJobColumns = `${baseJobColumns},
203
+ policy,
204
+ state,
205
+ priority,
206
+ retry_limit as "retryLimit",
207
+ retry_count as "retryCount",
208
+ retry_delay as "retryDelay",
209
+ retry_backoff as "retryBackoff",
210
+ start_after as "startAfter",
211
+ started_on as "startedOn",
212
+ singleton_key as "singletonKey",
213
+ singleton_on as "singletonOn",
214
+ expire_in as "expireIn",
215
+ created_on as "createdOn",
216
+ completed_on as "completedOn",
217
+ keep_until as "keepUntil",
218
+ dead_letter as "deadLetter",
219
+ output
220
+ `
221
+
222
+ function createQueueFunction (schema) {
166
223
  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}');
224
+ CREATE FUNCTION ${schema}.create_queue(queue_name text, options json)
225
+ RETURNS VOID AS
226
+ $$
227
+ DECLARE
228
+ table_name varchar := 'j' || encode(sha224(queue_name::bytea), 'hex');
229
+ BEGIN
230
+
231
+ INSERT INTO ${schema}.queue (
232
+ name,
233
+ policy,
234
+ retry_limit,
235
+ retry_delay,
236
+ retry_backoff,
237
+ expire_seconds,
238
+ retention_minutes,
239
+ dead_letter,
240
+ partition_name
241
+ )
242
+ VALUES (
243
+ queue_name,
244
+ options->>'policy',
245
+ (options->>'retryLimit')::int,
246
+ (options->>'retryDelay')::int,
247
+ (options->>'retryBackoff')::bool,
248
+ (options->>'expireInSeconds')::int,
249
+ (options->>'retentionMinutes')::int,
250
+ options->>'deadLetter',
251
+ table_name
252
+ );
253
+
254
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', table_name);
255
+
256
+ EXECUTE format('${formatPartitionCommand(createPrimaryKeyJob(schema))}', table_name);
257
+ EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJob(schema))}', table_name);
258
+ EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJobDeadLetter(schema))}', table_name);
259
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicyShort(schema))}', table_name);
260
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicySingleton(schema))}', table_name);
261
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicyStately(schema))}', table_name);
262
+ EXECUTE format('${formatPartitionCommand(createIndexJobThrottle(schema))}', table_name);
263
+ EXECUTE format('${formatPartitionCommand(createIndexJobFetch(schema))}', table_name);
264
+
265
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', table_name, queue_name);
266
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', table_name, queue_name);
267
+ END;
268
+ $$
269
+ LANGUAGE plpgsql;
170
270
  `
171
271
  }
172
272
 
173
- function dropJobTablePartition (schema, name) {
174
- return `DROP TABLE IF EXISTS ${schema}.job_${name}`
273
+ function formatPartitionCommand (command) {
274
+ return command.replace('.job', '.%1$I').replace('job_i', '%1$s_i').replaceAll('\'', '\'\'')
175
275
  }
176
276
 
177
- function createPrimaryKeyArchive (schema) {
178
- return `ALTER TABLE ${schema}.archive ADD CONSTRAINT archive_pkey PRIMARY KEY (name, id)`
277
+ function deleteQueueFunction (schema) {
278
+ return `
279
+ CREATE FUNCTION ${schema}.delete_queue(queue_name text)
280
+ RETURNS VOID AS
281
+ $$
282
+ DECLARE
283
+ table_name varchar;
284
+ BEGIN
285
+ WITH deleted as (
286
+ DELETE FROM ${schema}.queue
287
+ WHERE name = queue_name
288
+ RETURNING partition_name
289
+ )
290
+ SELECT partition_name from deleted INTO table_name;
291
+
292
+ EXECUTE format('DROP TABLE IF EXISTS ${schema}.%I', table_name);
293
+ END;
294
+ $$
295
+ LANGUAGE plpgsql;
296
+ `
179
297
  }
180
298
 
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}'`
299
+ function createQueue (schema) {
300
+ return `SELECT ${schema}.create_queue($1, $2)`
183
301
  }
184
302
 
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}'`
303
+ function deleteQueue (schema) {
304
+ return `SELECT ${schema}.delete_queue($1)`
187
305
  }
188
306
 
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}'`
307
+ function createPrimaryKeyJob (schema) {
308
+ return `ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)`
191
309
  }
192
310
 
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`
311
+ function createQueueForeignKeyJob (schema) {
312
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`
195
313
  }
196
314
 
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`
315
+ function createQueueForeignKeyJobDeadLetter (schema) {
316
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`
199
317
  }
200
318
 
201
- function createIndexJobName (schema) {
202
- return `CREATE INDEX job_name ON ${schema}.job (name)`
319
+ function createPrimaryKeyArchive (schema) {
320
+ return `ALTER TABLE ${schema}.archive ADD PRIMARY KEY (name, id)`
203
321
  }
204
322
 
205
- function createIndexJobFetch (schema) {
206
- return `CREATE INDEX job_fetch ON ${schema}.job (name, startAfter) INCLUDE (priority, createdOn, id) WHERE state < '${states.active}'`
323
+ function createIndexJobPolicyShort (schema) {
324
+ return `CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.created}' AND policy = '${QUEUE_POLICIES.short}';`
207
325
  }
208
326
 
209
- function createTableArchive (schema) {
210
- return `CREATE TABLE ${schema}.archive (LIKE ${schema}.job)`
327
+ function createIndexJobPolicySingleton (schema) {
328
+ return `CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.singleton}'`
211
329
  }
212
330
 
213
- function createColumnArchiveArchivedOn (schema) {
214
- return `ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
331
+ function createIndexJobPolicyStately (schema) {
332
+ return `CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.stately}'`
215
333
  }
216
334
 
217
- function createIndexArchiveArchivedOn (schema) {
218
- return `CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
335
+ function createIndexJobThrottle (schema) {
336
+ return `CREATE UNIQUE INDEX job_i4 ON ${schema}.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> '${JOB_STATES.cancelled}' AND singleton_on IS NOT NULL`
219
337
  }
220
338
 
221
- function createIndexArchiveName (schema) {
222
- return `CREATE INDEX archive_name_idx ON ${schema}.archive(name)`
339
+ function createIndexJobFetch (schema) {
340
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${JOB_STATES.active}'`
223
341
  }
224
342
 
225
- function getMaintenanceTime (schema) {
226
- return `SELECT maintained_on, EXTRACT( EPOCH FROM (now() - maintained_on) ) seconds_ago FROM ${schema}.version`
343
+ function createTableArchive (schema) {
344
+ return `CREATE TABLE ${schema}.archive (LIKE ${schema}.job)`
227
345
  }
228
346
 
229
- function setMaintenanceTime (schema) {
230
- return `UPDATE ${schema}.version SET maintained_on = now()`
347
+ function createColumnArchiveArchivedOn (schema) {
348
+ return `ALTER TABLE ${schema}.archive ADD archived_on timestamptz NOT NULL DEFAULT now()`
231
349
  }
232
350
 
233
- function getMonitorTime (schema) {
234
- return `SELECT monitored_on, EXTRACT( EPOCH FROM (now() - monitored_on) ) seconds_ago FROM ${schema}.version`
351
+ function createIndexArchiveArchivedOn (schema) {
352
+ return `CREATE INDEX archive_i1 ON ${schema}.archive(archived_on)`
235
353
  }
236
354
 
237
- function setMonitorTime (schema) {
238
- return `UPDATE ${schema}.version SET monitored_on = now()`
355
+ function trySetMaintenanceTime (schema) {
356
+ return trySetTimestamp(schema, 'maintained_on')
239
357
  }
240
358
 
241
- function setCronTime (schema, time) {
242
- time = time || 'now()'
243
- return `UPDATE ${schema}.version SET cron_on = ${time}`
359
+ function trySetMonitorTime (schema) {
360
+ return trySetTimestamp(schema, 'monitored_on')
244
361
  }
245
362
 
246
- function getCronTime (schema) {
247
- return `SELECT cron_on, EXTRACT( EPOCH FROM (now() - cron_on) ) seconds_ago FROM ${schema}.version`
363
+ function trySetCronTime (schema) {
364
+ return trySetTimestamp(schema, 'cron_on')
248
365
  }
249
366
 
250
- function createQueue (schema) {
367
+ function trySetTimestamp (schema, column) {
251
368
  return `
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)
369
+ UPDATE ${schema}.version SET ${column} = now()
370
+ WHERE EXTRACT( EPOCH FROM (now() - COALESCE(${column}, now() - interval '1 week') ) ) > $1
371
+ RETURNING true
254
372
  `
255
373
  }
256
374
 
257
375
  function updateQueue (schema) {
258
376
  return `
259
377
  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)
378
+ policy = COALESCE($2, policy),
379
+ retry_limit = COALESCE($3, retry_limit),
380
+ retry_delay = COALESCE($4, retry_delay),
381
+ retry_backoff = COALESCE($5, retry_backoff),
382
+ expire_seconds = COALESCE($6, expire_seconds),
383
+ retention_minutes = COALESCE($7, retention_minutes),
384
+ dead_letter = COALESCE($8, dead_letter),
385
+ updated_on = now()
266
386
  WHERE name = $1
267
387
  `
268
388
  }
269
389
 
270
- function getQueueByName (schema) {
271
- return `SELECT * FROM ${schema}.queue WHERE name = $1`
390
+ function getQueues (schema) {
391
+ return `
392
+ SELECT
393
+ name,
394
+ policy,
395
+ retry_limit as "retryLimit",
396
+ retry_delay as "retryDelay",
397
+ retry_backoff as "retryBackoff",
398
+ expire_seconds as "expireInSeconds",
399
+ retention_minutes as "retentionMinutes",
400
+ dead_letter as "deadLetter",
401
+ created_on as "createdOn",
402
+ updated_on as "updatedOn"
403
+ FROM ${schema}.queue
404
+ `
272
405
  }
273
406
 
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
279
- `
407
+ function getQueueByName (schema) {
408
+ return `${getQueues(schema)} WHERE name = $1`
280
409
  }
281
410
 
282
411
  function purgeQueue (schema) {
283
- return `DELETE from ${schema}.job WHERE name = $1 and state < '${states.active}'`
412
+ return `DELETE from ${schema}.job WHERE name = $1 and state < '${JOB_STATES.active}'`
284
413
  }
285
414
 
286
415
  function clearStorage (schema) {
@@ -288,57 +417,13 @@ function clearStorage (schema) {
288
417
  }
289
418
 
290
419
  function getQueueSize (schema, options = {}) {
291
- options.before = options.before || states.active
292
- assert(options.before in states, `${options.before} is not a valid state`)
420
+ options.before = options.before || JOB_STATES.active
421
+ assert(options.before in JOB_STATES, `${options.before} is not a valid state`)
293
422
  return `SELECT count(*) as count FROM ${schema}.job WHERE name = $1 AND state < '${options.before}'`
294
423
  }
295
424
 
296
- function createTableQueue (schema) {
297
- return `
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
- )
309
- `
310
- }
311
-
312
- function createTableSchedule (schema) {
313
- return `
314
- CREATE TABLE ${schema}.schedule (
315
- name text primary key,
316
- cron text not null,
317
- timezone text,
318
- data jsonb,
319
- options jsonb,
320
- created_on timestamp with time zone not null default now(),
321
- updated_on timestamp with time zone not null default now()
322
- )
323
- `
324
- }
325
-
326
- function createTableSubscription (schema) {
327
- return `
328
- CREATE TABLE ${schema}.subscription (
329
- event text not null,
330
- name text not null,
331
- created_on timestamp with time zone not null default now(),
332
- updated_on timestamp with time zone not null default now(),
333
- PRIMARY KEY(event, name)
334
- )
335
- `
336
- }
337
-
338
425
  function getSchedules (schema) {
339
- return `
340
- SELECT * FROM ${schema}.schedule
341
- `
426
+ return `SELECT * FROM ${schema}.schedule`
342
427
  }
343
428
 
344
429
  function schedule (schema) {
@@ -412,20 +497,19 @@ function fetchNextJob (schema) {
412
497
  SELECT id
413
498
  FROM ${schema}.job
414
499
  WHERE name = $1
415
- AND state < '${states.active}'
416
- AND startAfter < now()
417
- ORDER BY ${priority && 'priority desc, '} createdOn, id
500
+ AND state < '${JOB_STATES.active}'
501
+ AND start_after < now()
502
+ ORDER BY ${priority && 'priority desc, '} created_on, id
418
503
  LIMIT $2
419
504
  FOR UPDATE SKIP LOCKED
420
505
  )
421
506
  UPDATE ${schema}.job j SET
422
- state = '${states.active}',
423
- startedOn = now(),
424
- retryCount = CASE WHEN startedOn IS NOT NULL THEN retryCount + 1 ELSE retryCount END
507
+ state = '${JOB_STATES.active}',
508
+ started_on = now(),
509
+ retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
425
510
  FROM next
426
511
  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
512
+ RETURNING j.${includeMetadata ? allJobColumns : baseJobColumns}
429
513
  `
430
514
  }
431
515
 
@@ -433,12 +517,12 @@ function completeJobs (schema) {
433
517
  return `
434
518
  WITH results AS (
435
519
  UPDATE ${schema}.job
436
- SET completedOn = now(),
437
- state = '${states.completed}',
520
+ SET completed_on = now(),
521
+ state = '${JOB_STATES.completed}',
438
522
  output = $3::jsonb
439
523
  WHERE name = $1
440
524
  AND id IN (SELECT UNNEST($2::uuid[]))
441
- AND state = '${states.active}'
525
+ AND state = '${JOB_STATES.active}'
442
526
  RETURNING *
443
527
  )
444
528
  SELECT COUNT(*) FROM results
@@ -446,14 +530,14 @@ function completeJobs (schema) {
446
530
  }
447
531
 
448
532
  function failJobsById (schema) {
449
- const where = `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${states.completed}'`
533
+ const where = `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`
450
534
  const output = '$3::jsonb'
451
535
 
452
536
  return failJobs(schema, where, output)
453
537
  }
454
538
 
455
539
  function failJobsByTimeout (schema) {
456
- const where = `state = '${states.active}' AND (startedOn + expireIn) < now()`
540
+ const where = `state = '${JOB_STATES.active}' AND (started_on + expire_in) < now()`
457
541
  const output = '\'{ "value": { "message": "job failed by timeout in active state" } }\'::jsonb'
458
542
  return failJobs(schema, where, output)
459
543
  }
@@ -463,36 +547,36 @@ function failJobs (schema, where, output) {
463
547
  WITH results AS (
464
548
  UPDATE ${schema}.job SET
465
549
  state = CASE
466
- WHEN retryCount < retryLimit THEN '${states.retry}'::${schema}.job_state
467
- ELSE '${states.failed}'::${schema}.job_state
550
+ WHEN retry_count < retry_limit THEN '${JOB_STATES.retry}'::${schema}.job_state
551
+ ELSE '${JOB_STATES.failed}'::${schema}.job_state
468
552
  END,
469
- completedOn = CASE
470
- WHEN retryCount < retryLimit THEN NULL
553
+ completed_on = CASE
554
+ WHEN retry_count < retry_limit THEN NULL
471
555
  ELSE now()
472
556
  END,
473
- startAfter = CASE
474
- WHEN retryCount = retryLimit THEN startAfter
475
- WHEN NOT retryBackoff THEN now() + retryDelay * interval '1'
557
+ start_after = CASE
558
+ WHEN retry_count = retry_limit THEN start_after
559
+ WHEN NOT retry_backoff THEN now() + retry_delay * interval '1'
476
560
  ELSE now() + (
477
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 +
478
- retryDelay * 2 ^ LEAST(16, retryCount + 1) / 2 * random()
561
+ retry_delay * 2 ^ LEAST(16, retry_count + 1) / 2 +
562
+ retry_delay * 2 ^ LEAST(16, retry_count + 1) / 2 * random()
479
563
  ) * interval '1'
480
564
  END,
481
565
  output = ${output}
482
566
  WHERE ${where}
483
567
  RETURNING *
484
568
  ), dlq_jobs as (
485
- INSERT INTO ${schema}.job (name, data, output, retryLimit, keepUntil)
569
+ INSERT INTO ${schema}.job (name, data, output, retry_limit, keep_until)
486
570
  SELECT
487
- deadletter,
571
+ dead_letter,
488
572
  data,
489
573
  output,
490
- retryLimit,
491
- keepUntil + (keepUntil - startAfter)
574
+ retry_limit,
575
+ keep_until + (keep_until - start_after)
492
576
  FROM results
493
- WHERE state = '${states.failed}'
494
- AND deadletter IS NOT NULL
495
- AND NOT name = deadletter
577
+ WHERE state = '${JOB_STATES.failed}'
578
+ AND dead_letter IS NOT NULL
579
+ AND NOT name = dead_letter
496
580
  )
497
581
  SELECT COUNT(*) FROM results
498
582
  `
@@ -502,11 +586,11 @@ function cancelJobs (schema) {
502
586
  return `
503
587
  with results as (
504
588
  UPDATE ${schema}.job
505
- SET completedOn = now(),
506
- state = '${states.cancelled}'
589
+ SET completed_on = now(),
590
+ state = '${JOB_STATES.cancelled}'
507
591
  WHERE name = $1
508
592
  AND id IN (SELECT UNNEST($2::uuid[]))
509
- AND state < '${states.completed}'
593
+ AND state < '${JOB_STATES.completed}'
510
594
  RETURNING 1
511
595
  )
512
596
  SELECT COUNT(*) from results
@@ -517,10 +601,23 @@ function resumeJobs (schema) {
517
601
  return `
518
602
  with results as (
519
603
  UPDATE ${schema}.job
520
- SET completedOn = NULL,
521
- state = '${states.created}'
604
+ SET completed_on = NULL,
605
+ state = '${JOB_STATES.created}'
522
606
  WHERE name = $1
523
607
  AND id IN (SELECT UNNEST($2::uuid[]))
608
+ AND state = '${JOB_STATES.cancelled}'
609
+ RETURNING 1
610
+ )
611
+ SELECT COUNT(*) from results
612
+ `
613
+ }
614
+
615
+ function deleteJobs (schema) {
616
+ return `
617
+ with results as (
618
+ DELETE FROM ${schema}.job
619
+ WHERE name = $1
620
+ AND id IN (SELECT UNNEST($2::uuid[]))
524
621
  RETURNING 1
525
622
  )
526
623
  SELECT COUNT(*) from results
@@ -534,15 +631,15 @@ function insertJob (schema) {
534
631
  name,
535
632
  data,
536
633
  priority,
537
- startAfter,
538
- singletonKey,
539
- singletonOn,
540
- deadletter,
541
- expireIn,
542
- keepUntil,
543
- retryLimit,
544
- retryDelay,
545
- retryBackoff,
634
+ start_after,
635
+ singleton_key,
636
+ singleton_on,
637
+ dead_letter,
638
+ expire_in,
639
+ keep_until,
640
+ retry_limit,
641
+ retry_delay,
642
+ retry_backoff,
546
643
  policy
547
644
  )
548
645
  SELECT
@@ -550,27 +647,27 @@ function insertJob (schema) {
550
647
  j.name,
551
648
  data,
552
649
  priority,
553
- startAfter,
554
- singletonKey,
555
- singletonOn,
556
- COALESCE(deadLetter, q.dead_letter) as deadletter,
650
+ start_after,
651
+ singleton_key,
652
+ singleton_on,
653
+ COALESCE(j.dead_letter, q.dead_letter) as dead_letter,
557
654
  CASE
558
- WHEN expireIn IS NOT NULL THEN CAST(expireIn as interval)
655
+ WHEN expire_in IS NOT NULL THEN CAST(expire_in as interval)
559
656
  WHEN q.expire_seconds IS NOT NULL THEN q.expire_seconds * interval '1s'
560
- WHEN expireInDefault IS NOT NULL THEN CAST(expireInDefault as interval)
657
+ WHEN expire_in_default IS NOT NULL THEN CAST(expire_in_default as interval)
561
658
  ELSE interval '15 minutes'
562
- END as expireIn,
659
+ END as expire_in,
563
660
  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,
661
+ WHEN right(keep_until, 1) = 'Z' THEN CAST(keep_until as timestamp with time zone)
662
+ ELSE start_after + CAST(COALESCE(keep_until, (q.retention_minutes * 60)::text, keep_until_default, '14 days') as interval)
663
+ END as keep_until,
664
+ COALESCE(j.retry_limit, q.retry_limit, retry_limit_default, 2) as retry_limit,
568
665
  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,
666
+ WHEN COALESCE(j.retry_backoff, q.retry_backoff, retry_backoff_default, false)
667
+ THEN GREATEST(COALESCE(j.retry_delay, q.retry_delay, retry_delay_default), 1)
668
+ ELSE COALESCE(j.retry_delay, q.retry_delay, retry_delay_default, 0)
669
+ END as retry_delay,
670
+ COALESCE(j.retry_backoff, q.retry_backoff, retry_backoff_default, false) as retry_backoff,
574
671
  q.policy
575
672
  FROM
576
673
  ( SELECT
@@ -581,24 +678,24 @@ function insertJob (schema) {
581
678
  CASE
582
679
  WHEN right($5, 1) = 'Z' THEN CAST($5 as timestamp with time zone)
583
680
  ELSE now() + CAST(COALESCE($5,'0') as interval)
584
- END as startAfter,
585
- $6 as singletonKey,
681
+ END as start_after,
682
+ $6 as singleton_key,
586
683
  CASE
587
684
  WHEN $7::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($7 * floor((date_part('epoch', now()) + $8) / $7))
588
685
  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
686
+ END as singleton_on,
687
+ $9 as dead_letter,
688
+ $10 as expire_in,
689
+ $11 as expire_in_default,
690
+ $12 as keep_until,
691
+ $13 as keep_until_default,
692
+ $14::int as retry_limit,
693
+ $15::int as retry_limit_default,
694
+ $16::int as retry_delay,
695
+ $17::int as retry_delay_default,
696
+ $18::bool as retry_backoff,
697
+ $19::bool as retry_backoff_default
698
+ ) j JOIN ${schema}.queue q ON j.name = q.name
602
699
  ON CONFLICT DO NOTHING
603
700
  RETURNING id
604
701
  `
@@ -608,68 +705,76 @@ function insertJobs (schema) {
608
705
  return `
609
706
  WITH defaults as (
610
707
  SELECT
611
- $2 as expireIn,
612
- $3 as keepUntil,
613
- $4::int as retryLimit,
614
- $5::int as retryDelay,
615
- $6::bool as retryBackoff
708
+ $2 as expire_in,
709
+ $3 as keep_until,
710
+ $4::int as retry_limit,
711
+ $5::int as retry_delay,
712
+ $6::bool as retry_backoff
616
713
  )
617
714
  INSERT INTO ${schema}.job (
618
715
  id,
619
716
  name,
620
717
  data,
621
718
  priority,
622
- startAfter,
623
- singletonKey,
624
- deadletter,
625
- expireIn,
626
- keepUntil,
627
- retryLimit,
628
- retryDelay,
629
- retryBackoff,
719
+ start_after,
720
+ singleton_key,
721
+ dead_letter,
722
+ expire_in,
723
+ keep_until,
724
+ retry_limit,
725
+ retry_delay,
726
+ retry_backoff,
630
727
  policy
631
728
  )
632
729
  SELECT
633
730
  COALESCE(id, gen_random_uuid()) as id,
634
731
  j.name,
635
732
  data,
636
- COALESCE(priority, 0),
637
- COALESCE("startAfter", now()),
638
- "singletonKey",
639
- COALESCE("deadLetter", q.dead_letter),
733
+ COALESCE(priority, 0) as priority,
734
+ j.start_after,
735
+ "singletonKey" as singleton_key,
736
+ COALESCE("deadLetter", q.dead_letter) as dead_letter,
640
737
  CASE
641
738
  WHEN "expireInSeconds" IS NOT NULL THEN "expireInSeconds" * interval '1s'
642
739
  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)
740
+ WHEN defaults.expire_in IS NOT NULL THEN CAST(defaults.expire_in as interval)
644
741
  ELSE interval '15 minutes'
645
- END as expireIn,
742
+ END as expire_in,
646
743
  CASE
647
744
  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),
745
+ ELSE COALESCE(j.start_after, now()) + CAST(COALESCE((q.retention_minutes * 60)::text, defaults.keep_until, '14 days') as interval)
746
+ END as keep_until,
747
+ COALESCE("retryLimit", q.retry_limit, defaults.retry_limit, 2),
651
748
  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,
749
+ WHEN COALESCE("retryBackoff", q.retry_backoff, defaults.retry_backoff, false)
750
+ THEN GREATEST(COALESCE("retryDelay", q.retry_delay, defaults.retry_delay), 1)
751
+ ELSE COALESCE("retryDelay", q.retry_delay, defaults.retry_delay, 0)
752
+ END as retry_delay,
753
+ COALESCE("retryBackoff", q.retry_backoff, defaults.retry_backoff, false) as retry_backoff,
657
754
  q.policy
658
- FROM json_to_recordset($1) as j (
659
- id uuid,
660
- name text,
661
- priority integer,
662
- data jsonb,
663
- "startAfter" timestamp with time zone,
664
- "retryLimit" integer,
665
- "retryDelay" integer,
666
- "retryBackoff" boolean,
667
- "singletonKey" text,
668
- "expireInSeconds" integer,
669
- "keepUntil" timestamp with time zone,
670
- "deadLetter" text
671
- )
672
- LEFT JOIN ${schema}.queue q ON j.name = q.name,
755
+ FROM (
756
+ SELECT *,
757
+ CASE
758
+ WHEN right("startAfter", 1) = 'Z' THEN CAST("startAfter" as timestamp with time zone)
759
+ ELSE now() + CAST(COALESCE("startAfter",'0') as interval)
760
+ END as start_after
761
+ FROM json_to_recordset($1) as x (
762
+ id uuid,
763
+ name text,
764
+ priority integer,
765
+ data jsonb,
766
+ "startAfter" text,
767
+ "retryLimit" integer,
768
+ "retryDelay" integer,
769
+ "retryBackoff" boolean,
770
+ "singletonKey" text,
771
+ "singletonOn" text,
772
+ "expireInSeconds" integer,
773
+ "keepUntil" timestamp with time zone,
774
+ "deadLetter" text
775
+ )
776
+ ) j
777
+ JOIN ${schema}.queue q ON j.name = q.name,
673
778
  defaults
674
779
  ON CONFLICT DO NOTHING
675
780
  `
@@ -678,25 +783,25 @@ function insertJobs (schema) {
678
783
  function drop (schema, interval) {
679
784
  return `
680
785
  DELETE FROM ${schema}.archive
681
- WHERE archivedOn < (now() - interval '${interval}')
786
+ WHERE archived_on < (now() - interval '${interval}')
682
787
  `
683
788
  }
684
789
 
685
790
  function archive (schema, completedInterval, failedInterval = completedInterval) {
791
+ const columns = 'id, name, priority, data, state, retry_limit, retry_count, retry_delay, retry_backoff, start_after, started_on, singleton_key, singleton_on, expire_in, created_on, completed_on, keep_until, dead_letter, policy, output'
792
+
686
793
  return `
687
794
  WITH archived_rows AS (
688
795
  DELETE FROM ${schema}.job
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())
796
+ WHERE (state <> '${JOB_STATES.failed}' AND completed_on < (now() - interval '${completedInterval}'))
797
+ OR (state = '${JOB_STATES.failed}' AND completed_on < (now() - interval '${failedInterval}'))
798
+ OR (state < '${JOB_STATES.active}' AND keep_until < now())
692
799
  RETURNING *
693
800
  )
694
- INSERT INTO ${schema}.archive (
695
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
696
- )
697
- SELECT
698
- id, name, priority, data, state, retryLimit, retryCount, retryDelay, retryBackoff, startAfter, startedOn, singletonKey, singletonOn, expireIn, createdOn, completedOn, keepUntil, deadletter, policy, output
801
+ INSERT INTO ${schema}.archive (${columns})
802
+ SELECT ${columns}
699
803
  FROM archived_rows
804
+ ON CONFLICT DO NOTHING
700
805
  `
701
806
  }
702
807
 
@@ -716,6 +821,7 @@ function locked (schema, query) {
716
821
  return `
717
822
  BEGIN;
718
823
  SET LOCAL lock_timeout = '30s';
824
+ SET LOCAL idle_in_transaction_session_timeout = '30s';
719
825
  ${advisoryLock(schema)};
720
826
  ${query};
721
827
  COMMIT;
@@ -724,7 +830,7 @@ function locked (schema, query) {
724
830
 
725
831
  function advisoryLock (schema, key) {
726
832
  return `SELECT pg_advisory_xact_lock(
727
- ('x' || md5(current_database() || '.pgboss.${schema}${key || ''}'))::bit(64)::bigint
833
+ ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ''}')::bytea), 'hex'))::bit(64)::bigint
728
834
  )`
729
835
  }
730
836
 
@@ -734,13 +840,13 @@ function assertMigration (schema, version) {
734
840
  }
735
841
 
736
842
  function getJobById (schema) {
737
- return getJobByTableAndId(schema, 'job')
843
+ return getJobByTableQueueId(schema, 'job')
738
844
  }
739
845
 
740
846
  function getArchivedJobById (schema) {
741
- return getJobByTableAndId(schema, 'archive')
847
+ return getJobByTableQueueId(schema, 'archive')
742
848
  }
743
849
 
744
- function getJobByTableAndId (schema, table) {
745
- return `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = $2`
850
+ function getJobByTableQueueId (schema, table) {
851
+ return `SELECT ${allJobColumns} FROM ${schema}.${table} WHERE name = $1 AND id = $2`
746
852
  }