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/README.md +11 -27
- package/package.json +7 -11
- package/src/attorney.js +66 -90
- package/src/boss.js +97 -132
- package/src/contractor.js +16 -1
- package/src/db.js +37 -5
- package/src/index.js +65 -55
- package/src/manager.js +213 -130
- package/src/migrationStore.js +0 -105
- package/src/plans.js +329 -296
- package/src/timekeeper.js +8 -7
- package/src/tools.js +28 -0
- package/src/worker.js +4 -4
- package/types.d.ts +27 -45
- package/version.json +1 -1
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
|
-
|
|
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
|
-
|
|
45
|
+
drop,
|
|
44
46
|
countStates,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|
|
137
|
+
function createTableJob (schema) {
|
|
136
138
|
return `
|
|
137
139
|
CREATE TABLE ${schema}.job (
|
|
138
|
-
id 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
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
213
|
+
function createColumnArchiveArchivedOn (schema) {
|
|
166
214
|
return `ALTER TABLE ${schema}.archive ADD archivedOn timestamptz NOT NULL DEFAULT now()`
|
|
167
215
|
}
|
|
168
216
|
|
|
169
|
-
function
|
|
217
|
+
function createIndexArchiveArchivedOn (schema) {
|
|
170
218
|
return `CREATE INDEX archive_archivedon_idx ON ${schema}.archive(archivedon)`
|
|
171
219
|
}
|
|
172
220
|
|
|
173
|
-
function
|
|
174
|
-
return `CREATE INDEX
|
|
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
|
|
182
|
-
return `SELECT
|
|
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
|
|
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
|
-
|
|
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
|
|
224
|
-
// "singleton queue" means "only 1 job can be queued at a time"
|
|
257
|
+
function updateQueue (schema) {
|
|
225
258
|
return `
|
|
226
|
-
|
|
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
|
|
231
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
355
|
-
WITH
|
|
410
|
+
return ({ includeMetadata, priority = true } = {}) => `
|
|
411
|
+
WITH next as (
|
|
356
412
|
SELECT id
|
|
357
|
-
FROM ${schema}.job
|
|
358
|
-
WHERE
|
|
359
|
-
AND
|
|
413
|
+
FROM ${schema}.job
|
|
414
|
+
WHERE name = $1
|
|
415
|
+
AND state < '${states.active}'
|
|
360
416
|
AND startAfter < now()
|
|
361
|
-
|
|
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
|
|
387
|
-
FROM
|
|
388
|
-
WHERE j.id =
|
|
389
|
-
RETURNING ${includeMetadata ? 'j.*' : 'j.id, name, data'},
|
|
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 = $
|
|
434
|
-
WHERE
|
|
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
|
|
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
|
-
|
|
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 =
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
),
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
487
|
+
deadletter,
|
|
488
|
+
data,
|
|
489
|
+
output,
|
|
490
|
+
retryLimit,
|
|
491
|
+
keepUntil + (keepUntil - startAfter)
|
|
472
492
|
FROM results
|
|
473
493
|
WHERE state = '${states.failed}'
|
|
474
|
-
AND
|
|
475
|
-
AND
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
577
|
-
ELSE
|
|
578
|
-
END as
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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)
|
|
627
|
-
COALESCE("startAfter", now())
|
|
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("
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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 *
|
|
745
|
+
return `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = $2`
|
|
713
746
|
}
|