@nicnocquee/dataqueue 1.38.0 → 1.39.0-beta.20260322125514
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/ai/docs-content.json +11 -5
- package/ai/rules/advanced.md +27 -0
- package/ai/rules/basic.md +1 -1
- package/ai/skills/dataqueue-advanced/SKILL.md +40 -1
- package/ai/skills/dataqueue-core/SKILL.md +9 -0
- package/dist/index.cjs +563 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -2
- package/dist/index.d.ts +93 -2
- package/dist/index.js +558 -62
- package/dist/index.js.map +1 -1
- package/migrations/1781200000009_add_depends_on_to_job_queue.sql +10 -0
- package/package.json +1 -1
- package/src/backends/postgres.ts +254 -29
- package/src/backends/redis-scripts.ts +100 -4
- package/src/backends/redis.ts +194 -24
- package/src/index.ts +8 -0
- package/src/job-dependencies.test.ts +129 -0
- package/src/job-dependencies.ts +140 -0
- package/src/types.ts +36 -0
package/dist/index.cjs
CHANGED
|
@@ -922,7 +922,109 @@ Recommended: Remove sslmode from the connection string when using a custom CA.
|
|
|
922
922
|
}
|
|
923
923
|
return pool;
|
|
924
924
|
};
|
|
925
|
+
|
|
926
|
+
// src/job-dependencies.ts
|
|
927
|
+
function batchDepRef(batchIndex) {
|
|
928
|
+
if (!Number.isInteger(batchIndex) || batchIndex < 0) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
`batchDepRef: expected non-negative integer index, got ${batchIndex}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
return -(batchIndex + 1);
|
|
934
|
+
}
|
|
935
|
+
function normalizeDependsOn(dep) {
|
|
936
|
+
if (!dep) return { jobIds: void 0, tags: void 0 };
|
|
937
|
+
const jobIds = dep.jobIds && dep.jobIds.length > 0 ? [...new Set(dep.jobIds)] : void 0;
|
|
938
|
+
const tags = dep.tags && dep.tags.length > 0 ? [...new Set(dep.tags)] : void 0;
|
|
939
|
+
return { jobIds, tags };
|
|
940
|
+
}
|
|
941
|
+
function resolveDependsOnJobIdsForBatch(jobIds, insertedIds) {
|
|
942
|
+
return jobIds.map((id) => {
|
|
943
|
+
if (id >= 0) return id;
|
|
944
|
+
const idx = -id - 1;
|
|
945
|
+
if (idx < 0 || idx >= insertedIds.length) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
`Invalid batch-relative job id ${id}: index ${idx} out of range for ${insertedIds.length} inserted job(s)`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
return insertedIds[idx];
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function tagsAreSuperset(holderTags, requiredTags) {
|
|
954
|
+
if (!requiredTags || requiredTags.length === 0) return false;
|
|
955
|
+
if (!holderTags || holderTags.length === 0) return false;
|
|
956
|
+
const set = new Set(holderTags);
|
|
957
|
+
for (const t of requiredTags) {
|
|
958
|
+
if (!set.has(t)) return false;
|
|
959
|
+
}
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
async function validatePrerequisiteJobIdsExist(client, jobIds) {
|
|
963
|
+
if (jobIds.length === 0) return;
|
|
964
|
+
const r = await client.query(
|
|
965
|
+
`SELECT COUNT(*)::int AS c FROM job_queue WHERE id = ANY($1::int[])`,
|
|
966
|
+
[jobIds]
|
|
967
|
+
);
|
|
968
|
+
const c = r.rows[0]?.c ?? 0;
|
|
969
|
+
if (c !== jobIds.length) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`dependsOn.jobIds: one or more job ids do not exist (${jobIds.join(", ")})`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function assertNoDependencyCycle(client, newJobId, dependsOnJobIds) {
|
|
976
|
+
if (dependsOnJobIds.length === 0) return;
|
|
977
|
+
if (dependsOnJobIds.includes(newJobId)) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Job ${newJobId} cannot depend on itself (dependsOn.jobIds)`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
const result = await client.query(
|
|
983
|
+
`
|
|
984
|
+
WITH RECURSIVE downstream AS (
|
|
985
|
+
SELECT j.id
|
|
986
|
+
FROM job_queue j
|
|
987
|
+
WHERE j.depends_on_job_ids @> ARRAY[$1::integer]::integer[]
|
|
988
|
+
UNION
|
|
989
|
+
SELECT j.id
|
|
990
|
+
FROM job_queue j
|
|
991
|
+
INNER JOIN downstream d ON j.depends_on_job_ids @> ARRAY[d.id]::integer[]
|
|
992
|
+
)
|
|
993
|
+
SELECT 1 FROM downstream WHERE id = ANY($2::integer[]) LIMIT 1
|
|
994
|
+
`,
|
|
995
|
+
[newJobId, dependsOnJobIds]
|
|
996
|
+
);
|
|
997
|
+
if (result.rows.length > 0) {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`Adding job ${newJobId} would create a dependency cycle (dependsOn.jobIds)`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/backends/postgres.ts
|
|
925
1005
|
var MAX_TIMEOUT_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1006
|
+
var JOB_DEPENDS_ON_PREDICATE = `
|
|
1007
|
+
AND (
|
|
1008
|
+
candidate.depends_on_job_ids IS NULL
|
|
1009
|
+
OR cardinality(candidate.depends_on_job_ids) = 0
|
|
1010
|
+
OR NOT EXISTS (
|
|
1011
|
+
SELECT 1
|
|
1012
|
+
FROM unnest(candidate.depends_on_job_ids) AS dep(id)
|
|
1013
|
+
LEFT JOIN job_queue prereq ON prereq.id = dep.id
|
|
1014
|
+
WHERE prereq.id IS NULL OR prereq.status <> 'completed'
|
|
1015
|
+
)
|
|
1016
|
+
)
|
|
1017
|
+
AND (
|
|
1018
|
+
candidate.depends_on_tags IS NULL
|
|
1019
|
+
OR cardinality(candidate.depends_on_tags) = 0
|
|
1020
|
+
OR NOT EXISTS (
|
|
1021
|
+
SELECT 1 FROM job_queue blocker
|
|
1022
|
+
WHERE blocker.id <> candidate.id
|
|
1023
|
+
AND blocker.status IN ('pending', 'processing', 'waiting')
|
|
1024
|
+
AND blocker.tags IS NOT NULL
|
|
1025
|
+
AND blocker.tags @> candidate.depends_on_tags
|
|
1026
|
+
)
|
|
1027
|
+
)`;
|
|
926
1028
|
function parseTimeoutString(timeout) {
|
|
927
1029
|
const match = timeout.match(/^(\d+)(s|m|h|d)$/);
|
|
928
1030
|
if (!match) {
|
|
@@ -1029,18 +1131,35 @@ var PostgresBackend = class {
|
|
|
1029
1131
|
retryBackoff = void 0,
|
|
1030
1132
|
retryDelayMax = void 0,
|
|
1031
1133
|
deadLetterJobType = void 0,
|
|
1032
|
-
group = void 0
|
|
1134
|
+
group = void 0,
|
|
1135
|
+
dependsOn
|
|
1033
1136
|
}, options) {
|
|
1034
1137
|
const externalClient = options?.db;
|
|
1035
1138
|
const client = externalClient ?? await this.pool.connect();
|
|
1139
|
+
let manageTx = false;
|
|
1036
1140
|
try {
|
|
1141
|
+
const { jobIds: depJobIdsRaw, tags: depTags } = normalizeDependsOn(dependsOn);
|
|
1142
|
+
let resolvedDepJobIds = [];
|
|
1143
|
+
if (depJobIdsRaw?.length) {
|
|
1144
|
+
if (depJobIdsRaw.some((id) => id < 0)) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
"dependsOn.jobIds: batch-relative (negative) ids are only supported in addJobs()"
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
resolvedDepJobIds = depJobIdsRaw;
|
|
1150
|
+
await validatePrerequisiteJobIdsExist(client, resolvedDepJobIds);
|
|
1151
|
+
}
|
|
1152
|
+
const dependsOnJobIdsParam = resolvedDepJobIds.length > 0 ? resolvedDepJobIds : null;
|
|
1153
|
+
const dependsOnTagsParam = depTags?.length ? depTags : null;
|
|
1154
|
+
manageTx = resolvedDepJobIds.length > 0 && !externalClient;
|
|
1155
|
+
if (manageTx) await client.query("BEGIN");
|
|
1037
1156
|
let result;
|
|
1038
1157
|
const onConflict = idempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
1039
1158
|
if (runAt) {
|
|
1040
1159
|
result = await client.query(
|
|
1041
1160
|
`INSERT INTO job_queue
|
|
1042
|
-
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier)
|
|
1043
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
1161
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier, depends_on_job_ids, depends_on_tags)
|
|
1162
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
1044
1163
|
${onConflict}
|
|
1045
1164
|
RETURNING id`,
|
|
1046
1165
|
[
|
|
@@ -1058,14 +1177,16 @@ var PostgresBackend = class {
|
|
|
1058
1177
|
retryDelayMax ?? null,
|
|
1059
1178
|
deadLetterJobType ?? null,
|
|
1060
1179
|
group?.id ?? null,
|
|
1061
|
-
group?.tier ?? null
|
|
1180
|
+
group?.tier ?? null,
|
|
1181
|
+
dependsOnJobIdsParam,
|
|
1182
|
+
dependsOnTagsParam
|
|
1062
1183
|
]
|
|
1063
1184
|
);
|
|
1064
1185
|
} else {
|
|
1065
1186
|
result = await client.query(
|
|
1066
1187
|
`INSERT INTO job_queue
|
|
1067
|
-
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier)
|
|
1068
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
1188
|
+
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier, depends_on_job_ids, depends_on_tags)
|
|
1189
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
1069
1190
|
${onConflict}
|
|
1070
1191
|
RETURNING id`,
|
|
1071
1192
|
[
|
|
@@ -1082,11 +1203,14 @@ var PostgresBackend = class {
|
|
|
1082
1203
|
retryDelayMax ?? null,
|
|
1083
1204
|
deadLetterJobType ?? null,
|
|
1084
1205
|
group?.id ?? null,
|
|
1085
|
-
group?.tier ?? null
|
|
1206
|
+
group?.tier ?? null,
|
|
1207
|
+
dependsOnJobIdsParam,
|
|
1208
|
+
dependsOnTagsParam
|
|
1086
1209
|
]
|
|
1087
1210
|
);
|
|
1088
1211
|
}
|
|
1089
1212
|
if (result.rows.length === 0 && idempotencyKey) {
|
|
1213
|
+
if (manageTx) await client.query("ROLLBACK");
|
|
1090
1214
|
const existing = await client.query(
|
|
1091
1215
|
`SELECT id FROM job_queue WHERE idempotency_key = $1`,
|
|
1092
1216
|
[idempotencyKey]
|
|
@@ -1097,37 +1221,46 @@ var PostgresBackend = class {
|
|
|
1097
1221
|
);
|
|
1098
1222
|
return existing.rows[0].id;
|
|
1099
1223
|
}
|
|
1224
|
+
if (manageTx) await client.query("ROLLBACK");
|
|
1100
1225
|
throw new Error(
|
|
1101
1226
|
`Failed to insert job and could not find existing job with idempotency key "${idempotencyKey}"`
|
|
1102
1227
|
);
|
|
1103
1228
|
}
|
|
1104
1229
|
const jobId = result.rows[0].id;
|
|
1230
|
+
if (resolvedDepJobIds.length > 0) {
|
|
1231
|
+
await assertNoDependencyCycle(client, jobId, resolvedDepJobIds);
|
|
1232
|
+
}
|
|
1105
1233
|
log(
|
|
1106
1234
|
`Added job ${jobId}: payload ${JSON.stringify(payload)}, ${runAt ? `runAt ${runAt.toISOString()}, ` : ""}priority ${priority}, maxAttempts ${maxAttempts}, jobType ${jobType}, tags ${JSON.stringify(tags)}${idempotencyKey ? `, idempotencyKey "${idempotencyKey}"` : ""}`
|
|
1107
1235
|
);
|
|
1236
|
+
const addedMeta = {
|
|
1237
|
+
jobType,
|
|
1238
|
+
payload,
|
|
1239
|
+
tags,
|
|
1240
|
+
idempotencyKey,
|
|
1241
|
+
dependsOn: dependsOnJobIdsParam || dependsOnTagsParam ? dependsOn : void 0
|
|
1242
|
+
};
|
|
1108
1243
|
if (externalClient) {
|
|
1109
1244
|
try {
|
|
1110
1245
|
await client.query(
|
|
1111
1246
|
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
1112
|
-
[
|
|
1113
|
-
jobId,
|
|
1114
|
-
"added" /* Added */,
|
|
1115
|
-
JSON.stringify({ jobType, payload, tags, idempotencyKey })
|
|
1116
|
-
]
|
|
1247
|
+
[jobId, "added" /* Added */, JSON.stringify(addedMeta)]
|
|
1117
1248
|
);
|
|
1118
1249
|
} catch (error) {
|
|
1119
1250
|
log(`Error recording job event for job ${jobId}: ${error}`);
|
|
1120
1251
|
}
|
|
1121
1252
|
} else {
|
|
1122
|
-
await this.recordJobEvent(jobId, "added" /* Added */,
|
|
1123
|
-
jobType,
|
|
1124
|
-
payload,
|
|
1125
|
-
tags,
|
|
1126
|
-
idempotencyKey
|
|
1127
|
-
});
|
|
1253
|
+
await this.recordJobEvent(jobId, "added" /* Added */, addedMeta);
|
|
1128
1254
|
}
|
|
1255
|
+
if (manageTx) await client.query("COMMIT");
|
|
1129
1256
|
return jobId;
|
|
1130
1257
|
} catch (error) {
|
|
1258
|
+
if (manageTx) {
|
|
1259
|
+
try {
|
|
1260
|
+
await client.query("ROLLBACK");
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1131
1264
|
log(`Error adding job: ${error}`);
|
|
1132
1265
|
throw error;
|
|
1133
1266
|
} finally {
|
|
@@ -1145,7 +1278,50 @@ var PostgresBackend = class {
|
|
|
1145
1278
|
const externalClient = options?.db;
|
|
1146
1279
|
const client = externalClient ?? await this.pool.connect();
|
|
1147
1280
|
try {
|
|
1148
|
-
const
|
|
1281
|
+
const needsSequential = jobs.some((j) => {
|
|
1282
|
+
const n = normalizeDependsOn(j.dependsOn);
|
|
1283
|
+
return Boolean(n.jobIds?.length || n.tags?.length);
|
|
1284
|
+
});
|
|
1285
|
+
if (needsSequential) {
|
|
1286
|
+
const useOuterTx = !externalClient;
|
|
1287
|
+
if (useOuterTx) await client.query("BEGIN");
|
|
1288
|
+
try {
|
|
1289
|
+
const ids2 = [];
|
|
1290
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
1291
|
+
let job = jobs[i];
|
|
1292
|
+
const nd = normalizeDependsOn(job.dependsOn);
|
|
1293
|
+
if (nd.jobIds?.some((id2) => id2 < 0)) {
|
|
1294
|
+
const resolvedJobIds = resolveDependsOnJobIdsForBatch(
|
|
1295
|
+
nd.jobIds,
|
|
1296
|
+
ids2
|
|
1297
|
+
);
|
|
1298
|
+
job = {
|
|
1299
|
+
...job,
|
|
1300
|
+
dependsOn: {
|
|
1301
|
+
jobIds: resolvedJobIds,
|
|
1302
|
+
tags: job.dependsOn?.tags
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
const id = await this.addJob(job, { db: client });
|
|
1307
|
+
ids2.push(id);
|
|
1308
|
+
}
|
|
1309
|
+
if (useOuterTx) await client.query("COMMIT");
|
|
1310
|
+
log(
|
|
1311
|
+
`Batch-inserted ${jobs.length} jobs (sequential), IDs: [${ids2.join(", ")}]`
|
|
1312
|
+
);
|
|
1313
|
+
return ids2;
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
if (!externalClient) {
|
|
1316
|
+
try {
|
|
1317
|
+
await client.query("ROLLBACK");
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
throw e;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const COLS_PER_JOB = 17;
|
|
1149
1325
|
const valueClauses = [];
|
|
1150
1326
|
const params = [];
|
|
1151
1327
|
const hasAnyIdempotencyKey = jobs.some((j) => j.idempotencyKey);
|
|
@@ -1168,7 +1344,7 @@ var PostgresBackend = class {
|
|
|
1168
1344
|
} = jobs[i];
|
|
1169
1345
|
const base = i * COLS_PER_JOB;
|
|
1170
1346
|
valueClauses.push(
|
|
1171
|
-
`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, COALESCE($${base + 5}::timestamptz, CURRENT_TIMESTAMP), $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13}, $${base + 14}, $${base + 15})`
|
|
1347
|
+
`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, COALESCE($${base + 5}::timestamptz, CURRENT_TIMESTAMP), $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13}, $${base + 14}, $${base + 15}, $${base + 16}, $${base + 17})`
|
|
1172
1348
|
);
|
|
1173
1349
|
params.push(
|
|
1174
1350
|
jobType,
|
|
@@ -1185,13 +1361,15 @@ var PostgresBackend = class {
|
|
|
1185
1361
|
retryDelayMax ?? null,
|
|
1186
1362
|
deadLetterJobType ?? null,
|
|
1187
1363
|
group?.id ?? null,
|
|
1188
|
-
group?.tier ?? null
|
|
1364
|
+
group?.tier ?? null,
|
|
1365
|
+
null,
|
|
1366
|
+
null
|
|
1189
1367
|
);
|
|
1190
1368
|
}
|
|
1191
1369
|
const onConflict = hasAnyIdempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
1192
1370
|
const result = await client.query(
|
|
1193
1371
|
`INSERT INTO job_queue
|
|
1194
|
-
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier)
|
|
1372
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, dead_letter_job_type, group_id, group_tier, depends_on_job_ids, depends_on_tags)
|
|
1195
1373
|
VALUES ${valueClauses.join(", ")}
|
|
1196
1374
|
${onConflict}
|
|
1197
1375
|
RETURNING id, idempotency_key`,
|
|
@@ -1242,6 +1420,7 @@ var PostgresBackend = class {
|
|
|
1242
1420
|
const job = jobs[i];
|
|
1243
1421
|
const wasInserted = !job.idempotencyKey || !missingKeys.includes(job.idempotencyKey);
|
|
1244
1422
|
if (wasInserted) {
|
|
1423
|
+
const nd = normalizeDependsOn(job.dependsOn);
|
|
1245
1424
|
newJobEvents.push({
|
|
1246
1425
|
jobId: ids[i],
|
|
1247
1426
|
eventType: "added" /* Added */,
|
|
@@ -1249,7 +1428,8 @@ var PostgresBackend = class {
|
|
|
1249
1428
|
jobType: job.jobType,
|
|
1250
1429
|
payload: job.payload,
|
|
1251
1430
|
tags: job.tags,
|
|
1252
|
-
idempotencyKey: job.idempotencyKey
|
|
1431
|
+
idempotencyKey: job.idempotencyKey,
|
|
1432
|
+
...nd.jobIds?.length || nd.tags?.length ? { dependsOn: job.dependsOn } : {}
|
|
1253
1433
|
}
|
|
1254
1434
|
});
|
|
1255
1435
|
}
|
|
@@ -1291,7 +1471,7 @@ var PostgresBackend = class {
|
|
|
1291
1471
|
const client = await this.pool.connect();
|
|
1292
1472
|
try {
|
|
1293
1473
|
const result = await client.query(
|
|
1294
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue WHERE id = $1`,
|
|
1474
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output FROM job_queue WHERE id = $1`,
|
|
1295
1475
|
[id]
|
|
1296
1476
|
);
|
|
1297
1477
|
if (result.rows.length === 0) {
|
|
@@ -1318,7 +1498,7 @@ var PostgresBackend = class {
|
|
|
1318
1498
|
const client = await this.pool.connect();
|
|
1319
1499
|
try {
|
|
1320
1500
|
const result = await client.query(
|
|
1321
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
1501
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
1322
1502
|
[status, limit, offset]
|
|
1323
1503
|
);
|
|
1324
1504
|
log(`Found ${result.rows.length} jobs by status ${status}`);
|
|
@@ -1340,7 +1520,7 @@ var PostgresBackend = class {
|
|
|
1340
1520
|
const client = await this.pool.connect();
|
|
1341
1521
|
try {
|
|
1342
1522
|
const result = await client.query(
|
|
1343
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
1523
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
1344
1524
|
[limit, offset]
|
|
1345
1525
|
);
|
|
1346
1526
|
log(`Found ${result.rows.length} jobs (all)`);
|
|
@@ -1360,7 +1540,7 @@ var PostgresBackend = class {
|
|
|
1360
1540
|
async getJobs(filters, limit = 100, offset = 0) {
|
|
1361
1541
|
const client = await this.pool.connect();
|
|
1362
1542
|
try {
|
|
1363
|
-
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue`;
|
|
1543
|
+
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output FROM job_queue`;
|
|
1364
1544
|
const params = [];
|
|
1365
1545
|
const where = [];
|
|
1366
1546
|
let paramIdx = 1;
|
|
@@ -1461,7 +1641,7 @@ var PostgresBackend = class {
|
|
|
1461
1641
|
async getJobsByTags(tags, mode = "all", limit = 100, offset = 0) {
|
|
1462
1642
|
const client = await this.pool.connect();
|
|
1463
1643
|
try {
|
|
1464
|
-
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1644
|
+
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output
|
|
1465
1645
|
FROM job_queue`;
|
|
1466
1646
|
let params = [];
|
|
1467
1647
|
switch (mode) {
|
|
@@ -1553,11 +1733,12 @@ var PostgresBackend = class {
|
|
|
1553
1733
|
)
|
|
1554
1734
|
)
|
|
1555
1735
|
${jobTypeFilter}
|
|
1736
|
+
${JOB_DEPENDS_ON_PREDICATE}
|
|
1556
1737
|
ORDER BY candidate.priority DESC, candidate.created_at ASC
|
|
1557
1738
|
LIMIT $2
|
|
1558
1739
|
FOR UPDATE SKIP LOCKED
|
|
1559
1740
|
)
|
|
1560
|
-
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1741
|
+
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output
|
|
1561
1742
|
`,
|
|
1562
1743
|
params
|
|
1563
1744
|
);
|
|
@@ -1583,6 +1764,7 @@ var PostgresBackend = class {
|
|
|
1583
1764
|
)
|
|
1584
1765
|
)
|
|
1585
1766
|
${jobTypeFilter}
|
|
1767
|
+
${JOB_DEPENDS_ON_PREDICATE}
|
|
1586
1768
|
FOR UPDATE SKIP LOCKED
|
|
1587
1769
|
),
|
|
1588
1770
|
ranked AS (
|
|
@@ -1625,7 +1807,7 @@ var PostgresBackend = class {
|
|
|
1625
1807
|
last_retried_at = CASE WHEN status != 'waiting' AND attempts > 0 THEN NOW() ELSE last_retried_at END,
|
|
1626
1808
|
wait_until = NULL
|
|
1627
1809
|
WHERE id IN (SELECT id FROM selected)
|
|
1628
|
-
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1810
|
+
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", dead_letter_job_type AS "deadLetterJobType", dead_lettered_at AS "deadLetteredAt", dead_letter_job_id AS "deadLetterJobId", group_id AS "groupId", group_tier AS "groupTier", depends_on_job_ids AS "dependsOnJobIds", depends_on_tags AS "dependsOnTags", output
|
|
1629
1811
|
`,
|
|
1630
1812
|
constrainedParams
|
|
1631
1813
|
);
|
|
@@ -1682,6 +1864,75 @@ var PostgresBackend = class {
|
|
|
1682
1864
|
client.release();
|
|
1683
1865
|
}
|
|
1684
1866
|
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Cancel pending/waiting jobs that depend on any seed job (by job id or tag superset), transitively.
|
|
1869
|
+
*
|
|
1870
|
+
* @param client - Database client (must be inside an open transaction when used from fail/cancel).
|
|
1871
|
+
* @param initialSeeds - Job ids that just failed or were cancelled.
|
|
1872
|
+
* @param rootJobId - Original job id for event metadata.
|
|
1873
|
+
*/
|
|
1874
|
+
async propagateDependencyCancellations(client, initialSeeds, rootJobId) {
|
|
1875
|
+
const seeds = [...new Set(initialSeeds.filter((id) => id > 0))];
|
|
1876
|
+
if (seeds.length === 0) return;
|
|
1877
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
1878
|
+
const reasonJson = JSON.stringify({
|
|
1879
|
+
rootJobId,
|
|
1880
|
+
dependencyCascade: true
|
|
1881
|
+
});
|
|
1882
|
+
let frontier = seeds;
|
|
1883
|
+
while (frontier.length > 0) {
|
|
1884
|
+
const res = await client.query(
|
|
1885
|
+
`
|
|
1886
|
+
SELECT DISTINCT j.id
|
|
1887
|
+
FROM job_queue j
|
|
1888
|
+
CROSS JOIN unnest($1::int[]) AS s(id)
|
|
1889
|
+
INNER JOIN job_queue sx ON sx.id = s.id
|
|
1890
|
+
WHERE j.status IN ('pending', 'waiting')
|
|
1891
|
+
AND j.id <> sx.id
|
|
1892
|
+
AND (
|
|
1893
|
+
j.depends_on_job_ids @> ARRAY[s.id]::integer[]
|
|
1894
|
+
OR (
|
|
1895
|
+
j.depends_on_tags IS NOT NULL
|
|
1896
|
+
AND cardinality(j.depends_on_tags) > 0
|
|
1897
|
+
AND sx.tags IS NOT NULL
|
|
1898
|
+
AND sx.tags @> j.depends_on_tags
|
|
1899
|
+
)
|
|
1900
|
+
)
|
|
1901
|
+
`,
|
|
1902
|
+
[frontier]
|
|
1903
|
+
);
|
|
1904
|
+
const toCancel = [];
|
|
1905
|
+
for (const row of res.rows) {
|
|
1906
|
+
const pid = row.id;
|
|
1907
|
+
if (cancelled.has(pid)) continue;
|
|
1908
|
+
cancelled.add(pid);
|
|
1909
|
+
toCancel.push(pid);
|
|
1910
|
+
}
|
|
1911
|
+
if (toCancel.length === 0) break;
|
|
1912
|
+
await client.query(
|
|
1913
|
+
`
|
|
1914
|
+
UPDATE job_queue
|
|
1915
|
+
SET status = 'cancelled',
|
|
1916
|
+
updated_at = NOW(),
|
|
1917
|
+
last_cancelled_at = NOW(),
|
|
1918
|
+
wait_until = NULL,
|
|
1919
|
+
wait_token_id = NULL,
|
|
1920
|
+
pending_reason = $2
|
|
1921
|
+
WHERE id = ANY($1::int[])
|
|
1922
|
+
AND status IN ('pending', 'waiting')
|
|
1923
|
+
`,
|
|
1924
|
+
[toCancel, reasonJson]
|
|
1925
|
+
);
|
|
1926
|
+
const meta = JSON.stringify({ rootJobId, dependencyCascade: true });
|
|
1927
|
+
for (const jid of toCancel) {
|
|
1928
|
+
await client.query(
|
|
1929
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
1930
|
+
[jid, "cancelled" /* Cancelled */, meta]
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
frontier = toCancel;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1685
1936
|
async failJob(jobId, error, failureReason) {
|
|
1686
1937
|
const client = await this.pool.connect();
|
|
1687
1938
|
try {
|
|
@@ -1751,8 +2002,8 @@ var PostgresBackend = class {
|
|
|
1751
2002
|
});
|
|
1752
2003
|
const deadLetterInsert = await client.query(
|
|
1753
2004
|
`INSERT INTO job_queue
|
|
1754
|
-
(job_type, payload, max_attempts, priority, run_at)
|
|
1755
|
-
VALUES ($1, $2, $3, $4, NOW())
|
|
2005
|
+
(job_type, payload, max_attempts, priority, run_at, depends_on_job_ids, depends_on_tags)
|
|
2006
|
+
VALUES ($1, $2, $3, $4, NOW(), NULL, NULL)
|
|
1756
2007
|
RETURNING id`,
|
|
1757
2008
|
[failedJob.deadLetterJobType, deadLetterPayload, 1, 0]
|
|
1758
2009
|
);
|
|
@@ -1789,6 +2040,7 @@ var PostgresBackend = class {
|
|
|
1789
2040
|
})
|
|
1790
2041
|
]
|
|
1791
2042
|
);
|
|
2043
|
+
await this.propagateDependencyCancellations(client, [jobId], jobId);
|
|
1792
2044
|
await client.query("COMMIT");
|
|
1793
2045
|
log(
|
|
1794
2046
|
`Failed job ${jobId}${deadLetterJobId ? ` and routed to dead-letter job ${deadLetterJobId}` : ""}`
|
|
@@ -1884,7 +2136,8 @@ var PostgresBackend = class {
|
|
|
1884
2136
|
async cancelJob(jobId) {
|
|
1885
2137
|
const client = await this.pool.connect();
|
|
1886
2138
|
try {
|
|
1887
|
-
await client.query(
|
|
2139
|
+
await client.query("BEGIN");
|
|
2140
|
+
const upd = await client.query(
|
|
1888
2141
|
`
|
|
1889
2142
|
UPDATE job_queue
|
|
1890
2143
|
SET status = 'cancelled', updated_at = NOW(), last_cancelled_at = NOW(),
|
|
@@ -1893,9 +2146,25 @@ var PostgresBackend = class {
|
|
|
1893
2146
|
`,
|
|
1894
2147
|
[jobId]
|
|
1895
2148
|
);
|
|
1896
|
-
|
|
2149
|
+
if (upd.rowCount === 0) {
|
|
2150
|
+
await client.query("ROLLBACK");
|
|
2151
|
+
log(
|
|
2152
|
+
`Job ${jobId} could not be cancelled (not in pending/waiting state or does not exist)`
|
|
2153
|
+
);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
await client.query(
|
|
2157
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
2158
|
+
[jobId, "cancelled" /* Cancelled */, null]
|
|
2159
|
+
);
|
|
2160
|
+
await this.propagateDependencyCancellations(client, [jobId], jobId);
|
|
2161
|
+
await client.query("COMMIT");
|
|
1897
2162
|
log(`Cancelled job ${jobId}`);
|
|
1898
2163
|
} catch (error) {
|
|
2164
|
+
try {
|
|
2165
|
+
await client.query("ROLLBACK");
|
|
2166
|
+
} catch {
|
|
2167
|
+
}
|
|
1899
2168
|
log(`Error cancelling job ${jobId}: ${error}`);
|
|
1900
2169
|
throw error;
|
|
1901
2170
|
} finally {
|
|
@@ -2928,6 +3197,8 @@ local retryDelayMax = ARGV[13] -- "null" or seconds string
|
|
|
2928
3197
|
local deadLetterJobType = ARGV[14] -- "null" or jobType string
|
|
2929
3198
|
local groupId = ARGV[15] -- "null" or group ID
|
|
2930
3199
|
local groupTier = ARGV[16] -- "null" or group tier
|
|
3200
|
+
local dependsOnJobIdsJson = ARGV[17] -- "null" or JSON array of job ids
|
|
3201
|
+
local dependsOnTagsJson = ARGV[18] -- "null" or JSON array of tags
|
|
2931
3202
|
|
|
2932
3203
|
-- Idempotency check
|
|
2933
3204
|
if idempotencyKey ~= "null" then
|
|
@@ -2979,9 +3250,18 @@ redis.call('HMSET', jobKey,
|
|
|
2979
3250
|
'deadLetteredAt', 'null',
|
|
2980
3251
|
'deadLetterJobId', 'null',
|
|
2981
3252
|
'groupId', groupId,
|
|
2982
|
-
'groupTier', groupTier
|
|
3253
|
+
'groupTier', groupTier,
|
|
3254
|
+
'dependsOnJobIds', dependsOnJobIdsJson,
|
|
3255
|
+
'dependsOnTags', dependsOnTagsJson
|
|
2983
3256
|
)
|
|
2984
3257
|
|
|
3258
|
+
if dependsOnJobIdsJson ~= "null" then
|
|
3259
|
+
local depIds = cjson.decode(dependsOnJobIdsJson)
|
|
3260
|
+
for _, parentId in ipairs(depIds) do
|
|
3261
|
+
redis.call('SADD', prefix .. 'dep:' .. tostring(parentId), tostring(id))
|
|
3262
|
+
end
|
|
3263
|
+
end
|
|
3264
|
+
|
|
2985
3265
|
-- Status index
|
|
2986
3266
|
redis.call('SADD', prefix .. 'status:pending', id)
|
|
2987
3267
|
|
|
@@ -3044,6 +3324,8 @@ for i, job in ipairs(jobs) do
|
|
|
3044
3324
|
local deadLetterJobType = tostring(job.deadLetterJobType)
|
|
3045
3325
|
local groupId = tostring(job.groupId)
|
|
3046
3326
|
local groupTier = tostring(job.groupTier)
|
|
3327
|
+
local dependsOnJobIdsJson = (job.dependsOnJobIds ~= nil and job.dependsOnJobIds ~= cjson.null) and tostring(job.dependsOnJobIds) or "null"
|
|
3328
|
+
local dependsOnTagsJson = (job.dependsOnTags ~= nil and job.dependsOnTags ~= cjson.null) and tostring(job.dependsOnTags) or "null"
|
|
3047
3329
|
|
|
3048
3330
|
-- Idempotency check
|
|
3049
3331
|
local skip = false
|
|
@@ -3098,9 +3380,18 @@ for i, job in ipairs(jobs) do
|
|
|
3098
3380
|
'deadLetteredAt', 'null',
|
|
3099
3381
|
'deadLetterJobId', 'null',
|
|
3100
3382
|
'groupId', groupId,
|
|
3101
|
-
'groupTier', groupTier
|
|
3383
|
+
'groupTier', groupTier,
|
|
3384
|
+
'dependsOnJobIds', dependsOnJobIdsJson,
|
|
3385
|
+
'dependsOnTags', dependsOnTagsJson
|
|
3102
3386
|
)
|
|
3103
3387
|
|
|
3388
|
+
if dependsOnJobIdsJson ~= "null" then
|
|
3389
|
+
local depIds = cjson.decode(dependsOnJobIdsJson)
|
|
3390
|
+
for _, parentId in ipairs(depIds) do
|
|
3391
|
+
redis.call('SADD', prefix .. 'dep:' .. tostring(parentId), tostring(id))
|
|
3392
|
+
end
|
|
3393
|
+
end
|
|
3394
|
+
|
|
3104
3395
|
-- Status index
|
|
3105
3396
|
redis.call('SADD', prefix .. 'status:pending', id)
|
|
3106
3397
|
|
|
@@ -3264,6 +3555,48 @@ for i = 1, #candidates, 2 do
|
|
|
3264
3555
|
end
|
|
3265
3556
|
end
|
|
3266
3557
|
|
|
3558
|
+
if canClaim then
|
|
3559
|
+
local depIdsJson = redis.call('HGET', jk, 'dependsOnJobIds')
|
|
3560
|
+
local depTagsJson = redis.call('HGET', jk, 'dependsOnTags')
|
|
3561
|
+
local depsOk = true
|
|
3562
|
+
if depIdsJson and depIdsJson ~= 'null' then
|
|
3563
|
+
local dids = cjson.decode(depIdsJson)
|
|
3564
|
+
for _, pid in ipairs(dids) do
|
|
3565
|
+
local pst = redis.call('HGET', prefix .. 'job:' .. pid, 'status')
|
|
3566
|
+
if pst ~= 'completed' then depsOk = false break end
|
|
3567
|
+
end
|
|
3568
|
+
end
|
|
3569
|
+
if depsOk and depTagsJson and depTagsJson ~= 'null' then
|
|
3570
|
+
local req = cjson.decode(depTagsJson)
|
|
3571
|
+
if #req > 0 then
|
|
3572
|
+
for _, stname in ipairs({'pending','processing','waiting'}) do
|
|
3573
|
+
local members = redis.call('SMEMBERS', prefix .. 'status:' .. stname)
|
|
3574
|
+
for _, oid in ipairs(members) do
|
|
3575
|
+
if oid ~= jobId then
|
|
3576
|
+
local otags = redis.call('HGET', prefix .. 'job:' .. oid, 'tags')
|
|
3577
|
+
if otags and otags ~= 'null' then
|
|
3578
|
+
local oarr = cjson.decode(otags)
|
|
3579
|
+
local tagset = {}
|
|
3580
|
+
for _, t in ipairs(oarr) do tagset[t] = true end
|
|
3581
|
+
local all = true
|
|
3582
|
+
for _, rt in ipairs(req) do
|
|
3583
|
+
if not tagset[rt] then all = false break end
|
|
3584
|
+
end
|
|
3585
|
+
if all then depsOk = false break end
|
|
3586
|
+
end
|
|
3587
|
+
end
|
|
3588
|
+
end
|
|
3589
|
+
if not depsOk then break end
|
|
3590
|
+
end
|
|
3591
|
+
end
|
|
3592
|
+
end
|
|
3593
|
+
if not depsOk then
|
|
3594
|
+
table.insert(putBack, score)
|
|
3595
|
+
table.insert(putBack, jobId)
|
|
3596
|
+
canClaim = false
|
|
3597
|
+
end
|
|
3598
|
+
end
|
|
3599
|
+
|
|
3267
3600
|
if canClaim then
|
|
3268
3601
|
-- Claim this job
|
|
3269
3602
|
local attempts = tonumber(redis.call('HGET', jk, 'attempts'))
|
|
@@ -3344,6 +3677,14 @@ if groupId and groupId ~= 'null' then
|
|
|
3344
3677
|
end
|
|
3345
3678
|
end
|
|
3346
3679
|
|
|
3680
|
+
local depIdsJson = redis.call('HGET', jk, 'dependsOnJobIds')
|
|
3681
|
+
if depIdsJson and depIdsJson ~= 'null' then
|
|
3682
|
+
local dids = cjson.decode(depIdsJson)
|
|
3683
|
+
for _, pid in ipairs(dids) do
|
|
3684
|
+
redis.call('SREM', prefix .. 'dep:' .. tostring(pid), jobId)
|
|
3685
|
+
end
|
|
3686
|
+
end
|
|
3687
|
+
|
|
3347
3688
|
return 1
|
|
3348
3689
|
`;
|
|
3349
3690
|
var FAIL_JOB_SCRIPT = `
|
|
@@ -3491,7 +3832,11 @@ if nextAttemptAt == 'null' and deadLetterJobType and deadLetterJobType ~= 'null'
|
|
|
3491
3832
|
'retryDelayMax', 'null',
|
|
3492
3833
|
'deadLetterJobType', 'null',
|
|
3493
3834
|
'deadLetteredAt', 'null',
|
|
3494
|
-
'deadLetterJobId', 'null'
|
|
3835
|
+
'deadLetterJobId', 'null',
|
|
3836
|
+
'dependsOnJobIds', 'null',
|
|
3837
|
+
'dependsOnTags', 'null',
|
|
3838
|
+
'groupId', 'null',
|
|
3839
|
+
'groupTier', 'null'
|
|
3495
3840
|
)
|
|
3496
3841
|
|
|
3497
3842
|
redis.call('SADD', prefix .. 'status:pending', deadLetterJobId)
|
|
@@ -3506,6 +3851,14 @@ if nextAttemptAt == 'null' and deadLetterJobType and deadLetterJobType ~= 'null'
|
|
|
3506
3851
|
)
|
|
3507
3852
|
end
|
|
3508
3853
|
|
|
3854
|
+
local depIdsJsonFail = redis.call('HGET', jk, 'dependsOnJobIds')
|
|
3855
|
+
if depIdsJsonFail and depIdsJsonFail ~= 'null' then
|
|
3856
|
+
local dids = cjson.decode(depIdsJsonFail)
|
|
3857
|
+
for _, pid in ipairs(dids) do
|
|
3858
|
+
redis.call('SREM', prefix .. 'dep:' .. tostring(pid), jobId)
|
|
3859
|
+
end
|
|
3860
|
+
end
|
|
3861
|
+
|
|
3509
3862
|
return deadLetterJobId
|
|
3510
3863
|
`;
|
|
3511
3864
|
var RETRY_JOB_SCRIPT = `
|
|
@@ -3572,6 +3925,14 @@ redis.call('ZREM', prefix .. 'queue', jobId)
|
|
|
3572
3925
|
redis.call('ZREM', prefix .. 'delayed', jobId)
|
|
3573
3926
|
redis.call('ZREM', prefix .. 'waiting', jobId)
|
|
3574
3927
|
|
|
3928
|
+
local depIdsJsonCan = redis.call('HGET', jk, 'dependsOnJobIds')
|
|
3929
|
+
if depIdsJsonCan and depIdsJsonCan ~= 'null' then
|
|
3930
|
+
local dids = cjson.decode(depIdsJsonCan)
|
|
3931
|
+
for _, pid in ipairs(dids) do
|
|
3932
|
+
redis.call('SREM', prefix .. 'dep:' .. tostring(pid), jobId)
|
|
3933
|
+
end
|
|
3934
|
+
end
|
|
3935
|
+
|
|
3575
3936
|
return 1
|
|
3576
3937
|
`;
|
|
3577
3938
|
var PROLONG_JOB_SCRIPT = `
|
|
@@ -3923,9 +4284,29 @@ function deserializeJob(h) {
|
|
|
3923
4284
|
deadLetterJobId: numOrNull(h.deadLetterJobId),
|
|
3924
4285
|
groupId: nullish(h.groupId),
|
|
3925
4286
|
groupTier: nullish(h.groupTier),
|
|
3926
|
-
output: parseJsonField(h.output)
|
|
4287
|
+
output: parseJsonField(h.output),
|
|
4288
|
+
dependsOnJobIds: parseOptionalIntArray(h.dependsOnJobIds),
|
|
4289
|
+
dependsOnTags: parseOptionalStringArray(h.dependsOnTags)
|
|
3927
4290
|
};
|
|
3928
4291
|
}
|
|
4292
|
+
function parseOptionalIntArray(raw) {
|
|
4293
|
+
if (!raw || raw === "null") return null;
|
|
4294
|
+
try {
|
|
4295
|
+
const arr = JSON.parse(raw);
|
|
4296
|
+
return Array.isArray(arr) && arr.length > 0 ? arr : null;
|
|
4297
|
+
} catch {
|
|
4298
|
+
return null;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
function parseOptionalStringArray(raw) {
|
|
4302
|
+
if (!raw || raw === "null") return null;
|
|
4303
|
+
try {
|
|
4304
|
+
const arr = JSON.parse(raw);
|
|
4305
|
+
return Array.isArray(arr) && arr.length > 0 ? arr : null;
|
|
4306
|
+
} catch {
|
|
4307
|
+
return null;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
3929
4310
|
function parseJsonField(raw) {
|
|
3930
4311
|
if (!raw || raw === "null") return null;
|
|
3931
4312
|
try {
|
|
@@ -3992,6 +4373,61 @@ var RedisBackend = class {
|
|
|
3992
4373
|
nowMs() {
|
|
3993
4374
|
return Date.now();
|
|
3994
4375
|
}
|
|
4376
|
+
/**
|
|
4377
|
+
* Cancel pending/waiting jobs that depend on seed jobs (job id or tag), transitively.
|
|
4378
|
+
*
|
|
4379
|
+
* @param initialSeeds - Job ids that failed or were cancelled.
|
|
4380
|
+
* @param rootJobId - Root id for event metadata.
|
|
4381
|
+
*/
|
|
4382
|
+
async propagateDependencyCancellationsRedis(initialSeeds, rootJobId) {
|
|
4383
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
4384
|
+
let frontier = [...new Set(initialSeeds.filter((id) => id > 0))];
|
|
4385
|
+
while (frontier.length > 0) {
|
|
4386
|
+
const pendingRaw = await this.client.sunion(
|
|
4387
|
+
`${this.prefix}status:pending`,
|
|
4388
|
+
`${this.prefix}status:waiting`
|
|
4389
|
+
);
|
|
4390
|
+
const toCancel = [];
|
|
4391
|
+
for (const pidStr of pendingRaw) {
|
|
4392
|
+
const pid = Number(pidStr);
|
|
4393
|
+
if (cancelled.has(pid)) continue;
|
|
4394
|
+
const job = await this.getJob(pid);
|
|
4395
|
+
if (!job || job.status !== "pending" && job.status !== "waiting") {
|
|
4396
|
+
continue;
|
|
4397
|
+
}
|
|
4398
|
+
for (const seedId of frontier) {
|
|
4399
|
+
if (pid === seedId) continue;
|
|
4400
|
+
const seedJob = await this.getJob(seedId);
|
|
4401
|
+
if (!seedJob) continue;
|
|
4402
|
+
const byJobId = job.dependsOnJobIds?.includes(seedId) ?? false;
|
|
4403
|
+
const byTag = job.dependsOnTags && job.dependsOnTags.length > 0 && tagsAreSuperset(seedJob.tags, job.dependsOnTags);
|
|
4404
|
+
if (byJobId || byTag) {
|
|
4405
|
+
toCancel.push(pid);
|
|
4406
|
+
break;
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
if (toCancel.length === 0) break;
|
|
4411
|
+
const now = this.nowMs();
|
|
4412
|
+
for (const jid of toCancel) {
|
|
4413
|
+
const ok = await this.client.eval(
|
|
4414
|
+
CANCEL_JOB_SCRIPT,
|
|
4415
|
+
1,
|
|
4416
|
+
this.prefix,
|
|
4417
|
+
jid,
|
|
4418
|
+
now
|
|
4419
|
+
);
|
|
4420
|
+
if (Number(ok) === 1) {
|
|
4421
|
+
cancelled.add(jid);
|
|
4422
|
+
await this.recordJobEvent(jid, "cancelled" /* Cancelled */, {
|
|
4423
|
+
rootJobId,
|
|
4424
|
+
dependencyCascade: true
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
frontier = toCancel;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
3995
4431
|
// ── Events ──────────────────────────────────────────────────────────
|
|
3996
4432
|
async recordJobEvent(jobId, eventType, metadata) {
|
|
3997
4433
|
try {
|
|
@@ -4037,13 +4473,22 @@ var RedisBackend = class {
|
|
|
4037
4473
|
retryBackoff = void 0,
|
|
4038
4474
|
retryDelayMax = void 0,
|
|
4039
4475
|
deadLetterJobType = void 0,
|
|
4040
|
-
group = void 0
|
|
4476
|
+
group = void 0,
|
|
4477
|
+
dependsOn
|
|
4041
4478
|
}, options) {
|
|
4042
4479
|
if (options?.db) {
|
|
4043
4480
|
throw new Error(
|
|
4044
4481
|
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
4045
4482
|
);
|
|
4046
4483
|
}
|
|
4484
|
+
const { jobIds: depJobIdsRaw, tags: depTags } = normalizeDependsOn(dependsOn);
|
|
4485
|
+
if (depJobIdsRaw?.some((id) => id < 0)) {
|
|
4486
|
+
throw new Error(
|
|
4487
|
+
"dependsOn.jobIds: batch-relative (negative) ids are only supported in addJobs()"
|
|
4488
|
+
);
|
|
4489
|
+
}
|
|
4490
|
+
const dependsOnJobIdsJson = depJobIdsRaw && depJobIdsRaw.length > 0 ? JSON.stringify(depJobIdsRaw) : "null";
|
|
4491
|
+
const dependsOnTagsJson = depTags && depTags.length > 0 ? JSON.stringify(depTags) : "null";
|
|
4047
4492
|
const now = this.nowMs();
|
|
4048
4493
|
const runAtMs = runAt ? runAt.getTime() : 0;
|
|
4049
4494
|
const result = await this.client.eval(
|
|
@@ -4065,7 +4510,9 @@ var RedisBackend = class {
|
|
|
4065
4510
|
retryDelayMax !== void 0 ? retryDelayMax.toString() : "null",
|
|
4066
4511
|
deadLetterJobType ?? "null",
|
|
4067
4512
|
group?.id ?? "null",
|
|
4068
|
-
group?.tier ?? "null"
|
|
4513
|
+
group?.tier ?? "null",
|
|
4514
|
+
dependsOnJobIdsJson,
|
|
4515
|
+
dependsOnTagsJson
|
|
4069
4516
|
);
|
|
4070
4517
|
const jobId = Number(result);
|
|
4071
4518
|
log(
|
|
@@ -4075,7 +4522,8 @@ var RedisBackend = class {
|
|
|
4075
4522
|
jobType,
|
|
4076
4523
|
payload,
|
|
4077
4524
|
tags,
|
|
4078
|
-
idempotencyKey
|
|
4525
|
+
idempotencyKey,
|
|
4526
|
+
dependsOn: dependsOnJobIdsJson !== "null" || dependsOnTagsJson !== "null" ? dependsOn : void 0
|
|
4079
4527
|
});
|
|
4080
4528
|
return jobId;
|
|
4081
4529
|
}
|
|
@@ -4090,24 +4538,58 @@ var RedisBackend = class {
|
|
|
4090
4538
|
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
4091
4539
|
);
|
|
4092
4540
|
}
|
|
4541
|
+
const needsSequential = jobs.some((j) => {
|
|
4542
|
+
const n = normalizeDependsOn(j.dependsOn);
|
|
4543
|
+
return Boolean(n.jobIds?.length || n.tags?.length);
|
|
4544
|
+
});
|
|
4545
|
+
if (needsSequential) {
|
|
4546
|
+
const ids2 = [];
|
|
4547
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
4548
|
+
let job = jobs[i];
|
|
4549
|
+
const nd = normalizeDependsOn(job.dependsOn);
|
|
4550
|
+
if (nd.jobIds?.some((id) => id < 0)) {
|
|
4551
|
+
const resolvedJobIds = resolveDependsOnJobIdsForBatch(
|
|
4552
|
+
nd.jobIds,
|
|
4553
|
+
ids2
|
|
4554
|
+
);
|
|
4555
|
+
job = {
|
|
4556
|
+
...job,
|
|
4557
|
+
dependsOn: {
|
|
4558
|
+
jobIds: resolvedJobIds,
|
|
4559
|
+
tags: job.dependsOn?.tags
|
|
4560
|
+
}
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4563
|
+
ids2.push(await this.addJob(job));
|
|
4564
|
+
}
|
|
4565
|
+
log(
|
|
4566
|
+
`Batch-inserted ${jobs.length} jobs (sequential), IDs: [${ids2.join(", ")}]`
|
|
4567
|
+
);
|
|
4568
|
+
return ids2;
|
|
4569
|
+
}
|
|
4093
4570
|
const now = this.nowMs();
|
|
4094
|
-
const jobsPayload = jobs.map((job) =>
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4571
|
+
const jobsPayload = jobs.map((job) => {
|
|
4572
|
+
const nd = normalizeDependsOn(job.dependsOn);
|
|
4573
|
+
return {
|
|
4574
|
+
jobType: job.jobType,
|
|
4575
|
+
payload: JSON.stringify(job.payload),
|
|
4576
|
+
maxAttempts: job.maxAttempts ?? 3,
|
|
4577
|
+
priority: job.priority ?? 0,
|
|
4578
|
+
runAtMs: job.runAt ? job.runAt.getTime() : 0,
|
|
4579
|
+
timeoutMs: job.timeoutMs !== void 0 ? job.timeoutMs.toString() : "null",
|
|
4580
|
+
forceKillOnTimeout: job.forceKillOnTimeout ? "true" : "false",
|
|
4581
|
+
tags: job.tags ? JSON.stringify(job.tags) : "null",
|
|
4582
|
+
idempotencyKey: job.idempotencyKey ?? "null",
|
|
4583
|
+
retryDelay: job.retryDelay !== void 0 ? job.retryDelay.toString() : "null",
|
|
4584
|
+
retryBackoff: job.retryBackoff !== void 0 ? job.retryBackoff.toString() : "null",
|
|
4585
|
+
retryDelayMax: job.retryDelayMax !== void 0 ? job.retryDelayMax.toString() : "null",
|
|
4586
|
+
deadLetterJobType: job.deadLetterJobType ?? "null",
|
|
4587
|
+
groupId: job.group?.id ?? "null",
|
|
4588
|
+
groupTier: job.group?.tier ?? "null",
|
|
4589
|
+
dependsOnJobIds: nd.jobIds?.length ? JSON.stringify(nd.jobIds) : null,
|
|
4590
|
+
dependsOnTags: nd.tags?.length ? JSON.stringify(nd.tags) : null
|
|
4591
|
+
};
|
|
4592
|
+
});
|
|
4111
4593
|
const result = await this.client.eval(
|
|
4112
4594
|
ADD_JOBS_SCRIPT,
|
|
4113
4595
|
1,
|
|
@@ -4277,6 +4759,7 @@ var RedisBackend = class {
|
|
|
4277
4759
|
failureReason,
|
|
4278
4760
|
deadLetterJobId
|
|
4279
4761
|
});
|
|
4762
|
+
await this.propagateDependencyCancellationsRedis([jobId], jobId);
|
|
4280
4763
|
if (deadLetterJobId) {
|
|
4281
4764
|
const sourceJob = await this.client.hget(
|
|
4282
4765
|
`${this.prefix}job:${jobId}`,
|
|
@@ -4345,9 +4828,22 @@ var RedisBackend = class {
|
|
|
4345
4828
|
}
|
|
4346
4829
|
async cancelJob(jobId) {
|
|
4347
4830
|
const now = this.nowMs();
|
|
4348
|
-
await this.client.eval(
|
|
4349
|
-
|
|
4350
|
-
|
|
4831
|
+
const ok = await this.client.eval(
|
|
4832
|
+
CANCEL_JOB_SCRIPT,
|
|
4833
|
+
1,
|
|
4834
|
+
this.prefix,
|
|
4835
|
+
jobId,
|
|
4836
|
+
now
|
|
4837
|
+
);
|
|
4838
|
+
if (Number(ok) === 1) {
|
|
4839
|
+
await this.recordJobEvent(jobId, "cancelled" /* Cancelled */);
|
|
4840
|
+
await this.propagateDependencyCancellationsRedis([jobId], jobId);
|
|
4841
|
+
log(`Cancelled job ${jobId}`);
|
|
4842
|
+
} else {
|
|
4843
|
+
log(
|
|
4844
|
+
`Job ${jobId} could not be cancelled (not in pending/waiting state or does not exist)`
|
|
4845
|
+
);
|
|
4846
|
+
}
|
|
4351
4847
|
}
|
|
4352
4848
|
async cancelAllUpcomingJobs(filters) {
|
|
4353
4849
|
let ids = await this.client.smembers(`${this.prefix}status:pending`);
|
|
@@ -5678,10 +6174,16 @@ exports.FailureReason = FailureReason;
|
|
|
5678
6174
|
exports.JobEventType = JobEventType;
|
|
5679
6175
|
exports.PostgresBackend = PostgresBackend;
|
|
5680
6176
|
exports.WaitSignal = WaitSignal;
|
|
6177
|
+
exports.assertNoDependencyCycle = assertNoDependencyCycle;
|
|
6178
|
+
exports.batchDepRef = batchDepRef;
|
|
5681
6179
|
exports.getNextCronOccurrence = getNextCronOccurrence;
|
|
5682
6180
|
exports.initJobQueue = initJobQueue;
|
|
6181
|
+
exports.normalizeDependsOn = normalizeDependsOn;
|
|
6182
|
+
exports.resolveDependsOnJobIdsForBatch = resolveDependsOnJobIdsForBatch;
|
|
6183
|
+
exports.tagsAreSuperset = tagsAreSuperset;
|
|
5683
6184
|
exports.testHandlerSerialization = testHandlerSerialization;
|
|
5684
6185
|
exports.validateCronExpression = validateCronExpression;
|
|
5685
6186
|
exports.validateHandlerSerializable = validateHandlerSerializable2;
|
|
6187
|
+
exports.validatePrerequisiteJobIdsExist = validatePrerequisiteJobIdsExist;
|
|
5686
6188
|
//# sourceMappingURL=index.cjs.map
|
|
5687
6189
|
//# sourceMappingURL=index.cjs.map
|