pg-boss 12.1.1 → 12.3.0

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.
@@ -0,0 +1,868 @@
1
+ //#region src/plans.ts
2
+ const DEFAULT_SCHEMA = "pgboss";
3
+ const MIGRATE_RACE_MESSAGE = "division by zero";
4
+ const CREATE_RACE_MESSAGE = "already exists";
5
+ const FIFTEEN_MINUTES = 900;
6
+ const FORTEEN_DAYS = 3600 * 24 * 14;
7
+ const SEVEN_DAYS = 3600 * 24 * 7;
8
+ const JOB_STATES = Object.freeze({
9
+ created: "created",
10
+ retry: "retry",
11
+ active: "active",
12
+ completed: "completed",
13
+ cancelled: "cancelled",
14
+ failed: "failed"
15
+ });
16
+ const QUEUE_POLICIES = Object.freeze({
17
+ standard: "standard",
18
+ short: "short",
19
+ singleton: "singleton",
20
+ stately: "stately",
21
+ exclusive: "exclusive"
22
+ });
23
+ const QUEUE_DEFAULTS = {
24
+ expire_seconds: FIFTEEN_MINUTES,
25
+ retention_seconds: FORTEEN_DAYS,
26
+ deletion_seconds: SEVEN_DAYS,
27
+ retry_limit: 2,
28
+ retry_delay: 0,
29
+ warning_queued: 0,
30
+ retry_backoff: false,
31
+ partition: false
32
+ };
33
+ const COMMON_JOB_TABLE = "job_common";
34
+ function create(schema, version, options) {
35
+ return locked(schema, [
36
+ options?.createSchema ? createSchema(schema) : "",
37
+ createEnumJobState(schema),
38
+ createTableVersion(schema),
39
+ createTableQueue(schema),
40
+ createTableSchedule(schema),
41
+ createTableSubscription(schema),
42
+ createTableJob(schema),
43
+ createPrimaryKeyJob(schema),
44
+ createTableJobCommon(schema, COMMON_JOB_TABLE),
45
+ createQueueFunction(schema),
46
+ deleteQueueFunction(schema),
47
+ insertVersion(schema, version)
48
+ ]);
49
+ }
50
+ function createSchema(schema) {
51
+ return `CREATE SCHEMA IF NOT EXISTS ${schema}`;
52
+ }
53
+ function createEnumJobState(schema) {
54
+ return `
55
+ CREATE TYPE ${schema}.job_state AS ENUM (
56
+ '${JOB_STATES.created}',
57
+ '${JOB_STATES.retry}',
58
+ '${JOB_STATES.active}',
59
+ '${JOB_STATES.completed}',
60
+ '${JOB_STATES.cancelled}',
61
+ '${JOB_STATES.failed}'
62
+ )
63
+ `;
64
+ }
65
+ function createTableVersion(schema) {
66
+ return `
67
+ CREATE TABLE ${schema}.version (
68
+ version int primary key,
69
+ cron_on timestamp with time zone
70
+ )
71
+ `;
72
+ }
73
+ function createTableQueue(schema) {
74
+ return `
75
+ CREATE TABLE ${schema}.queue (
76
+ name text NOT NULL,
77
+ policy text NOT NULL,
78
+ retry_limit int NOT NULL,
79
+ retry_delay int NOT NULL,
80
+ retry_backoff bool NOT NULL,
81
+ retry_delay_max int,
82
+ expire_seconds int NOT NULL,
83
+ retention_seconds int NOT NULL,
84
+ deletion_seconds int NOT NULL,
85
+ dead_letter text REFERENCES ${schema}.queue (name) CHECK (dead_letter IS DISTINCT FROM name),
86
+ partition bool NOT NULL,
87
+ table_name text NOT NULL,
88
+ deferred_count int NOT NULL default 0,
89
+ queued_count int NOT NULL default 0,
90
+ warning_queued int NOT NULL default 0,
91
+ active_count int NOT NULL default 0,
92
+ total_count int NOT NULL default 0,
93
+ singletons_active text[],
94
+ monitor_on timestamp with time zone,
95
+ maintain_on timestamp with time zone,
96
+ created_on timestamp with time zone not null default now(),
97
+ updated_on timestamp with time zone not null default now(),
98
+ PRIMARY KEY (name)
99
+ )
100
+ `;
101
+ }
102
+ function createTableSchedule(schema) {
103
+ return `
104
+ CREATE TABLE ${schema}.schedule (
105
+ name text REFERENCES ${schema}.queue ON DELETE CASCADE,
106
+ key text not null DEFAULT '',
107
+ cron text not null,
108
+ timezone text,
109
+ data jsonb,
110
+ options jsonb,
111
+ created_on timestamp with time zone not null default now(),
112
+ updated_on timestamp with time zone not null default now(),
113
+ PRIMARY KEY (name, key)
114
+ )
115
+ `;
116
+ }
117
+ function createTableSubscription(schema) {
118
+ return `
119
+ CREATE TABLE ${schema}.subscription (
120
+ event text not null,
121
+ name text not null REFERENCES ${schema}.queue ON DELETE CASCADE,
122
+ created_on timestamp with time zone not null default now(),
123
+ updated_on timestamp with time zone not null default now(),
124
+ PRIMARY KEY(event, name)
125
+ )
126
+ `;
127
+ }
128
+ function createTableJob(schema) {
129
+ return `
130
+ CREATE TABLE ${schema}.job (
131
+ id uuid not null default gen_random_uuid(),
132
+ name text not null,
133
+ priority integer not null default(0),
134
+ data jsonb,
135
+ state ${schema}.job_state not null default '${JOB_STATES.created}',
136
+ retry_limit integer not null default ${QUEUE_DEFAULTS.retry_limit},
137
+ retry_count integer not null default 0,
138
+ retry_delay integer not null default ${QUEUE_DEFAULTS.retry_delay},
139
+ retry_backoff boolean not null default ${QUEUE_DEFAULTS.retry_backoff},
140
+ retry_delay_max integer,
141
+ expire_seconds int not null default ${QUEUE_DEFAULTS.expire_seconds},
142
+ deletion_seconds int not null default ${QUEUE_DEFAULTS.deletion_seconds},
143
+ singleton_key text,
144
+ singleton_on timestamp without time zone,
145
+ start_after timestamp with time zone not null default now(),
146
+ created_on timestamp with time zone not null default now(),
147
+ started_on timestamp with time zone,
148
+ completed_on timestamp with time zone,
149
+ keep_until timestamp with time zone NOT NULL default now() + interval '${QUEUE_DEFAULTS.retention_seconds}',
150
+ output jsonb,
151
+ dead_letter text,
152
+ policy text
153
+ ) PARTITION BY LIST (name)
154
+ `;
155
+ }
156
+ const JOB_COLUMNS_MIN = "id, name, data, expire_seconds as \"expireInSeconds\"";
157
+ const JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
158
+ policy,
159
+ state,
160
+ priority,
161
+ retry_limit as "retryLimit",
162
+ retry_count as "retryCount",
163
+ retry_delay as "retryDelay",
164
+ retry_backoff as "retryBackoff",
165
+ retry_delay_max as "retryDelayMax",
166
+ start_after as "startAfter",
167
+ started_on as "startedOn",
168
+ singleton_key as "singletonKey",
169
+ singleton_on as "singletonOn",
170
+ deletion_seconds as "deleteAfterSeconds",
171
+ created_on as "createdOn",
172
+ completed_on as "completedOn",
173
+ keep_until as "keepUntil",
174
+ dead_letter as "deadLetter",
175
+ output
176
+ `;
177
+ function createTableJobCommon(schema, table) {
178
+ const format = (command) => command.replaceAll(".job", `.${table}`) + ";";
179
+ return `
180
+ CREATE TABLE ${schema}.${table} (LIKE ${schema}.job INCLUDING GENERATED INCLUDING DEFAULTS);
181
+ ${format(createPrimaryKeyJob(schema))}
182
+ ${format(createQueueForeignKeyJob(schema))}
183
+ ${format(createQueueForeignKeyJobDeadLetter(schema))}
184
+ ${format(createIndexJobPolicyShort(schema))}
185
+ ${format(createIndexJobPolicySingleton(schema))}
186
+ ${format(createIndexJobPolicyStately(schema))}
187
+ ${format(createIndexJobPolicyExclusive(schema))}
188
+ ${format(createIndexJobThrottle(schema))}
189
+ ${format(createIndexJobFetch(schema))}
190
+
191
+ ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${table} DEFAULT;
192
+ `;
193
+ }
194
+ function createQueueFunction(schema) {
195
+ return `
196
+ CREATE FUNCTION ${schema}.create_queue(queue_name text, options jsonb)
197
+ RETURNS VOID AS
198
+ $$
199
+ DECLARE
200
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
201
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
202
+ ELSE '${COMMON_JOB_TABLE}'
203
+ END;
204
+ queue_created_on timestamptz;
205
+ BEGIN
206
+
207
+ WITH q as (
208
+ INSERT INTO ${schema}.queue (
209
+ name,
210
+ policy,
211
+ retry_limit,
212
+ retry_delay,
213
+ retry_backoff,
214
+ retry_delay_max,
215
+ expire_seconds,
216
+ retention_seconds,
217
+ deletion_seconds,
218
+ warning_queued,
219
+ dead_letter,
220
+ partition,
221
+ table_name
222
+ )
223
+ VALUES (
224
+ queue_name,
225
+ options->>'policy',
226
+ COALESCE((options->>'retryLimit')::int, ${QUEUE_DEFAULTS.retry_limit}),
227
+ COALESCE((options->>'retryDelay')::int, ${QUEUE_DEFAULTS.retry_delay}),
228
+ COALESCE((options->>'retryBackoff')::bool, ${QUEUE_DEFAULTS.retry_backoff}),
229
+ (options->>'retryDelayMax')::int,
230
+ COALESCE((options->>'expireInSeconds')::int, ${QUEUE_DEFAULTS.expire_seconds}),
231
+ COALESCE((options->>'retentionSeconds')::int, ${QUEUE_DEFAULTS.retention_seconds}),
232
+ COALESCE((options->>'deleteAfterSeconds')::int, ${QUEUE_DEFAULTS.deletion_seconds}),
233
+ COALESCE((options->>'warningQueueSize')::int, ${QUEUE_DEFAULTS.warning_queued}),
234
+ options->>'deadLetter',
235
+ COALESCE((options->>'partition')::bool, ${QUEUE_DEFAULTS.partition}),
236
+ tablename
237
+ )
238
+ ON CONFLICT DO NOTHING
239
+ RETURNING created_on
240
+ )
241
+ SELECT created_on into queue_created_on from q;
242
+
243
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
244
+ RETURN;
245
+ END IF;
246
+
247
+ EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
248
+
249
+ EXECUTE format('${formatPartitionCommand(createPrimaryKeyJob(schema))}', tablename);
250
+ EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJob(schema))}', tablename);
251
+ EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJobDeadLetter(schema))}', tablename);
252
+
253
+ EXECUTE format('${formatPartitionCommand(createIndexJobFetch(schema))}', tablename);
254
+ EXECUTE format('${formatPartitionCommand(createIndexJobThrottle(schema))}', tablename);
255
+
256
+ IF options->>'policy' = 'short' THEN
257
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicyShort(schema))}', tablename);
258
+ ELSIF options->>'policy' = 'singleton' THEN
259
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicySingleton(schema))}', tablename);
260
+ ELSIF options->>'policy' = 'stately' THEN
261
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicyStately(schema))}', tablename);
262
+ ELSIF options->>'policy' = 'exclusive' THEN
263
+ EXECUTE format('${formatPartitionCommand(createIndexJobPolicyExclusive(schema))}', tablename);
264
+ END IF;
265
+
266
+ EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
267
+ EXECUTE format('ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.%I FOR VALUES IN (%L)', tablename, queue_name);
268
+ END;
269
+ $$
270
+ LANGUAGE plpgsql;
271
+ `;
272
+ }
273
+ function formatPartitionCommand(command) {
274
+ return command.replace(".job", ".%1$I").replace("job_i", "%1$s_i").replaceAll("'", "''");
275
+ }
276
+ function deleteQueueFunction(schema) {
277
+ return `
278
+ CREATE FUNCTION ${schema}.delete_queue(queue_name text)
279
+ RETURNS VOID AS
280
+ $$
281
+ DECLARE
282
+ v_table varchar;
283
+ v_partition bool;
284
+ BEGIN
285
+ SELECT table_name, partition
286
+ FROM ${schema}.queue
287
+ WHERE name = queue_name
288
+ INTO v_table, v_partition;
289
+
290
+ IF v_partition THEN
291
+ EXECUTE format('DROP TABLE IF EXISTS ${schema}.%I', v_table);
292
+ ELSE
293
+ EXECUTE format('DELETE FROM ${schema}.%I WHERE name = %L', v_table, queue_name);
294
+ END IF;
295
+
296
+ DELETE FROM ${schema}.queue WHERE name = queue_name;
297
+ END;
298
+ $$
299
+ LANGUAGE plpgsql;
300
+ `;
301
+ }
302
+ function createQueue(schema, name, options) {
303
+ return locked(schema, `SELECT ${schema}.create_queue('${name}', '${JSON.stringify(options)}'::jsonb)`, "create-queue");
304
+ }
305
+ function deleteQueue(schema, name) {
306
+ return locked(schema, `SELECT ${schema}.delete_queue('${name}')`, "delete-queue");
307
+ }
308
+ function createPrimaryKeyJob(schema) {
309
+ return `ALTER TABLE ${schema}.job ADD PRIMARY KEY (name, id)`;
310
+ }
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`;
313
+ }
314
+ function createQueueForeignKeyJobDeadLetter(schema) {
315
+ return `ALTER TABLE ${schema}.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES ${schema}.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED`;
316
+ }
317
+ function createIndexJobPolicyShort(schema) {
318
+ return `CREATE UNIQUE INDEX job_i1 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.created}' AND policy = '${QUEUE_POLICIES.short}'`;
319
+ }
320
+ function createIndexJobPolicySingleton(schema) {
321
+ return `CREATE UNIQUE INDEX job_i2 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state = '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.singleton}'`;
322
+ }
323
+ function createIndexJobPolicyStately(schema) {
324
+ return `CREATE UNIQUE INDEX job_i3 ON ${schema}.job (name, state, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.stately}'`;
325
+ }
326
+ function createIndexJobThrottle(schema) {
327
+ 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`;
328
+ }
329
+ function createIndexJobFetch(schema) {
330
+ return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) INCLUDE (priority, created_on, id) WHERE state < '${JOB_STATES.active}'`;
331
+ }
332
+ function createIndexJobPolicyExclusive(schema) {
333
+ return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
334
+ }
335
+ function trySetQueueMonitorTime(schema, queues, seconds) {
336
+ return trySetQueueTimestamp(schema, queues, "monitor_on", seconds);
337
+ }
338
+ function trySetQueueDeletionTime(schema, queues, seconds) {
339
+ return trySetQueueTimestamp(schema, queues, "maintain_on", seconds);
340
+ }
341
+ function trySetCronTime(schema, seconds) {
342
+ return trySetTimestamp(schema, "cron_on", seconds);
343
+ }
344
+ function trySetTimestamp(schema, column, seconds) {
345
+ return `
346
+ UPDATE ${schema}.version
347
+ SET ${column} = now()
348
+ WHERE EXTRACT( EPOCH FROM (now() - COALESCE(${column}, now() - interval '1 week') ) ) > ${seconds}
349
+ RETURNING true
350
+ `;
351
+ }
352
+ function trySetQueueTimestamp(schema, queues, column, seconds) {
353
+ return `
354
+ UPDATE ${schema}.queue
355
+ SET ${column} = now()
356
+ WHERE name IN(${getQueueInClause(queues)})
357
+ AND EXTRACT( EPOCH FROM (now() - COALESCE(${column}, now() - interval '1 week') ) ) > ${seconds}
358
+ RETURNING name
359
+ `;
360
+ }
361
+ function updateQueue(schema, { deadLetter } = {}) {
362
+ return `
363
+ WITH options as (SELECT $2::jsonb as data)
364
+ UPDATE ${schema}.queue SET
365
+ retry_limit = COALESCE((o.data->>'retryLimit')::int, retry_limit),
366
+ retry_delay = COALESCE((o.data->>'retryDelay')::int, retry_delay),
367
+ retry_backoff = COALESCE((o.data->>'retryBackoff')::bool, retry_backoff),
368
+ retry_delay_max = CASE WHEN o.data ? 'retryDelayMax'
369
+ THEN (o.data->>'retryDelayMax')::int
370
+ ELSE retry_delay_max END,
371
+ expire_seconds = COALESCE((o.data->>'expireInSeconds')::int, expire_seconds),
372
+ retention_seconds = COALESCE((o.data->>'retentionSeconds')::int, retention_seconds),
373
+ deletion_seconds = COALESCE((o.data->>'deleteAfterSeconds')::int, deletion_seconds),
374
+ warning_queued = COALESCE((o.data->>'warningQueueSize')::int, warning_queued),
375
+ ${deadLetter === void 0 ? "" : `dead_letter = CASE WHEN '${deadLetter}' IS DISTINCT FROM dead_letter THEN '${deadLetter}' ELSE dead_letter END,`}
376
+ updated_on = now()
377
+ FROM options o
378
+ WHERE name = $1
379
+ `;
380
+ }
381
+ function getQueues(schema, names) {
382
+ return `
383
+ SELECT
384
+ q.name,
385
+ q.policy,
386
+ q.retry_limit as "retryLimit",
387
+ q.retry_delay as "retryDelay",
388
+ q.retry_backoff as "retryBackoff",
389
+ q.retry_delay_max as "retryDelayMax",
390
+ q.expire_seconds as "expireInSeconds",
391
+ q.retention_seconds as "retentionSeconds",
392
+ q.deletion_seconds as "deleteAfterSeconds",
393
+ q.partition,
394
+ q.dead_letter as "deadLetter",
395
+ q.deferred_count as "deferredCount",
396
+ q.warning_queued as "warningQueueSize",
397
+ q.queued_count as "queuedCount",
398
+ q.active_count as "activeCount",
399
+ q.total_count as "totalCount",
400
+ q.singletons_active as "singletonsActive",
401
+ q.table_name as "table",
402
+ q.created_on as "createdOn",
403
+ q.updated_on as "updatedOn"
404
+ FROM ${schema}.queue q
405
+ ${names ? `WHERE q.name IN (${names.map((i) => `'${i}'`)})` : ""}
406
+ `;
407
+ }
408
+ function deleteJobsById(schema, table) {
409
+ return `
410
+ WITH results as (
411
+ DELETE FROM ${schema}.${table}
412
+ WHERE name = $1
413
+ AND id IN (SELECT UNNEST($2::uuid[]))
414
+ RETURNING 1
415
+ )
416
+ SELECT COUNT(*) from results
417
+ `;
418
+ }
419
+ function deleteQueuedJobs(schema, table) {
420
+ return `DELETE from ${schema}.${table} WHERE name = $1 and state < '${JOB_STATES.active}'`;
421
+ }
422
+ function deleteStoredJobs(schema, table) {
423
+ return `DELETE from ${schema}.${table} WHERE name = $1 and state > '${JOB_STATES.active}'`;
424
+ }
425
+ function truncateTable(schema, table) {
426
+ return `TRUNCATE ${schema}.${table}`;
427
+ }
428
+ function deleteAllJobs(schema, table) {
429
+ return `DELETE from ${schema}.${table} WHERE name = $1`;
430
+ }
431
+ function getSchedules(schema) {
432
+ return `SELECT * FROM ${schema}.schedule`;
433
+ }
434
+ function getSchedulesByQueue(schema) {
435
+ return `SELECT * FROM ${schema}.schedule WHERE name = $1 AND COALESCE(key, '') = $2`;
436
+ }
437
+ function schedule(schema) {
438
+ return `
439
+ INSERT INTO ${schema}.schedule (name, key, cron, timezone, data, options)
440
+ VALUES ($1, $2, $3, $4, $5, $6)
441
+ ON CONFLICT (name, key) DO UPDATE SET
442
+ cron = EXCLUDED.cron,
443
+ timezone = EXCLUDED.timezone,
444
+ data = EXCLUDED.data,
445
+ options = EXCLUDED.options,
446
+ updated_on = now()
447
+ `;
448
+ }
449
+ function unschedule(schema) {
450
+ return `
451
+ DELETE FROM ${schema}.schedule
452
+ WHERE name = $1
453
+ AND COALESCE(key, '') = $2
454
+ `;
455
+ }
456
+ function subscribe(schema) {
457
+ return `
458
+ INSERT INTO ${schema}.subscription (event, name)
459
+ VALUES ($1, $2)
460
+ ON CONFLICT (event, name) DO UPDATE SET
461
+ event = EXCLUDED.event,
462
+ name = EXCLUDED.name,
463
+ updated_on = now()
464
+ `;
465
+ }
466
+ function unsubscribe(schema) {
467
+ return `
468
+ DELETE FROM ${schema}.subscription
469
+ WHERE event = $1 and name = $2
470
+ `;
471
+ }
472
+ function getQueuesForEvent(schema) {
473
+ return `
474
+ SELECT name FROM ${schema}.subscription
475
+ WHERE event = $1
476
+ `;
477
+ }
478
+ function getTime() {
479
+ return "SELECT round(date_part('epoch', now()) * 1000) as time";
480
+ }
481
+ function getVersion(schema) {
482
+ return `SELECT version from ${schema}.version`;
483
+ }
484
+ function setVersion(schema, version) {
485
+ return `UPDATE ${schema}.version SET version = '${version}'`;
486
+ }
487
+ function versionTableExists(schema) {
488
+ return `SELECT to_regclass('${schema}.version') as name`;
489
+ }
490
+ function insertVersion(schema, version) {
491
+ return `INSERT INTO ${schema}.version(version) VALUES ('${version}')`;
492
+ }
493
+ function fetchNextJob({ schema, table, name, policy, limit, includeMetadata, priority = true, ignoreStartAfter = false, ignoreSingletons = null }) {
494
+ const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
495
+ const cte = singletonFetch ? "grouped" : "next";
496
+ return `
497
+ WITH next as (
498
+ SELECT id ${singletonFetch ? ", singleton_key" : ""}
499
+ FROM ${schema}.${table}
500
+ WHERE name = '${name}'
501
+ AND state < '${JOB_STATES.active}'
502
+ ${ignoreStartAfter ? "" : "AND start_after < now()"}
503
+ ${ignoreSingletons != null && ignoreSingletons?.length > 0 ? `AND singleton_key NOT IN (${ignoreSingletons.map((i) => `'${i}'`).join()})` : ""}
504
+ ORDER BY ${priority ? "priority desc, " : ""}created_on, id
505
+ LIMIT ${limit}
506
+ FOR UPDATE SKIP LOCKED
507
+ )
508
+ ${singletonFetch ? ", grouped as ( SELECT id, row_number() OVER (PARTITION BY singleton_key) FROM next)" : ""}
509
+ UPDATE ${schema}.${table} j SET
510
+ state = '${JOB_STATES.active}',
511
+ started_on = now(),
512
+ retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
513
+ FROM ${cte}
514
+ WHERE name = '${name}' AND j.id = ${cte}.id
515
+ ${singletonFetch ? ` AND ${cte}.row_number = 1` : ""}
516
+ RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
517
+ `;
518
+ }
519
+ function completeJobs(schema, table) {
520
+ return `
521
+ WITH results AS (
522
+ UPDATE ${schema}.${table}
523
+ SET completed_on = now(),
524
+ state = '${JOB_STATES.completed}',
525
+ output = $3::jsonb
526
+ WHERE name = $1
527
+ AND id IN (SELECT UNNEST($2::uuid[]))
528
+ AND state = '${JOB_STATES.active}'
529
+ RETURNING *
530
+ )
531
+ SELECT COUNT(*) FROM results
532
+ `;
533
+ }
534
+ function cancelJobs(schema, table) {
535
+ return `
536
+ WITH results as (
537
+ UPDATE ${schema}.${table}
538
+ SET completed_on = now(),
539
+ state = '${JOB_STATES.cancelled}'
540
+ WHERE name = $1
541
+ AND id IN (SELECT UNNEST($2::uuid[]))
542
+ AND state < '${JOB_STATES.completed}'
543
+ RETURNING 1
544
+ )
545
+ SELECT COUNT(*) from results
546
+ `;
547
+ }
548
+ function resumeJobs(schema, table) {
549
+ return `
550
+ WITH results as (
551
+ UPDATE ${schema}.${table}
552
+ SET completed_on = NULL,
553
+ state = '${JOB_STATES.created}'
554
+ WHERE name = $1
555
+ AND id IN (SELECT UNNEST($2::uuid[]))
556
+ AND state = '${JOB_STATES.cancelled}'
557
+ RETURNING 1
558
+ )
559
+ SELECT COUNT(*) from results
560
+ `;
561
+ }
562
+ function insertJobs(schema, { table, name, returnId = true }) {
563
+ return `
564
+ INSERT INTO ${schema}.${table} (
565
+ id,
566
+ name,
567
+ data,
568
+ priority,
569
+ start_after,
570
+ singleton_key,
571
+ singleton_on,
572
+ expire_seconds,
573
+ deletion_seconds,
574
+ keep_until,
575
+ retry_limit,
576
+ retry_delay,
577
+ retry_backoff,
578
+ retry_delay_max,
579
+ policy,
580
+ dead_letter
581
+ )
582
+ SELECT
583
+ COALESCE(id, gen_random_uuid()) as id,
584
+ '${name}' as name,
585
+ data,
586
+ COALESCE(priority, 0) as priority,
587
+ j.start_after,
588
+ "singletonKey",
589
+ CASE
590
+ WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds" * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)) / "singletonSeconds" ))
591
+ ELSE NULL
592
+ END as singleton_on,
593
+ COALESCE("expireInSeconds", q.expire_seconds) as expire_seconds,
594
+ COALESCE("deleteAfterSeconds", q.deletion_seconds) as deletion_seconds,
595
+ j.start_after + (COALESCE("retentionSeconds", q.retention_seconds) * interval '1s') as keep_until,
596
+ COALESCE("retryLimit", q.retry_limit) as retry_limit,
597
+ COALESCE("retryDelay", q.retry_delay) as retry_delay,
598
+ COALESCE("retryBackoff", q.retry_backoff, false) as retry_backoff,
599
+ COALESCE("retryDelayMax", q.retry_delay_max) as retry_delay_max,
600
+ q.policy,
601
+ q.dead_letter
602
+ FROM (
603
+ SELECT *,
604
+ CASE
605
+ WHEN right("startAfter", 1) = 'Z' THEN CAST("startAfter" as timestamp with time zone)
606
+ ELSE now() + CAST(COALESCE("startAfter",'0') as interval)
607
+ END as start_after
608
+ FROM json_to_recordset($1::json) as x (
609
+ id uuid,
610
+ priority integer,
611
+ data jsonb,
612
+ "startAfter" text,
613
+ "retryLimit" integer,
614
+ "retryDelay" integer,
615
+ "retryDelayMax" integer,
616
+ "retryBackoff" boolean,
617
+ "singletonKey" text,
618
+ "singletonSeconds" integer,
619
+ "singletonOffset" integer,
620
+ "expireInSeconds" integer,
621
+ "deleteAfterSeconds" integer,
622
+ "retentionSeconds" integer
623
+ )
624
+ ) j
625
+ JOIN ${schema}.queue q ON q.name = '${name}'
626
+ ON CONFLICT DO NOTHING
627
+ ${returnId ? "RETURNING id" : ""}
628
+ `;
629
+ }
630
+ function failJobsById(schema, table) {
631
+ return failJobs(schema, table, `name = $1 AND id IN (SELECT UNNEST($2::uuid[])) AND state < '${JOB_STATES.completed}'`, "$3::jsonb");
632
+ }
633
+ function failJobsByTimeout(schema, table, queues) {
634
+ return locked(schema, failJobs(schema, table, `state = '${JOB_STATES.active}'
635
+ AND (started_on + expire_seconds * interval '1s') < now()
636
+ AND name IN (${getQueueInClause(queues)})`, "'{ \"value\": { \"message\": \"job timed out\" } }'::jsonb"), table + "failJobsByTimeout");
637
+ }
638
+ function failJobs(schema, table, where, output) {
639
+ return `
640
+ WITH deleted_jobs AS (
641
+ DELETE FROM ${schema}.${table}
642
+ WHERE ${where}
643
+ RETURNING *
644
+ ),
645
+ retried_jobs AS (
646
+ INSERT INTO ${schema}.${table} (
647
+ id,
648
+ name,
649
+ priority,
650
+ data,
651
+ state,
652
+ retry_limit,
653
+ retry_count,
654
+ retry_delay,
655
+ retry_backoff,
656
+ retry_delay_max,
657
+ start_after,
658
+ started_on,
659
+ singleton_key,
660
+ singleton_on,
661
+ expire_seconds,
662
+ deletion_seconds,
663
+ created_on,
664
+ completed_on,
665
+ keep_until,
666
+ policy,
667
+ output,
668
+ dead_letter
669
+ )
670
+ SELECT
671
+ id,
672
+ name,
673
+ priority,
674
+ data,
675
+ CASE
676
+ WHEN retry_count < retry_limit THEN '${JOB_STATES.retry}'::${schema}.job_state
677
+ ELSE '${JOB_STATES.failed}'::${schema}.job_state
678
+ END as state,
679
+ retry_limit,
680
+ retry_count,
681
+ retry_delay,
682
+ retry_backoff,
683
+ retry_delay_max,
684
+ CASE WHEN retry_count = retry_limit THEN start_after
685
+ WHEN NOT retry_backoff THEN now() + retry_delay * interval '1'
686
+ ELSE now() + LEAST(
687
+ retry_delay_max,
688
+ retry_delay + (
689
+ 2 ^ LEAST(16, retry_count + 1) / 2 +
690
+ 2 ^ LEAST(16, retry_count + 1) / 2 * random()
691
+ )
692
+ ) * interval '1s'
693
+ END as start_after,
694
+ started_on,
695
+ singleton_key,
696
+ singleton_on,
697
+ expire_seconds,
698
+ deletion_seconds,
699
+ created_on,
700
+ CASE WHEN retry_count < retry_limit THEN NULL ELSE now() END as completed_on,
701
+ keep_until,
702
+ policy,
703
+ ${output},
704
+ dead_letter
705
+ FROM deleted_jobs
706
+ ON CONFLICT DO NOTHING
707
+ RETURNING *
708
+ ),
709
+ failed_jobs as (
710
+ INSERT INTO ${schema}.${table} (
711
+ id,
712
+ name,
713
+ priority,
714
+ data,
715
+ state,
716
+ retry_limit,
717
+ retry_count,
718
+ retry_delay,
719
+ retry_backoff,
720
+ retry_delay_max,
721
+ start_after,
722
+ started_on,
723
+ singleton_key,
724
+ singleton_on,
725
+ expire_seconds,
726
+ deletion_seconds,
727
+ created_on,
728
+ completed_on,
729
+ keep_until,
730
+ policy,
731
+ output,
732
+ dead_letter
733
+ )
734
+ SELECT
735
+ id,
736
+ name,
737
+ priority,
738
+ data,
739
+ '${JOB_STATES.failed}'::${schema}.job_state as state,
740
+ retry_limit,
741
+ retry_count,
742
+ retry_delay,
743
+ retry_backoff,
744
+ retry_delay_max,
745
+ start_after,
746
+ started_on,
747
+ singleton_key,
748
+ singleton_on,
749
+ expire_seconds,
750
+ deletion_seconds,
751
+ created_on,
752
+ now() as completed_on,
753
+ keep_until,
754
+ policy,
755
+ ${output},
756
+ dead_letter
757
+ FROM deleted_jobs
758
+ WHERE id NOT IN (SELECT id from retried_jobs)
759
+ RETURNING *
760
+ ),
761
+ results as (
762
+ SELECT * FROM retried_jobs
763
+ UNION ALL
764
+ SELECT * FROM failed_jobs
765
+ ),
766
+ dlq_jobs as (
767
+ INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds)
768
+ SELECT
769
+ r.dead_letter,
770
+ data,
771
+ output,
772
+ q.retry_limit,
773
+ q.retry_backoff,
774
+ q.retry_delay,
775
+ now() + q.retention_seconds * interval '1s',
776
+ q.deletion_seconds
777
+ FROM results r
778
+ JOIN ${schema}.queue q ON q.name = r.dead_letter
779
+ WHERE state = '${JOB_STATES.failed}'
780
+ )
781
+ SELECT COUNT(*) FROM results
782
+ `;
783
+ }
784
+ function deletion(schema, table, queues) {
785
+ return locked(schema, `
786
+ DELETE FROM ${schema}.${table}
787
+ WHERE name IN (${getQueueInClause(queues)})
788
+ AND
789
+ (
790
+ completed_on + deletion_seconds * interval '1s' < now()
791
+ OR
792
+ (state < '${JOB_STATES.active}' AND keep_until < now())
793
+ )
794
+ `, table + "deletion");
795
+ }
796
+ function retryJobs(schema, table) {
797
+ return `
798
+ WITH results as (
799
+ UPDATE ${schema}.job
800
+ SET state = '${JOB_STATES.retry}',
801
+ retry_limit = retry_limit + 1
802
+ WHERE name = $1
803
+ AND id IN (SELECT UNNEST($2::uuid[]))
804
+ AND state = '${JOB_STATES.failed}'
805
+ RETURNING 1
806
+ )
807
+ SELECT COUNT(*) from results
808
+ `;
809
+ }
810
+ function getQueueStats(schema, table, queues) {
811
+ return `
812
+ SELECT
813
+ name,
814
+ (count(*) FILTER (WHERE start_after > now()))::int as "deferredCount",
815
+ (count(*) FILTER (WHERE state < '${JOB_STATES.active}'))::int as "queuedCount",
816
+ (count(*) FILTER (WHERE state = '${JOB_STATES.active}'))::int as "activeCount",
817
+ count(*)::int as "totalCount",
818
+ array_agg(singleton_key) FILTER (WHERE policy IN ('${QUEUE_POLICIES.singleton}','${QUEUE_POLICIES.stately}') AND state = '${JOB_STATES.active}') as "singletonsActive"
819
+ FROM ${schema}.${table}
820
+ WHERE name IN (${getQueueInClause(queues)})
821
+ GROUP BY 1
822
+ `;
823
+ }
824
+ function cacheQueueStats(schema, table, queues) {
825
+ return locked(schema, `
826
+ WITH stats AS (${getQueueStats(schema, table, queues)})
827
+ UPDATE ${schema}.queue SET
828
+ deferred_count = "deferredCount",
829
+ queued_count = "queuedCount",
830
+ active_count = "activeCount",
831
+ total_count = "totalCount",
832
+ singletons_active = "singletonsActive"
833
+ FROM stats
834
+ WHERE queue.name = stats.name
835
+ RETURNING
836
+ queue.name,
837
+ "queuedCount",
838
+ warning_queued as "warningQueueSize"
839
+ `, "queue-stats");
840
+ }
841
+ function locked(schema, query, key) {
842
+ if (Array.isArray(query)) query = query.join(";\n");
843
+ return `
844
+ BEGIN;
845
+ SET LOCAL lock_timeout = 30000;
846
+ SET LOCAL idle_in_transaction_session_timeout = 30000;
847
+ ${advisoryLock(schema, key)};
848
+ ${query};
849
+ COMMIT;
850
+ `;
851
+ }
852
+ function advisoryLock(schema, key) {
853
+ return `SELECT pg_advisory_xact_lock(
854
+ ('x' || encode(sha224((current_database() || '.pgboss.${schema}${key || ""}')::bytea), 'hex'))::bit(64)::bigint
855
+ )`;
856
+ }
857
+ function assertMigration(schema, version) {
858
+ return `SELECT version::int/(version::int-${version}) from ${schema}.version`;
859
+ }
860
+ function getJobById(schema, table) {
861
+ return `SELECT ${JOB_COLUMNS_ALL} FROM ${schema}.${table} WHERE name = $1 AND id = $2`;
862
+ }
863
+ function getQueueInClause(queues) {
864
+ return queues.map((i) => `'${i}'`).join(",");
865
+ }
866
+
867
+ //#endregion
868
+ export { CREATE_RACE_MESSAGE, DEFAULT_SCHEMA, JOB_STATES, MIGRATE_RACE_MESSAGE, QUEUE_POLICIES, assertMigration, cacheQueueStats, cancelJobs, completeJobs, create, createQueue, deleteAllJobs, deleteJobsById, deleteQueue, deleteQueuedJobs, deleteStoredJobs, deletion, failJobsById, failJobsByTimeout, fetchNextJob, getJobById, getQueueStats, getQueues, getQueuesForEvent, getSchedules, getSchedulesByQueue, getTime, getVersion, insertJobs, locked, resumeJobs, retryJobs, schedule, setVersion, subscribe, truncateTable, trySetCronTime, trySetQueueDeletionTime, trySetQueueMonitorTime, unschedule, unsubscribe, updateQueue, versionTableExists };