pg-boss 12.21.2 → 12.23.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/README.md +1 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +6 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/manager.d.ts +2 -0
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +53 -22
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +134 -0
- package/dist/navigator.d.ts +17 -0
- package/dist/navigator.d.ts.map +1 -0
- package/dist/navigator.js +141 -0
- package/dist/plans.d.ts +6 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +188 -67
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import * as plans from "./plans.js";
|
|
3
|
+
import { delay } from "./tools.js";
|
|
4
|
+
import * as types from "./types.js";
|
|
5
|
+
const events = {
|
|
6
|
+
error: 'error',
|
|
7
|
+
flow: 'flow'
|
|
8
|
+
};
|
|
9
|
+
// Cap audit batches per resolve pass so a large backlog can't monopolize the loop; whatever is
|
|
10
|
+
// left over is picked up on the next poll.
|
|
11
|
+
const MAX_BATCHES_PER_PASS = 100;
|
|
12
|
+
// Background flow resolver. Completion is kept on a join-free hot path (see issue #824); the
|
|
13
|
+
// dependency bookkeeping that used to run inline now happens here, out of band. Modeled on the
|
|
14
|
+
// Bam poller: on each tick it claims the cluster-wide cadence gate (version.flow_on) and, if it
|
|
15
|
+
// wins, audits for completed "blocking" parents via the job_i9 partial index, decrements their
|
|
16
|
+
// children, unblocks those reaching zero, and clears the parents' blocking flag so they are not
|
|
17
|
+
// reprocessed. The Guild Navigator that keeps the spice flowing.
|
|
18
|
+
class Navigator extends EventEmitter {
|
|
19
|
+
#stopped;
|
|
20
|
+
#stopping;
|
|
21
|
+
#working;
|
|
22
|
+
#pollInterval;
|
|
23
|
+
#db;
|
|
24
|
+
#manager;
|
|
25
|
+
#config;
|
|
26
|
+
events = events;
|
|
27
|
+
constructor(db, manager, config) {
|
|
28
|
+
super();
|
|
29
|
+
this.#db = db;
|
|
30
|
+
this.#manager = manager;
|
|
31
|
+
this.#config = config;
|
|
32
|
+
this.#stopped = true;
|
|
33
|
+
this.#stopping = false;
|
|
34
|
+
this.#working = false;
|
|
35
|
+
}
|
|
36
|
+
get working() {
|
|
37
|
+
return this.#working;
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
if (!this.#stopped)
|
|
41
|
+
return;
|
|
42
|
+
this.#stopped = false;
|
|
43
|
+
this.#stopping = false;
|
|
44
|
+
setImmediate(() => this.#onPoll());
|
|
45
|
+
this.#pollInterval = setInterval(() => this.#onPoll(), this.#config.flowIntervalSeconds * 1000);
|
|
46
|
+
}
|
|
47
|
+
async stop() {
|
|
48
|
+
if (this.#stopped)
|
|
49
|
+
return;
|
|
50
|
+
this.#stopping = true;
|
|
51
|
+
this.#stopped = true;
|
|
52
|
+
if (this.#pollInterval) {
|
|
53
|
+
clearInterval(this.#pollInterval);
|
|
54
|
+
this.#pollInterval = undefined;
|
|
55
|
+
}
|
|
56
|
+
while (this.#working) {
|
|
57
|
+
await delay(10);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async #onPoll() {
|
|
61
|
+
if (this.#stopped || this.#working)
|
|
62
|
+
return;
|
|
63
|
+
this.#working = true;
|
|
64
|
+
try {
|
|
65
|
+
if (this.#config.__test__throw_flow) {
|
|
66
|
+
throw new Error(this.#config.__test__throw_flow);
|
|
67
|
+
}
|
|
68
|
+
if (this.#config.__test__delay_flow_ms) {
|
|
69
|
+
await delay(this.#config.__test__delay_flow_ms);
|
|
70
|
+
}
|
|
71
|
+
const gate = plans.trySetFlowTime(this.#config.schema, this.#config.flowIntervalSeconds);
|
|
72
|
+
const { rows } = await this.#db.executeSql(gate);
|
|
73
|
+
if (rows.length === 1) {
|
|
74
|
+
await this.#resolve();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.emit(events.error, err);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
this.#working = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// On-demand, ungated resolution pass. Like boss.supervise(), it is callable whether or not the
|
|
85
|
+
// background poll is running, so tests and apps can resolve flows deterministically. It skips
|
|
86
|
+
// the version-table cadence gate but still serializes against an in-flight poll via #working.
|
|
87
|
+
async resolveNow() {
|
|
88
|
+
while (this.#working) {
|
|
89
|
+
await delay(10);
|
|
90
|
+
}
|
|
91
|
+
if (this.#stopping)
|
|
92
|
+
return;
|
|
93
|
+
this.#working = true;
|
|
94
|
+
try {
|
|
95
|
+
await this.#resolve();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
this.#working = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async #resolve() {
|
|
102
|
+
const queues = await this.#manager.getQueues();
|
|
103
|
+
// Group queues by partition table so each audit statement targets a single table and prunes to
|
|
104
|
+
// the chunk's queue names (mirrors boss.supervise()'s grouping).
|
|
105
|
+
const queueGroups = queues.reduce((acc, q) => {
|
|
106
|
+
acc[q.table] = acc[q.table] || { table: q.table, names: [] };
|
|
107
|
+
acc[q.table].names.push(q.name);
|
|
108
|
+
return acc;
|
|
109
|
+
}, {});
|
|
110
|
+
for (const group of Object.values(queueGroups)) {
|
|
111
|
+
if (this.#stopping)
|
|
112
|
+
return;
|
|
113
|
+
const { table } = group;
|
|
114
|
+
const names = [...group.names];
|
|
115
|
+
while (names.length) {
|
|
116
|
+
if (this.#stopping)
|
|
117
|
+
return;
|
|
118
|
+
const chunk = names.splice(0, 100);
|
|
119
|
+
let batches = 0;
|
|
120
|
+
let resolved = 0;
|
|
121
|
+
do {
|
|
122
|
+
if (this.#stopping)
|
|
123
|
+
return;
|
|
124
|
+
resolved = this.#config.noMultiMutationCte
|
|
125
|
+
? await this.#manager.resolveFlowJobsDistributed(table, chunk)
|
|
126
|
+
: await this.#resolveStandard(table, chunk);
|
|
127
|
+
if (resolved > 0) {
|
|
128
|
+
this.emit(events.flow, { table, resolved });
|
|
129
|
+
}
|
|
130
|
+
} while (resolved >= plans.FLOW_BATCH_SIZE && ++batches < MAX_BATCHES_PER_PASS && !this.#stopping);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async #resolveStandard(table, names) {
|
|
135
|
+
const query = plans.resolveFlowJobs(this.#config.schema, table, names);
|
|
136
|
+
const { rows } = await this.#db.executeSql(query.text, query.values);
|
|
137
|
+
// CockroachDB returns integer columns as strings; coerce so the drain-loop comparison is numeric.
|
|
138
|
+
return Number(rows[0]?.resolved ?? 0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export default Navigator;
|
package/dist/plans.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare function trySetQueueMonitorTime(schema: string, queues: string[],
|
|
|
45
45
|
export declare function trySetQueueDeletionTime(schema: string, queues: string[], seconds: number): SqlQuery;
|
|
46
46
|
export declare function trySetCronTime(schema: string, seconds: number): string;
|
|
47
47
|
export declare function trySetBamTime(schema: string, seconds: number): string;
|
|
48
|
+
export declare function trySetFlowTime(schema: string, seconds: number): string;
|
|
48
49
|
export declare function updateQueue(schema: string, { deadLetter }?: UpdateQueueOptions): string;
|
|
49
50
|
export declare function getQueues(schema: string, names?: string[]): SqlQuery;
|
|
50
51
|
export declare function deleteJobsById(schema: string, table: string): string;
|
|
@@ -136,8 +137,13 @@ export declare function selectJobsToFailByHeartbeat(schema: string, table: strin
|
|
|
136
137
|
export declare function deleteJobsByIds(schema: string, table: string): SqlQuery;
|
|
137
138
|
export declare function completeJobsDistributed(schema: string, table: string, includeQueued?: boolean): string;
|
|
138
139
|
export declare function decrementDependents(schema: string): string;
|
|
140
|
+
export declare const FLOW_BATCH_SIZE = 1000;
|
|
141
|
+
export declare function resolveFlowJobs(schema: string, table: string, names: string[]): SqlQuery;
|
|
142
|
+
export declare function selectBlockingParents(schema: string, table: string, names: string[], noSkipLocked?: boolean): SqlQuery;
|
|
143
|
+
export declare function clearBlocking(schema: string): string;
|
|
139
144
|
export declare function insertRetryJob(schema: string, table: string): string;
|
|
140
145
|
export declare function insertDeadLetterJob(schema: string): string;
|
|
146
|
+
export declare function redriveJobs(schema: string, table: string): string;
|
|
141
147
|
export declare function deletion(schema: string, table: string, queues: string[], noAdvisoryLocks?: boolean): string;
|
|
142
148
|
export declare function retryJobs(schema: string, table: string): string;
|
|
143
149
|
export declare function getQueueStats(schema: string, table: string, queues: string[]): SqlQuery;
|
package/dist/plans.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,EAAE,CAAA;CAClB;AAED,eAAO,MAAM,QAAQ;;CAEpB,CAAA;AAED,eAAO,MAAM,cAAc,WAAW,CAAA;AACtC,eAAO,MAAM,oBAAoB,qBAAqB,CAAA;AACtD,eAAO,MAAM,mBAAmB,mBAAmB,CAAA;AAMnD,eAAO,MAAM,UAAU;;;;;;;EAOrB,CAAA;AAEF,eAAO,MAAM,cAAc;;;;;;;EAOzB,CAAA;AAeF,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAgB,MAAM,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,UAyC/E;
|
|
1
|
+
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"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"}
|
package/dist/plans.js
CHANGED
|
@@ -90,7 +90,8 @@ function createTableVersion(schema) {
|
|
|
90
90
|
CREATE TABLE ${schema}.version (
|
|
91
91
|
version int primary key,
|
|
92
92
|
cron_on timestamp with time zone,
|
|
93
|
-
bam_on timestamp with time zone
|
|
93
|
+
bam_on timestamp with time zone,
|
|
94
|
+
flow_on timestamp with time zone
|
|
94
95
|
)
|
|
95
96
|
`;
|
|
96
97
|
}
|
|
@@ -320,7 +321,11 @@ function createTableJob(schema, noPartitioning = false) {
|
|
|
320
321
|
heartbeat_seconds int,
|
|
321
322
|
blocked boolean not null default false,
|
|
322
323
|
blocking boolean not null default false,
|
|
323
|
-
pending_dependencies int not null default 0
|
|
324
|
+
pending_dependencies int not null default 0,
|
|
325
|
+
source_name text,
|
|
326
|
+
source_id uuid,
|
|
327
|
+
source_created_on timestamp with time zone,
|
|
328
|
+
source_retry_count int
|
|
324
329
|
) ${partitionClause}
|
|
325
330
|
`;
|
|
326
331
|
}
|
|
@@ -347,7 +352,11 @@ const JOB_COLUMNS_ALL = `${JOB_COLUMNS_MIN},
|
|
|
347
352
|
blocked,
|
|
348
353
|
blocking,
|
|
349
354
|
pending_dependencies as "pendingDependencies",
|
|
350
|
-
output
|
|
355
|
+
output,
|
|
356
|
+
source_name as "sourceName",
|
|
357
|
+
source_id as "sourceId",
|
|
358
|
+
source_created_on as "sourceCreatedOn",
|
|
359
|
+
source_retry_count as "sourceRetryCount"
|
|
351
360
|
`;
|
|
352
361
|
function createTableJobCommon(schema) {
|
|
353
362
|
return `
|
|
@@ -365,6 +374,7 @@ function createTableJobCommon(schema) {
|
|
|
365
374
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobThrottle(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
366
375
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobFetch(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
367
376
|
SELECT ${schema}.job_table_run($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
377
|
+
SELECT ${schema}.job_table_run($cmd$${createIndexJobBlocking(schema)}$cmd$, '${COMMON_JOB_TABLE}');
|
|
368
378
|
|
|
369
379
|
ALTER TABLE ${schema}.job ATTACH PARTITION ${schema}.${COMMON_JOB_TABLE} DEFAULT;
|
|
370
380
|
`;
|
|
@@ -383,6 +393,7 @@ function createTableJobIndexes(schema, noDeferrableConstraints = false, noCoveri
|
|
|
383
393
|
${createIndexJobThrottle(schema)};
|
|
384
394
|
${createIndexJobFetch(schema, noCoveringIndex)};
|
|
385
395
|
${createIndexJobGroupConcurrency(schema)};
|
|
396
|
+
${createIndexJobBlocking(schema)};
|
|
386
397
|
`;
|
|
387
398
|
}
|
|
388
399
|
function createQueueFunction(schema, noPartitioning = false) {
|
|
@@ -496,6 +507,7 @@ function createQueueFunction(schema, noPartitioning = false) {
|
|
|
496
507
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobFetch(schema)}$cmd$, tablename);
|
|
497
508
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobThrottle(schema)}$cmd$, tablename);
|
|
498
509
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobGroupConcurrency(schema)}$cmd$, tablename);
|
|
510
|
+
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobBlocking(schema)}$cmd$, tablename);
|
|
499
511
|
|
|
500
512
|
IF options->>'policy' = 'short' THEN
|
|
501
513
|
EXECUTE ${schema}.job_table_format($cmd$${createIndexJobPolicyShort(schema)}$cmd$, tablename);
|
|
@@ -602,10 +614,12 @@ function createIndexJobThrottle(schema) {
|
|
|
602
614
|
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`;
|
|
603
615
|
}
|
|
604
616
|
function createIndexJobFetch(schema, noCoveringIndex = false) {
|
|
605
|
-
//
|
|
606
|
-
// so
|
|
607
|
-
|
|
608
|
-
|
|
617
|
+
// No covering INCLUDE: the fetch locks candidate rows with FOR UPDATE ... SKIP LOCKED, which
|
|
618
|
+
// forces heap access, so an index-only scan is impossible and a covering payload would never be
|
|
619
|
+
// read from the index. Confirmed dead weight via EXPLAIN ANALYZE (see examples/index-perf);
|
|
620
|
+
// dropping it shrinks job_i5 on the hot insert path at no read-side cost.
|
|
621
|
+
// noCoveringIndex (the CockroachDB profile flag that stripped the old INCLUDE) is now moot here.
|
|
622
|
+
return `CREATE INDEX job_i5 ON ${schema}.job (name, start_after) WHERE state < '${JOB_STATES.active}' AND NOT blocked`;
|
|
609
623
|
}
|
|
610
624
|
function createIndexJobPolicyExclusive(schema) {
|
|
611
625
|
return `CREATE UNIQUE INDEX job_i6 ON ${schema}.job (name, COALESCE(singleton_key, '')) WHERE state <= '${JOB_STATES.active}' AND policy = '${QUEUE_POLICIES.exclusive}'`;
|
|
@@ -619,6 +633,13 @@ function createCheckConstraintKeyStrictFifo(schema) {
|
|
|
619
633
|
function createIndexJobGroupConcurrency(schema) {
|
|
620
634
|
return `CREATE INDEX job_i7 ON ${schema}.job (name, group_id) WHERE state = '${JOB_STATES.active}' AND group_id IS NOT NULL`;
|
|
621
635
|
}
|
|
636
|
+
// Partial index supporting the background flow resolver (Navigator): lets it find completed
|
|
637
|
+
// blocking parents with an index scan instead of a partition-wide scan. The `state = completed`
|
|
638
|
+
// predicate keeps still-running and permanently-failed blocking parents out of the index, so
|
|
639
|
+
// non-flow queues (and high-partition-count deployments) carry an empty index that costs nothing.
|
|
640
|
+
function createIndexJobBlocking(schema) {
|
|
641
|
+
return `CREATE INDEX job_i9 ON ${schema}.job (name, id) WHERE blocking AND state = '${JOB_STATES.completed}'`;
|
|
642
|
+
}
|
|
622
643
|
export function trySetQueueMonitorTime(schema, queues, seconds) {
|
|
623
644
|
return trySetQueueTimestamp(schema, queues, 'monitor_on', seconds);
|
|
624
645
|
}
|
|
@@ -631,6 +652,9 @@ export function trySetCronTime(schema, seconds) {
|
|
|
631
652
|
export function trySetBamTime(schema, seconds) {
|
|
632
653
|
return trySetTimestamp(schema, 'bam_on', seconds);
|
|
633
654
|
}
|
|
655
|
+
export function trySetFlowTime(schema, seconds) {
|
|
656
|
+
return trySetTimestamp(schema, 'flow_on', seconds);
|
|
657
|
+
}
|
|
634
658
|
function trySetTimestamp(schema, column, seconds) {
|
|
635
659
|
return `
|
|
636
660
|
UPDATE ${schema}.version
|
|
@@ -717,7 +741,7 @@ export function deleteJobsById(schema, table) {
|
|
|
717
741
|
WITH results as (
|
|
718
742
|
DELETE FROM ${schema}.${table}
|
|
719
743
|
WHERE name = $1
|
|
720
|
-
AND id
|
|
744
|
+
AND id = ANY($2::uuid[])
|
|
721
745
|
RETURNING 1
|
|
722
746
|
)
|
|
723
747
|
SELECT COUNT(*) from results
|
|
@@ -1031,7 +1055,7 @@ function completeJobsUpdate(schema, table, includeQueued) {
|
|
|
1031
1055
|
blocked = ${includeQueued ? 'false' : 'blocked'},
|
|
1032
1056
|
pending_dependencies = ${includeQueued ? '0' : 'pending_dependencies'}
|
|
1033
1057
|
WHERE name = $1
|
|
1034
|
-
AND id
|
|
1058
|
+
AND id = ANY($2::uuid[])
|
|
1035
1059
|
AND ${includeQueued
|
|
1036
1060
|
? `state < '${JOB_STATES.completed}'`
|
|
1037
1061
|
: `state = '${JOB_STATES.active}'`}`;
|
|
@@ -1058,26 +1082,17 @@ function unblockChildrenUpdate(schema) {
|
|
|
1058
1082
|
WHERE j.name = lc.name
|
|
1059
1083
|
AND j.id = lc.id`;
|
|
1060
1084
|
}
|
|
1085
|
+
// Dependency unblocking is intentionally NOT done here. Completion is the hot path; chasing
|
|
1086
|
+
// dependents inline (joining job_dependency and the partitioned job table) made completion
|
|
1087
|
+
// scale with partition count (see issue #824). The background resolver (Navigator) handles
|
|
1088
|
+
// unblocking out of band, driven by the job_i9 partial index.
|
|
1061
1089
|
export function completeJobs(schema, table, includeQueued) {
|
|
1062
1090
|
return `
|
|
1063
|
-
WITH
|
|
1091
|
+
WITH results AS (
|
|
1064
1092
|
${completeJobsUpdate(schema, table, includeQueued)}
|
|
1065
|
-
RETURNING name, id, blocking
|
|
1066
|
-
),
|
|
1067
|
-
decremented AS (
|
|
1068
|
-
SELECT d.child_name, d.child_id, COUNT(*)::int AS n
|
|
1069
|
-
FROM ${schema}.job_dependency d
|
|
1070
|
-
JOIN completed c ON c.blocking
|
|
1071
|
-
AND d.parent_name = c.name
|
|
1072
|
-
AND d.parent_id = c.id
|
|
1073
|
-
GROUP BY d.child_name, d.child_id
|
|
1074
|
-
),
|
|
1075
|
-
${lockedChildrenCte(schema)},
|
|
1076
|
-
unblocked AS (
|
|
1077
|
-
${unblockChildrenUpdate(schema)}
|
|
1078
1093
|
RETURNING 1
|
|
1079
1094
|
)
|
|
1080
|
-
SELECT COUNT(*) FROM
|
|
1095
|
+
SELECT COUNT(*) FROM results
|
|
1081
1096
|
`;
|
|
1082
1097
|
}
|
|
1083
1098
|
// Per-job-output completion: each job's output is supplied via a JSON recordset ($2) and applied by
|
|
@@ -1088,7 +1103,7 @@ export function completeJobsWithOutputs(schema, table) {
|
|
|
1088
1103
|
WITH input AS (
|
|
1089
1104
|
SELECT * FROM json_to_recordset($2::json) AS x (id uuid, output jsonb)
|
|
1090
1105
|
),
|
|
1091
|
-
|
|
1106
|
+
results AS (
|
|
1092
1107
|
UPDATE ${schema}.${table} j
|
|
1093
1108
|
SET completed_on = now(),
|
|
1094
1109
|
state = '${JOB_STATES.completed}',
|
|
@@ -1097,27 +1112,14 @@ export function completeJobsWithOutputs(schema, table) {
|
|
|
1097
1112
|
WHERE j.name = $1
|
|
1098
1113
|
AND j.id = i.id
|
|
1099
1114
|
AND j.state = '${JOB_STATES.active}'
|
|
1100
|
-
RETURNING j.name, j.id, j.blocking
|
|
1101
|
-
),
|
|
1102
|
-
decremented AS (
|
|
1103
|
-
SELECT d.child_name, d.child_id, COUNT(*)::int AS n
|
|
1104
|
-
FROM ${schema}.job_dependency d
|
|
1105
|
-
JOIN completed c ON c.blocking
|
|
1106
|
-
AND d.parent_name = c.name
|
|
1107
|
-
AND d.parent_id = c.id
|
|
1108
|
-
GROUP BY d.child_name, d.child_id
|
|
1109
|
-
),
|
|
1110
|
-
${lockedChildrenCte(schema)},
|
|
1111
|
-
unblocked AS (
|
|
1112
|
-
${unblockChildrenUpdate(schema)}
|
|
1113
1115
|
RETURNING 1
|
|
1114
1116
|
)
|
|
1115
|
-
SELECT COUNT(*) FROM
|
|
1117
|
+
SELECT COUNT(*) FROM results
|
|
1116
1118
|
`;
|
|
1117
1119
|
}
|
|
1118
1120
|
// Distributed equivalent of completeJobsWithOutputs: a single mutation that returns the completed
|
|
1119
|
-
// ids
|
|
1120
|
-
//
|
|
1121
|
+
// ids. Dependency unblocking is handled out of band by the background resolver (Navigator), so
|
|
1122
|
+
// completion does no dependency work on any backend.
|
|
1121
1123
|
export function completeJobsWithOutputsDistributed(schema, table) {
|
|
1122
1124
|
return `
|
|
1123
1125
|
WITH input AS (
|
|
@@ -1131,7 +1133,7 @@ export function completeJobsWithOutputsDistributed(schema, table) {
|
|
|
1131
1133
|
WHERE j.name = $1
|
|
1132
1134
|
AND j.id = i.id
|
|
1133
1135
|
AND j.state = '${JOB_STATES.active}'
|
|
1134
|
-
RETURNING j.id
|
|
1136
|
+
RETURNING j.id
|
|
1135
1137
|
`;
|
|
1136
1138
|
}
|
|
1137
1139
|
export function cancelJobs(schema, table) {
|
|
@@ -1141,7 +1143,7 @@ export function cancelJobs(schema, table) {
|
|
|
1141
1143
|
SET completed_on = now(),
|
|
1142
1144
|
state = '${JOB_STATES.cancelled}'
|
|
1143
1145
|
WHERE name = $1
|
|
1144
|
-
AND id
|
|
1146
|
+
AND id = ANY($2::uuid[])
|
|
1145
1147
|
AND state < '${JOB_STATES.completed}'
|
|
1146
1148
|
RETURNING 1
|
|
1147
1149
|
)
|
|
@@ -1155,7 +1157,7 @@ export function resumeJobs(schema, table) {
|
|
|
1155
1157
|
SET completed_on = NULL,
|
|
1156
1158
|
state = '${JOB_STATES.created}'
|
|
1157
1159
|
WHERE name = $1
|
|
1158
|
-
AND id
|
|
1160
|
+
AND id = ANY($2::uuid[])
|
|
1159
1161
|
AND state = '${JOB_STATES.cancelled}'
|
|
1160
1162
|
RETURNING 1
|
|
1161
1163
|
)
|
|
@@ -1169,7 +1171,7 @@ export function restoreJobs(schema, table) {
|
|
|
1169
1171
|
started_on = NULL,
|
|
1170
1172
|
heartbeat_on = NULL
|
|
1171
1173
|
WHERE name = $1
|
|
1172
|
-
AND id
|
|
1174
|
+
AND id = ANY($2::uuid[])
|
|
1173
1175
|
`;
|
|
1174
1176
|
}
|
|
1175
1177
|
export function insertJobs(schema, { table, name, returnId = true, notify = false }) {
|
|
@@ -1297,7 +1299,7 @@ export function insertFlowJobs(schema, { table, name }, jobs) {
|
|
|
1297
1299
|
`;
|
|
1298
1300
|
}
|
|
1299
1301
|
export function failJobsById(schema, table) {
|
|
1300
|
-
const where = `name = $1 AND id
|
|
1302
|
+
const where = `name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`;
|
|
1301
1303
|
const output = '$3::jsonb';
|
|
1302
1304
|
return failJobs(schema, table, where, output);
|
|
1303
1305
|
}
|
|
@@ -1322,7 +1324,7 @@ export function touchJobs(schema, table) {
|
|
|
1322
1324
|
UPDATE ${schema}.${table}
|
|
1323
1325
|
SET heartbeat_on = now()
|
|
1324
1326
|
WHERE name = $1
|
|
1325
|
-
AND id
|
|
1327
|
+
AND id = ANY($2::uuid[])
|
|
1326
1328
|
AND state = '${JOB_STATES.active}'
|
|
1327
1329
|
RETURNING 1
|
|
1328
1330
|
)
|
|
@@ -1504,16 +1506,21 @@ function failJobsBody(schema, table, where, output, forceTerminal = false) {
|
|
|
1504
1506
|
SELECT * FROM failed_jobs
|
|
1505
1507
|
),
|
|
1506
1508
|
dlq_jobs as (
|
|
1507
|
-
INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds
|
|
1509
|
+
INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds,
|
|
1510
|
+
source_name, source_id, source_created_on, source_retry_count)
|
|
1508
1511
|
SELECT
|
|
1509
1512
|
r.dead_letter,
|
|
1510
|
-
data,
|
|
1511
|
-
output,
|
|
1513
|
+
r.data,
|
|
1514
|
+
r.output,
|
|
1512
1515
|
q.retry_limit,
|
|
1513
1516
|
q.retry_backoff,
|
|
1514
1517
|
q.retry_delay,
|
|
1515
1518
|
now() + q.retention_seconds * interval '1s',
|
|
1516
|
-
q.deletion_seconds
|
|
1519
|
+
q.deletion_seconds,
|
|
1520
|
+
r.name,
|
|
1521
|
+
r.id,
|
|
1522
|
+
r.created_on,
|
|
1523
|
+
r.retry_count
|
|
1517
1524
|
FROM results r
|
|
1518
1525
|
JOIN ${schema}.queue q ON q.name = r.dead_letter
|
|
1519
1526
|
WHERE state = '${JOB_STATES.failed}'
|
|
@@ -1549,13 +1556,13 @@ export function deadLetterJobsByIdWithOutputs(schema, table) {
|
|
|
1549
1556
|
// Distributed mode: separate queries to avoid CockroachDB's multi-mutation CTE limitation
|
|
1550
1557
|
export function selectJobsToFailById(schema, table) {
|
|
1551
1558
|
return {
|
|
1552
|
-
text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id
|
|
1559
|
+
text: `SELECT * FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[]) AND state < '${JOB_STATES.completed}'`,
|
|
1553
1560
|
values: []
|
|
1554
1561
|
};
|
|
1555
1562
|
}
|
|
1556
1563
|
export function deleteJobsToFail(schema, table) {
|
|
1557
1564
|
return {
|
|
1558
|
-
text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id
|
|
1565
|
+
text: `DELETE FROM ${schema}.${table} WHERE name = $1 AND id = ANY($2::uuid[])`,
|
|
1559
1566
|
values: []
|
|
1560
1567
|
};
|
|
1561
1568
|
}
|
|
@@ -1584,37 +1591,112 @@ export function selectJobsToFailByHeartbeat(schema, table, queues) {
|
|
|
1584
1591
|
}
|
|
1585
1592
|
export function deleteJobsByIds(schema, table) {
|
|
1586
1593
|
return {
|
|
1587
|
-
text: `DELETE FROM ${schema}.${table} WHERE id
|
|
1594
|
+
text: `DELETE FROM ${schema}.${table} WHERE id = ANY($1::uuid[])`,
|
|
1588
1595
|
values: []
|
|
1589
1596
|
};
|
|
1590
1597
|
}
|
|
1591
|
-
// Distributed mode: complete jobs
|
|
1592
|
-
//
|
|
1593
|
-
// completeJobs() because it updates both the job table and job (for unblocking)
|
|
1594
|
-
// within one statement. The unblocking is handled separately by decrementDependents().
|
|
1598
|
+
// Distributed mode: complete jobs as a single-table mutation. Dependency unblocking is handled
|
|
1599
|
+
// out of band by the background resolver (Navigator), so completion does no dependency work.
|
|
1595
1600
|
export function completeJobsDistributed(schema, table, includeQueued) {
|
|
1596
1601
|
return `
|
|
1597
1602
|
${completeJobsUpdate(schema, table, includeQueued)}
|
|
1598
|
-
RETURNING id
|
|
1603
|
+
RETURNING id
|
|
1599
1604
|
`;
|
|
1600
1605
|
}
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
//
|
|
1606
|
+
// Decrement pending_dependencies for children of the given completed parent jobs, unblocking
|
|
1607
|
+
// any that reach zero. Only the final UPDATE mutates job, so this is a single mutation acceptable
|
|
1608
|
+
// to CockroachDB. Used by the distributed flow resolver path. $1 is the parent queue name, $2 the
|
|
1609
|
+
// list of resolved parent ids for that queue.
|
|
1605
1610
|
export function decrementDependents(schema) {
|
|
1606
1611
|
return `
|
|
1607
1612
|
WITH decremented AS (
|
|
1608
1613
|
SELECT d.child_name, d.child_id, COUNT(*)::int AS n
|
|
1609
1614
|
FROM ${schema}.job_dependency d
|
|
1610
1615
|
WHERE d.parent_name = $1
|
|
1611
|
-
AND d.parent_id
|
|
1616
|
+
AND d.parent_id = ANY($2::uuid[])
|
|
1612
1617
|
GROUP BY d.child_name, d.child_id
|
|
1613
1618
|
),
|
|
1614
1619
|
${lockedChildrenCte(schema)}
|
|
1615
1620
|
${unblockChildrenUpdate(schema)}
|
|
1616
1621
|
`;
|
|
1617
1622
|
}
|
|
1623
|
+
// Background flow resolver (Navigator) batch size: the max number of completed blocking parents
|
|
1624
|
+
// locked per audit statement. The resolver loops until a batch drains, so this only bounds the
|
|
1625
|
+
// lock footprint and per-statement cost.
|
|
1626
|
+
export const FLOW_BATCH_SIZE = 1000;
|
|
1627
|
+
// Standard (multi-mutation CTE) flow audit. Locks a batch of completed blocking parents in the
|
|
1628
|
+
// given partition table, decrements their children's pending_dependencies (reusing the shared
|
|
1629
|
+
// unblock fragments, which reach across partitions via the parent job table), unblocks children
|
|
1630
|
+
// that reach zero, and clears `blocking` on the resolved parents so they leave the job_i9 index
|
|
1631
|
+
// and are never reprocessed. $1 is the chunk of queue names (for partition pruning). Returns the
|
|
1632
|
+
// number of parents resolved so the caller can loop until a batch drains.
|
|
1633
|
+
export function resolveFlowJobs(schema, table, names) {
|
|
1634
|
+
return {
|
|
1635
|
+
text: `
|
|
1636
|
+
WITH locked_parents AS (
|
|
1637
|
+
SELECT j.name, j.id
|
|
1638
|
+
FROM ${schema}.${table} j
|
|
1639
|
+
WHERE j.blocking
|
|
1640
|
+
AND j.state = '${JOB_STATES.completed}'
|
|
1641
|
+
AND j.name = ANY($1::text[])
|
|
1642
|
+
ORDER BY j.name, j.id
|
|
1643
|
+
FOR UPDATE OF j SKIP LOCKED
|
|
1644
|
+
LIMIT ${FLOW_BATCH_SIZE}
|
|
1645
|
+
),
|
|
1646
|
+
decremented AS (
|
|
1647
|
+
SELECT d.child_name, d.child_id, COUNT(*)::int AS n
|
|
1648
|
+
FROM ${schema}.job_dependency d
|
|
1649
|
+
JOIN locked_parents p ON d.parent_name = p.name
|
|
1650
|
+
AND d.parent_id = p.id
|
|
1651
|
+
GROUP BY d.child_name, d.child_id
|
|
1652
|
+
),
|
|
1653
|
+
${lockedChildrenCte(schema)},
|
|
1654
|
+
unblocked AS (
|
|
1655
|
+
${unblockChildrenUpdate(schema)}
|
|
1656
|
+
RETURNING 1
|
|
1657
|
+
),
|
|
1658
|
+
cleared AS (
|
|
1659
|
+
UPDATE ${schema}.${table} j
|
|
1660
|
+
SET blocking = false
|
|
1661
|
+
FROM locked_parents p
|
|
1662
|
+
WHERE j.name = p.name
|
|
1663
|
+
AND j.id = p.id
|
|
1664
|
+
RETURNING 1
|
|
1665
|
+
)
|
|
1666
|
+
SELECT COUNT(*)::int AS resolved FROM cleared
|
|
1667
|
+
`,
|
|
1668
|
+
values: [names]
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
// Distributed flow audit (CockroachDB / noMultiMutationCte). Locks a batch of completed blocking
|
|
1672
|
+
// parents without mutating, so the caller can run the single-mutation decrementDependents() and
|
|
1673
|
+
// clearBlocking() separately within one transaction. $1 is the chunk of queue names; SKIP LOCKED
|
|
1674
|
+
// is omitted under noSkipLocked.
|
|
1675
|
+
export function selectBlockingParents(schema, table, names, noSkipLocked) {
|
|
1676
|
+
return {
|
|
1677
|
+
text: `
|
|
1678
|
+
SELECT name, id
|
|
1679
|
+
FROM ${schema}.${table}
|
|
1680
|
+
WHERE blocking
|
|
1681
|
+
AND state = '${JOB_STATES.completed}'
|
|
1682
|
+
AND name = ANY($1::text[])
|
|
1683
|
+
ORDER BY name, id
|
|
1684
|
+
FOR UPDATE${noSkipLocked ? '' : ' SKIP LOCKED'}
|
|
1685
|
+
LIMIT ${FLOW_BATCH_SIZE}
|
|
1686
|
+
`,
|
|
1687
|
+
values: [names]
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
// Distributed flow audit: clear `blocking` on resolved parents (single mutation). $1 is the parent
|
|
1691
|
+
// queue name, $2 the list of resolved parent ids for that queue.
|
|
1692
|
+
export function clearBlocking(schema) {
|
|
1693
|
+
return `
|
|
1694
|
+
UPDATE ${schema}.job
|
|
1695
|
+
SET blocking = false
|
|
1696
|
+
WHERE name = $1
|
|
1697
|
+
AND id = ANY($2::uuid[])
|
|
1698
|
+
`;
|
|
1699
|
+
}
|
|
1618
1700
|
export function insertRetryJob(schema, table) {
|
|
1619
1701
|
return `
|
|
1620
1702
|
INSERT INTO ${schema}.${table} (
|
|
@@ -1632,11 +1714,50 @@ export function insertRetryJob(schema, table) {
|
|
|
1632
1714
|
}
|
|
1633
1715
|
export function insertDeadLetterJob(schema) {
|
|
1634
1716
|
return `
|
|
1635
|
-
INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds
|
|
1636
|
-
|
|
1717
|
+
INSERT INTO ${schema}.job (name, data, output, retry_limit, retry_backoff, retry_delay, keep_until, deletion_seconds,
|
|
1718
|
+
source_name, source_id, source_created_on, source_retry_count)
|
|
1719
|
+
SELECT $1, $2, $3, q.retry_limit, q.retry_backoff, q.retry_delay, now() + q.retention_seconds * interval '1s', q.deletion_seconds,
|
|
1720
|
+
$4, $5, $6, $7
|
|
1637
1721
|
FROM ${schema}.queue q WHERE q.name = $1
|
|
1638
1722
|
`;
|
|
1639
1723
|
}
|
|
1724
|
+
// Dead-letter redrive. Moves un-started jobs out of a dead-letter queue and
|
|
1725
|
+
// re-creates them as fresh jobs on their original source queue (or $2 destination override),
|
|
1726
|
+
// oldest-first, capped at $4. The JOIN in `candidates` only matches jobs whose destination queue
|
|
1727
|
+
// exists, so legacy/orphaned jobs (NULL source_name, no override) are never deleted — they stay
|
|
1728
|
+
// in the DLQ rather than being lost. Re-created jobs get a new id, `created` state, retry_count 0,
|
|
1729
|
+
// cleared output, NULL source_*, and the destination queue's current retry/retention/policy config.
|
|
1730
|
+
export function redriveJobs(schema, table) {
|
|
1731
|
+
return `
|
|
1732
|
+
WITH candidates AS (
|
|
1733
|
+
SELECT j.id
|
|
1734
|
+
FROM ${schema}.${table} j
|
|
1735
|
+
JOIN ${schema}.queue q ON q.name = COALESCE($2, j.source_name)
|
|
1736
|
+
WHERE j.name = $1
|
|
1737
|
+
AND j.state < '${JOB_STATES.active}'
|
|
1738
|
+
AND ($3::text IS NULL OR j.source_name = $3)
|
|
1739
|
+
ORDER BY j.created_on
|
|
1740
|
+
LIMIT $4
|
|
1741
|
+
FOR UPDATE OF j SKIP LOCKED
|
|
1742
|
+
),
|
|
1743
|
+
moved AS (
|
|
1744
|
+
DELETE FROM ${schema}.${table}
|
|
1745
|
+
WHERE id IN (SELECT id FROM candidates)
|
|
1746
|
+
RETURNING *
|
|
1747
|
+
),
|
|
1748
|
+
ins AS (
|
|
1749
|
+
INSERT INTO ${schema}.job
|
|
1750
|
+
(name, data, priority, retry_limit, retry_backoff, retry_delay, retry_delay_max,
|
|
1751
|
+
expire_seconds, keep_until, deletion_seconds, policy)
|
|
1752
|
+
SELECT COALESCE($2, m.source_name), m.data, m.priority, q.retry_limit, q.retry_backoff,
|
|
1753
|
+
q.retry_delay, q.retry_delay_max, q.expire_seconds,
|
|
1754
|
+
now() + q.retention_seconds * interval '1s', q.deletion_seconds, q.policy
|
|
1755
|
+
FROM moved m JOIN ${schema}.queue q ON q.name = COALESCE($2, m.source_name)
|
|
1756
|
+
RETURNING 1
|
|
1757
|
+
)
|
|
1758
|
+
SELECT count(*)::int AS moved FROM ins
|
|
1759
|
+
`;
|
|
1760
|
+
}
|
|
1640
1761
|
export function deletion(schema, table, queues, noAdvisoryLocks) {
|
|
1641
1762
|
const sql = `
|
|
1642
1763
|
DELETE FROM ${schema}.${table}
|
|
@@ -1657,7 +1778,7 @@ export function retryJobs(schema, table) {
|
|
|
1657
1778
|
SET state = '${JOB_STATES.retry}',
|
|
1658
1779
|
retry_limit = retry_limit + 1
|
|
1659
1780
|
WHERE name = $1
|
|
1660
|
-
AND id
|
|
1781
|
+
AND id = ANY($2::uuid[])
|
|
1661
1782
|
AND state = '${JOB_STATES.failed}'
|
|
1662
1783
|
RETURNING 1
|
|
1663
1784
|
)
|