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