pg-boss 12.5.4 → 12.7.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.
package/dist/plans.js CHANGED
@@ -39,9 +39,13 @@ function create(schema, version, options) {
39
39
  createTableQueue(schema),
40
40
  createTableSchedule(schema),
41
41
  createTableSubscription(schema),
42
+ createTableBam(schema),
43
+ jobTableFormatFunction(schema),
44
+ jobTableRunFunction(schema),
45
+ jobTableRunAsyncFunction(schema),
42
46
  createTableJob(schema),
43
47
  createPrimaryKeyJob(schema),
44
- createTableJobCommon(schema, COMMON_JOB_TABLE),
48
+ createTableJobCommon(schema),
45
49
  createQueueFunction(schema),
46
50
  deleteQueueFunction(schema),
47
51
  insertVersion(schema, version)
@@ -69,7 +73,8 @@ function createTableVersion(schema) {
69
73
  return `
70
74
  CREATE TABLE ${schema}.version (
71
75
  version int primary key,
72
- cron_on timestamp with time zone
76
+ cron_on timestamp with time zone,
77
+ bam_on timestamp with time zone
73
78
  )
74
79
  `;
75
80
  }
@@ -128,6 +133,113 @@ function createTableSubscription(schema) {
128
133
  )
129
134
  `;
130
135
  }
136
+ function createTableBam(schema) {
137
+ return `
138
+ CREATE TABLE ${schema}.bam (
139
+ id uuid PRIMARY KEY default gen_random_uuid(),
140
+ name text NOT NULL,
141
+ version int NOT NULL,
142
+ status text NOT NULL DEFAULT 'pending',
143
+ queue text,
144
+ table_name text NOT NULL,
145
+ command text NOT NULL,
146
+ error text,
147
+ created_on timestamp with time zone NOT NULL DEFAULT now(),
148
+ started_on timestamp with time zone,
149
+ completed_on timestamp with time zone
150
+ )
151
+ `;
152
+ }
153
+ function jobTableFormatFunction(schema) {
154
+ return `
155
+ CREATE FUNCTION ${schema}.job_table_format(command text, table_name text)
156
+ RETURNS text AS
157
+ $$
158
+ SELECT format(
159
+ replace(
160
+ replace(command, '.job', '.%1$I'),
161
+ 'job_i', '%1$s_i'
162
+ ),
163
+ table_name
164
+ );
165
+ $$
166
+ LANGUAGE sql IMMUTABLE;
167
+ `;
168
+ }
169
+ function jobTableRunFunction(schema) {
170
+ return `
171
+ CREATE FUNCTION ${schema}.job_table_run(command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
172
+ RETURNS VOID AS
173
+ $$
174
+ DECLARE
175
+ tbl RECORD;
176
+ BEGIN
177
+ IF queue_name IS NOT NULL THEN
178
+ SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
179
+ END IF;
180
+
181
+ IF tbl_name IS NOT NULL THEN
182
+ EXECUTE ${schema}.job_table_format(command, tbl_name);
183
+ RETURN;
184
+ END IF;
185
+
186
+ EXECUTE ${schema}.job_table_format(command, '${COMMON_JOB_TABLE}');
187
+
188
+ FOR tbl IN SELECT table_name FROM ${schema}.queue WHERE partition = true
189
+ LOOP
190
+ EXECUTE ${schema}.job_table_format(command, tbl.table_name);
191
+ END LOOP;
192
+ END;
193
+ $$
194
+ LANGUAGE plpgsql;
195
+ `;
196
+ }
197
+ function jobTableRunAsyncFunction(schema) {
198
+ return `
199
+ CREATE FUNCTION ${schema}.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
200
+ RETURNS VOID AS
201
+ $$
202
+ BEGIN
203
+ IF queue_name IS NOT NULL THEN
204
+ SELECT table_name INTO tbl_name FROM ${schema}.queue WHERE name = queue_name;
205
+ END IF;
206
+
207
+ IF tbl_name IS NOT NULL THEN
208
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
209
+ VALUES (
210
+ command_name,
211
+ version,
212
+ 'pending',
213
+ queue_name,
214
+ tbl_name,
215
+ ${schema}.job_table_format(command, tbl_name)
216
+ );
217
+ RETURN;
218
+ END IF;
219
+
220
+ INSERT INTO ${schema}.bam (name, version, status, queue, table_name, command)
221
+ SELECT
222
+ command_name,
223
+ version,
224
+ 'pending',
225
+ NULL,
226
+ '${COMMON_JOB_TABLE}',
227
+ ${schema}.job_table_format(command, '${COMMON_JOB_TABLE}')
228
+ UNION ALL
229
+ SELECT
230
+ command_name,
231
+ version,
232
+ 'pending',
233
+ queue.name,
234
+ queue.table_name,
235
+ ${schema}.job_table_format(command, queue.table_name)
236
+ FROM ${schema}.queue
237
+ WHERE partition = true;
238
+ END;
239
+ $$
240
+ LANGUAGE plpgsql;
241
+ `;
242
+ }
131
243
  function createTableJob(schema) {
132
244
  return `
133
245
  CREATE TABLE ${schema}.job (
@@ -145,6 +257,8 @@ function createTableJob(schema) {
145
257
  deletion_seconds int not null default ${QUEUE_DEFAULTS.deletion_seconds},
146
258
  singleton_key text,
147
259
  singleton_on timestamp without time zone,
260
+ group_id text,
261
+ group_tier text,
148
262
  start_after timestamp with time zone not null default now(),
149
263
  created_on timestamp with time zone not null default now(),
150
264
  started_on timestamp with time zone,
@@ -156,7 +270,7 @@ function createTableJob(schema) {
156
270
  ) PARTITION BY LIST (name)
157
271
  `;
158
272
  }
159
- const JOB_COLUMNS_MIN = 'id, name, data, expire_seconds as "expireInSeconds"';
273
+ const JOB_COLUMNS_MIN = 'id, name, data, expire_seconds as "expireInSeconds", group_id as "groupId", group_tier as "groupTier"';
160
274
  const JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
161
275
  policy,
162
276
  state,
@@ -177,21 +291,22 @@ const JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
177
291
  dead_letter as "deadLetter",
178
292
  output
179
293
  `;
180
- function createTableJobCommon(schema, table) {
181
- const format = (command) => command.replaceAll('.job', `.${table}`) + ';';
182
- return `
183
- CREATE TABLE ${schema}.${table} (LIKE ${schema}.job INCLUDING GENERATED INCLUDING DEFAULTS);
184
- ${format(createPrimaryKeyJob(schema))}
185
- ${format(createQueueForeignKeyJob(schema))}
186
- ${format(createQueueForeignKeyJobDeadLetter(schema))}
187
- ${format(createIndexJobPolicyShort(schema))}
188
- ${format(createIndexJobPolicySingleton(schema))}
189
- ${format(createIndexJobPolicyStately(schema))}
190
- ${format(createIndexJobPolicyExclusive(schema))}
191
- ${format(createIndexJobThrottle(schema))}
192
- ${format(createIndexJobFetch(schema))}
294
+ function createTableJobCommon(schema) {
295
+ return `
296
+ CREATE TABLE ${schema}.${COMMON_JOB_TABLE} (LIKE ${schema}.job INCLUDING GENERATED INCLUDING DEFAULTS);
193
297
 
194
- ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${table} DEFAULT;
298
+ SELECT ${schema}.job_table_run($cmd$${createPrimaryKeyJob(schema)}$cmd$, '${COMMON_JOB_TABLE}');
299
+ SELECT ${schema}.job_table_run($cmd$${createQueueForeignKeyJob(schema)}$cmd$, '${COMMON_JOB_TABLE}');
300
+ SELECT ${schema}.job_table_run($cmd$${createQueueForeignKeyJobDeadLetter(schema)}$cmd$, '${COMMON_JOB_TABLE}');
301
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobPolicyShort(schema)}$cmd$, '${COMMON_JOB_TABLE}');
302
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobPolicySingleton(schema)}$cmd$, '${COMMON_JOB_TABLE}');
303
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobPolicyStately(schema)}$cmd$, '${COMMON_JOB_TABLE}');
304
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobPolicyExclusive(schema)}$cmd$, '${COMMON_JOB_TABLE}');
305
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobThrottle(schema)}$cmd$, '${COMMON_JOB_TABLE}');
306
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobFetch(schema)}$cmd$, '${COMMON_JOB_TABLE}');
307
+ SELECT ${schema}.job_table_run($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, '${COMMON_JOB_TABLE}');
308
+
309
+ ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
195
310
  `;
196
311
  }
197
312
  function createQueueFunction(schema) {
@@ -248,22 +363,23 @@ function createQueueFunction(schema) {
248
363
  END IF;
249
364
 
250
365
  EXECUTE format('CREATE TABLE ${schema}.%I (LIKE ${schema}.job INCLUDING DEFAULTS)', tablename);
251
-
252
- EXECUTE format('${formatPartitionCommand(createPrimaryKeyJob(schema))}', tablename);
253
- EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJob(schema))}', tablename);
254
- EXECUTE format('${formatPartitionCommand(createQueueForeignKeyJobDeadLetter(schema))}', tablename);
255
366
 
256
- EXECUTE format('${formatPartitionCommand(createIndexJobFetch(schema))}', tablename);
257
- EXECUTE format('${formatPartitionCommand(createIndexJobThrottle(schema))}', tablename);
258
-
367
+ EXECUTE ${schema}.job_table_format($cmd$${createPrimaryKeyJob(schema)}$cmd$, tablename);
368
+ EXECUTE ${schema}.job_table_format($cmd$${createQueueForeignKeyJob(schema)}$cmd$, tablename);
369
+ EXECUTE ${schema}.job_table_format($cmd$${createQueueForeignKeyJobDeadLetter(schema)}$cmd$, tablename);
370
+
371
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobFetch(schema)}$cmd$, tablename);
372
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobThrottle(schema)}$cmd$, tablename);
373
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, tablename);
374
+
259
375
  IF options->>'policy' = 'short' THEN
260
- EXECUTE format('${formatPartitionCommand(createIndexJobPolicyShort(schema))}', tablename);
376
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyShort(schema)}$cmd$, tablename);
261
377
  ELSIF options->>'policy' = 'singleton' THEN
262
- EXECUTE format('${formatPartitionCommand(createIndexJobPolicySingleton(schema))}', tablename);
378
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicySingleton(schema)}$cmd$, tablename);
263
379
  ELSIF options->>'policy' = 'stately' THEN
264
- EXECUTE format('${formatPartitionCommand(createIndexJobPolicyStately(schema))}', tablename);
380
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyStately(schema)}$cmd$, tablename);
265
381
  ELSIF options->>'policy' = 'exclusive' THEN
266
- EXECUTE format('${formatPartitionCommand(createIndexJobPolicyExclusive(schema))}', tablename);
382
+ EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyExclusive(schema)}$cmd$, tablename);
267
383
  END IF;
268
384
 
269
385
  EXECUTE format('ALTER TABLE ${schema}.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
@@ -273,12 +389,6 @@ function createQueueFunction(schema) {
273
389
  LANGUAGE plpgsql;
274
390
  `;
275
391
  }
276
- function formatPartitionCommand(command) {
277
- return command
278
- .replace('.job', '.%1$I')
279
- .replace('job_i', '%1$s_i')
280
- .replaceAll("'", "''");
281
- }
282
392
  function deleteQueueFunction(schema) {
283
393
  return `
284
394
  CREATE FUNCTION ${schema}.delete_queue(queue_name text)
@@ -340,6 +450,9 @@ function createIndexJobFetch(schema) {
340
450
  function createIndexJobPolicyExclusive(schema) {
341
451
  return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
342
452
  }
453
+ function createIndexJobGroupConcurrency(schema) {
454
+ return `CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = '${JOB_STATES.active}' AND group_id IS NOT NULL`;
455
+ }
343
456
  function trySetQueueMonitorTime(schema, queues, seconds) {
344
457
  return trySetQueueTimestamp(schema, queues, 'monitor_on', seconds);
345
458
  }
@@ -349,6 +462,9 @@ function trySetQueueDeletionTime(schema, queues, seconds) {
349
462
  function trySetCronTime(schema, seconds) {
350
463
  return trySetTimestamp(schema, 'cron_on', seconds);
351
464
  }
465
+ function trySetBamTime(schema, seconds) {
466
+ return trySetTimestamp(schema, 'bam_on', seconds);
467
+ }
352
468
  function trySetTimestamp(schema, column, seconds) {
353
469
  return `
354
470
  UPDATE ${schema}.version
@@ -507,34 +623,133 @@ function versionTableExists(schema) {
507
623
  function insertVersion(schema, version) {
508
624
  return `INSERT INTO ${schema}.version(version) VALUES ('${version}')`;
509
625
  }
510
- function fetchNextJob({ schema, table, name, policy, limit, includeMetadata, priority = true, ignoreStartAfter = false, ignoreSingletons = null }) {
511
- const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
512
- const cte = singletonFetch ? 'grouped' : 'next';
626
+ function buildFetchParams(options) {
627
+ const { ignoreSingletons, ignoreGroups, groupConcurrency } = options;
513
628
  const hasIgnoreSingletons = ignoreSingletons != null && ignoreSingletons.length > 0;
629
+ const hasIgnoreGroups = ignoreGroups != null && ignoreGroups.length > 0;
630
+ const hasGroupConcurrency = groupConcurrency != null;
631
+ const groupConcurrencyConfig = hasGroupConcurrency
632
+ ? (typeof groupConcurrency === 'number' ? { default: groupConcurrency } : groupConcurrency)
633
+ : null;
634
+ const hasTiers = groupConcurrencyConfig?.tiers && Object.keys(groupConcurrencyConfig.tiers).length > 0;
635
+ const values = [];
636
+ let paramIndex = 0;
637
+ let ignoreSingletonsParam = '';
638
+ let ignoreGroupsParam = '';
639
+ let defaultGroupLimitParam = '';
640
+ let tiersParam = '';
641
+ if (hasIgnoreSingletons) {
642
+ paramIndex++;
643
+ ignoreSingletonsParam = `$${paramIndex}::text[]`;
644
+ values.push(ignoreSingletons);
645
+ }
646
+ if (hasIgnoreGroups) {
647
+ paramIndex++;
648
+ ignoreGroupsParam = `$${paramIndex}::text[]`;
649
+ values.push(ignoreGroups);
650
+ }
651
+ if (hasGroupConcurrency && groupConcurrencyConfig) {
652
+ paramIndex++;
653
+ defaultGroupLimitParam = `$${paramIndex}::int`;
654
+ values.push(groupConcurrencyConfig.default);
655
+ if (hasTiers) {
656
+ paramIndex++;
657
+ tiersParam = `$${paramIndex}::jsonb`;
658
+ values.push(JSON.stringify(groupConcurrencyConfig.tiers));
659
+ }
660
+ }
661
+ return { values, ignoreSingletonsParam, ignoreGroupsParam, defaultGroupLimitParam, tiersParam };
662
+ }
663
+ function fetchNextJob(options) {
664
+ const { schema, table, name, policy, limit, includeMetadata, priority = true, orderByCreatedOn = true, ignoreStartAfter = false, groupConcurrency } = options;
665
+ const singletonFetch = limit > 1 && (policy === QUEUE_POLICIES.singleton || policy === QUEUE_POLICIES.stately);
666
+ const hasIgnoreSingletons = options.ignoreSingletons != null && options.ignoreSingletons.length > 0;
667
+ const hasIgnoreGroups = options.ignoreGroups != null && options.ignoreGroups.length > 0;
668
+ const hasGroupConcurrency = groupConcurrency != null;
669
+ const hasTiers = hasGroupConcurrency &&
670
+ typeof groupConcurrency === 'object' &&
671
+ groupConcurrency.tiers &&
672
+ Object.keys(groupConcurrency.tiers).length > 0;
673
+ const params = buildFetchParams(options);
674
+ const whereConditions = [
675
+ `name = '${name}'`,
676
+ `state < '${JOB_STATES.active}'`,
677
+ !ignoreStartAfter ? 'start_after < now()' : '',
678
+ hasIgnoreSingletons ? `singleton_key <> ALL(${params.ignoreSingletonsParam})` : '',
679
+ hasIgnoreGroups ? `(group_id IS NULL OR group_id <> ALL(${params.ignoreGroupsParam}))` : ''
680
+ ].filter(Boolean).join(' AND ');
681
+ const selectCols = [
682
+ 'id',
683
+ singletonFetch ? 'singleton_key' : '',
684
+ hasGroupConcurrency ? 'group_id, group_tier' : ''
685
+ ].filter(Boolean).join(', ');
686
+ const activeGroupCountsCte = hasGroupConcurrency
687
+ ? `active_group_counts AS (
688
+ SELECT group_id, COUNT(*)::int as active_cnt
689
+ FROM ${schema}.${table}
690
+ WHERE name = '${name}' AND state = '${JOB_STATES.active}' AND group_id IS NOT NULL
691
+ GROUP BY group_id
692
+ ), `
693
+ : '';
694
+ const nextCte = `
695
+ next AS (
696
+ SELECT ${selectCols}
697
+ FROM ${schema}.${table}
698
+ WHERE ${whereConditions}
699
+ ORDER BY ${priority ? 'priority desc, ' : ''}${orderByCreatedOn ? 'created_on, ' : ''}id
700
+ LIMIT ${limit}
701
+ FOR UPDATE SKIP LOCKED
702
+ )`;
703
+ const singletonCte = singletonFetch
704
+ ? `, singleton_ranking AS (
705
+ SELECT id, ${hasGroupConcurrency ? 'group_id, group_tier, ' : ''}
706
+ row_number() OVER (PARTITION BY singleton_key) as singleton_rn
707
+ FROM next
708
+ )`
709
+ : '';
710
+ const groupConcurrencyCtes = hasGroupConcurrency
711
+ ? `,
712
+ group_ranking AS (
713
+ SELECT t.id
714
+ , t.group_id
715
+ , t.group_tier
716
+ ${singletonFetch ? ', singleton_rn' : ''}
717
+ , ROW_NUMBER() OVER (PARTITION BY t.group_id ORDER BY t.id) as group_rn
718
+ , COALESCE(agc.active_cnt, 0) as active_cnt
719
+ FROM ${singletonFetch ? 'singleton_ranking' : 'next'} t
720
+ LEFT JOIN active_group_counts agc ON t.group_id = agc.group_id
721
+ ${singletonFetch ? 'WHERE singleton_rn = 1' : ''}
722
+ ),
723
+ group_filtered AS (
724
+ SELECT id FROM group_ranking
725
+ WHERE group_id IS NULL
726
+ OR (active_cnt + group_rn) <= ${hasTiers
727
+ ? `COALESCE((${params.tiersParam} ->> group_tier)::int, ${params.defaultGroupLimitParam})`
728
+ : params.defaultGroupLimitParam}
729
+ )`
730
+ : '';
731
+ const finalCte = (hasGroupConcurrency)
732
+ ? 'group_filtered'
733
+ : (singletonFetch)
734
+ ? 'singleton_ranking'
735
+ : 'next';
514
736
  return {
515
737
  text: `
516
- WITH next as (
517
- SELECT id ${singletonFetch ? ', singleton_key' : ''}
518
- FROM ${schema}.${table}
519
- WHERE name = '${name}'
520
- AND state < '${JOB_STATES.active}'
521
- ${ignoreStartAfter ? '' : 'AND start_after < now()'}
522
- ${hasIgnoreSingletons ? 'AND singleton_key <> ALL($1::text[])' : ''}
523
- ORDER BY ${priority ? 'priority desc, ' : ''}created_on, id
524
- LIMIT ${limit}
525
- FOR UPDATE SKIP LOCKED
526
- )
527
- ${singletonFetch ? ', grouped as ( SELECT id, row_number() OVER (PARTITION BY singleton_key) FROM next)' : ''}
528
- UPDATE ${schema}.${table} j SET
529
- state = '${JOB_STATES.active}',
530
- started_on = now(),
531
- retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
532
- FROM ${cte}
533
- WHERE name = '${name}' AND j.id = ${cte}.id
534
- ${singletonFetch ? ` AND ${cte}.row_number = 1` : ''}
535
- RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
536
- `,
537
- values: hasIgnoreSingletons ? [ignoreSingletons] : []
738
+ WITH
739
+ ${activeGroupCountsCte}
740
+ ${nextCte}
741
+ ${singletonCte}
742
+ ${groupConcurrencyCtes}
743
+ UPDATE ${schema}.${table} j SET
744
+ state = '${JOB_STATES.active}',
745
+ started_on = now(),
746
+ retry_count = CASE WHEN started_on IS NOT NULL THEN retry_count + 1 ELSE retry_count END
747
+ FROM ${finalCte}
748
+ WHERE name = '${name}' AND j.id = ${finalCte}.id
749
+ ${singletonFetch && !hasGroupConcurrency ? 'AND singleton_rn = 1' : ''}
750
+ RETURNING j.${includeMetadata ? JOB_COLUMNS_ALL : JOB_COLUMNS_MIN}
751
+ `,
752
+ values: params.values
538
753
  };
539
754
  }
540
755
  function completeJobs(schema, table) {
@@ -580,6 +795,14 @@ function resumeJobs(schema, table) {
580
795
  SELECT COUNT(*) from results
581
796
  `;
582
797
  }
798
+ function restoreJobs(schema, table) {
799
+ return `
800
+ UPDATE ${schema}.${table}
801
+ SET state = '${JOB_STATES.created}'
802
+ WHERE name = $1
803
+ AND id IN (SELECT UNNEST($2::uuid[]))
804
+ `;
805
+ }
583
806
  function insertJobs(schema, { table, name, returnId = true }) {
584
807
  const sql = `
585
808
  INSERT INTO ${schema}.${table} (
@@ -590,6 +813,8 @@ function insertJobs(schema, { table, name, returnId = true }) {
590
813
  start_after,
591
814
  singleton_key,
592
815
  singleton_on,
816
+ group_id,
817
+ group_tier,
593
818
  expire_seconds,
594
819
  deletion_seconds,
595
820
  keep_until,
@@ -611,6 +836,8 @@ function insertJobs(schema, { table, name, returnId = true }) {
611
836
  WHEN "singletonSeconds" IS NOT NULL THEN 'epoch'::timestamp + '1s'::interval * ("singletonSeconds" * floor(( date_part('epoch', now()) + COALESCE("singletonOffset",0)) / "singletonSeconds" ))
612
837
  ELSE NULL
613
838
  END as singleton_on,
839
+ "groupId" as group_id,
840
+ "groupTier" as group_tier,
614
841
  COALESCE("expireInSeconds", q.expire_seconds) as expire_seconds,
615
842
  COALESCE("deleteAfterSeconds", q.deletion_seconds) as deletion_seconds,
616
843
  j.start_after + (COALESCE("retentionSeconds", q.retention_seconds) * interval '1s') as keep_until,
@@ -638,10 +865,12 @@ function insertJobs(schema, { table, name, returnId = true }) {
638
865
  "singletonKey" text,
639
866
  "singletonSeconds" integer,
640
867
  "singletonOffset" integer,
868
+ "groupId" text,
869
+ "groupTier" text,
641
870
  "expireInSeconds" integer,
642
871
  "deleteAfterSeconds" integer,
643
872
  "retentionSeconds" integer
644
- )
873
+ )
645
874
  ) j
646
875
  JOIN ${schema}.queue q ON q.name = '${name}'
647
876
  ON CONFLICT DO NOTHING
@@ -684,6 +913,8 @@ function failJobs(schema, table, where, output) {
684
913
  started_on,
685
914
  singleton_key,
686
915
  singleton_on,
916
+ group_id,
917
+ group_tier,
687
918
  expire_seconds,
688
919
  deletion_seconds,
689
920
  created_on,
@@ -711,7 +942,7 @@ function failJobs(schema, table, where, output) {
711
942
  WHEN NOT retry_backoff THEN now() + retry_delay * interval '1'
712
943
  ELSE now() + LEAST(
713
944
  retry_delay_max,
714
- retry_delay + (
945
+ retry_delay * (
715
946
  2 ^ LEAST(16, retry_count + 1) / 2 +
716
947
  2 ^ LEAST(16, retry_count + 1) / 2 * random()
717
948
  )
@@ -720,6 +951,8 @@ function failJobs(schema, table, where, output) {
720
951
  started_on,
721
952
  singleton_key,
722
953
  singleton_on,
954
+ group_id,
955
+ group_tier,
723
956
  expire_seconds,
724
957
  deletion_seconds,
725
958
  created_on,
@@ -748,6 +981,8 @@ function failJobs(schema, table, where, output) {
748
981
  started_on,
749
982
  singleton_key,
750
983
  singleton_on,
984
+ group_id,
985
+ group_tier,
751
986
  expire_seconds,
752
987
  deletion_seconds,
753
988
  created_on,
@@ -772,6 +1007,8 @@ function failJobs(schema, table, where, output) {
772
1007
  started_on,
773
1008
  singleton_key,
774
1009
  singleton_on,
1010
+ group_id,
1011
+ group_tier,
775
1012
  expire_seconds,
776
1013
  deletion_seconds,
777
1014
  created_on,
@@ -813,7 +1050,7 @@ function deletion(schema, table, queues) {
813
1050
  WHERE name = ANY(${serializeArrayParam(queues)})
814
1051
  AND
815
1052
  (
816
- completed_on + deletion_seconds * interval '1s' < now()
1053
+ (deletion_seconds > 0 AND completed_on + deletion_seconds * interval '1s' < now())
817
1054
  OR
818
1055
  (state < '${JOB_STATES.active}' AND keep_until < now())
819
1056
  )
@@ -901,7 +1138,83 @@ function assertMigration(schema, version) {
901
1138
  // raises 'division by zero' if already on desired schema version
902
1139
  return `SELECT version::int/(version::int-${version}) from ${schema}.version`;
903
1140
  }
1141
+ function findJobs(schema, table, options) {
1142
+ const { queued, byKey, byData, byId } = options;
1143
+ let paramIndex = 1;
1144
+ const whereConditions = [];
1145
+ if (byId) {
1146
+ ++paramIndex;
1147
+ whereConditions.push(`AND id = $${paramIndex}`);
1148
+ }
1149
+ if (byKey) {
1150
+ ++paramIndex;
1151
+ whereConditions.push(`AND singleton_key = $${paramIndex}`);
1152
+ }
1153
+ if (byData) {
1154
+ ++paramIndex;
1155
+ whereConditions.push(`AND data @> $${paramIndex}`);
1156
+ }
1157
+ if (queued) {
1158
+ whereConditions.push(`AND state < '${JOB_STATES.active}'`);
1159
+ }
1160
+ return `
1161
+ SELECT ${JOB_COLUMNS_ALL}
1162
+ FROM ${schema}.${table}
1163
+ WHERE name = $1
1164
+ ${whereConditions.join('\n ')}
1165
+ `;
1166
+ }
904
1167
  function getJobById(schema, table) {
905
- return `SELECT ${JOB_COLUMNS_ALL} FROM ${schema}.${table} WHERE name = $1 AND id = $2`;
1168
+ return `
1169
+ SELECT ${JOB_COLUMNS_ALL}
1170
+ FROM ${schema}.${table}
1171
+ WHERE name = $1
1172
+ AND id = $2
1173
+ `;
1174
+ }
1175
+ function getNextBamCommand(schema) {
1176
+ return `
1177
+ UPDATE ${schema}.bam
1178
+ SET status = 'in_progress', started_on = now()
1179
+ WHERE id = (
1180
+ SELECT id FROM ${schema}.bam
1181
+ WHERE status IN ('pending', 'failed')
1182
+ AND NOT EXISTS (SELECT 1 FROM ${schema}.bam WHERE status = 'in_progress')
1183
+ ORDER BY created_on
1184
+ LIMIT 1
1185
+ )
1186
+ RETURNING id, name, version, status, queue, table_name as "table", command, error,
1187
+ created_on as "createdOn", started_on as "startedOn", completed_on as "completedOn"
1188
+ `;
1189
+ }
1190
+ function setBamCompleted(schema, id) {
1191
+ return `
1192
+ UPDATE ${schema}.bam
1193
+ SET status = 'completed', completed_on = now()
1194
+ WHERE id = '${id}'
1195
+ `;
1196
+ }
1197
+ function setBamFailed(schema, id, error) {
1198
+ const escapedError = error.replace(/'/g, "''");
1199
+ return `
1200
+ UPDATE ${schema}.bam
1201
+ SET status = 'failed', error = '${escapedError}', completed_on = now()
1202
+ WHERE id = '${id}'
1203
+ `;
1204
+ }
1205
+ function getBamStatus(schema) {
1206
+ return `
1207
+ SELECT status, count(*)::int as count, max(created_on) as "lastCreatedOn"
1208
+ FROM ${schema}.bam
1209
+ GROUP BY status
1210
+ `;
1211
+ }
1212
+ function getBamEntries(schema) {
1213
+ return `
1214
+ SELECT id, name, version, status, queue, table_name as "table", command, error,
1215
+ created_on as "createdOn", started_on as "startedOn", completed_on as "completedOn"
1216
+ FROM ${schema}.bam
1217
+ ORDER BY version, created_on
1218
+ `;
906
1219
  }
907
- export { create, insertVersion, getVersion, setVersion, versionTableExists, fetchNextJob, completeJobs, cancelJobs, resumeJobs, retryJobs, deleteJobsById, deleteAllJobs, deleteQueuedJobs, deleteStoredJobs, truncateTable, failJobsById, failJobsByTimeout, insertJobs, getTime, getSchedules, getSchedulesByQueue, schedule, unschedule, subscribe, unsubscribe, getQueuesForEvent, deletion, cacheQueueStats, updateQueue, createQueue, deleteQueue, getQueues, getQueueStats, trySetQueueMonitorTime, trySetQueueDeletionTime, trySetCronTime, locked, assertMigration, getJobById, QUEUE_POLICIES, JOB_STATES, MIGRATE_RACE_MESSAGE, CREATE_RACE_MESSAGE, DEFAULT_SCHEMA, };
1220
+ export { create, insertVersion, getVersion, setVersion, versionTableExists, fetchNextJob, completeJobs, cancelJobs, resumeJobs, restoreJobs, retryJobs, findJobs, deleteJobsById, deleteAllJobs, deleteQueuedJobs, deleteStoredJobs, truncateTable, failJobsById, failJobsByTimeout, insertJobs, getTime, getSchedules, getSchedulesByQueue, schedule, unschedule, subscribe, unsubscribe, getQueuesForEvent, deletion, cacheQueueStats, updateQueue, createQueue, deleteQueue, getQueues, getQueueStats, trySetQueueMonitorTime, trySetQueueDeletionTime, trySetCronTime, trySetBamTime, locked, assertMigration, getJobById, getNextBamCommand, setBamCompleted, setBamFailed, getBamStatus, getBamEntries, QUEUE_POLICIES, JOB_STATES, MIGRATE_RACE_MESSAGE, CREATE_RACE_MESSAGE, DEFAULT_SCHEMA, };