@tstdl/base 0.93.142 → 0.93.143
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/package.json +1 -1
- package/task-queue/postgres/drizzle/{0000_great_gwen_stacy.sql → 0000_faithful_daimon_hellstrom.sql} +2 -2
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +5 -5
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/task-queue.js +46 -47
- package/task-queue/postgres/task.model.d.ts +1 -1
- package/task-queue/postgres/task.model.js +2 -2
- package/task-queue/task-queue.d.ts +5 -5
- package/task-queue/tests/coverage-branch.test.js +2 -2
- package/task-queue/tests/dag.test.js +8 -8
- package/task-queue/tests/dependencies.test.js +10 -10
- package/task-queue/tests/fan-out-spawning.test.js +3 -3
- package/task-queue/tests/queue.test.js +4 -4
- package/task-queue/tests/zombie-parent.test.js +3 -3
package/package.json
CHANGED
package/task-queue/postgres/drizzle/{0000_great_gwen_stacy.sql → 0000_faithful_daimon_hellstrom.sql}
RENAMED
|
@@ -9,7 +9,7 @@ CREATE TABLE "task_queue"."task" (
|
|
|
9
9
|
"trace_id" text,
|
|
10
10
|
"parent_id" uuid,
|
|
11
11
|
"tags" text[] NOT NULL,
|
|
12
|
-
"
|
|
12
|
+
"abort_on_dependency_failure" boolean NOT NULL,
|
|
13
13
|
"priority" integer NOT NULL,
|
|
14
14
|
"unresolved_schedule_dependencies" integer NOT NULL,
|
|
15
15
|
"unresolved_complete_dependencies" integer NOT NULL,
|
|
@@ -39,7 +39,7 @@ CREATE TABLE "task_queue"."task_archive" (
|
|
|
39
39
|
"trace_id" text,
|
|
40
40
|
"parent_id" uuid,
|
|
41
41
|
"tags" text[] NOT NULL,
|
|
42
|
-
"
|
|
42
|
+
"abort_on_dependency_failure" boolean NOT NULL,
|
|
43
43
|
"priority" integer NOT NULL,
|
|
44
44
|
"unresolved_schedule_dependencies" integer NOT NULL,
|
|
45
45
|
"unresolved_complete_dependencies" integer NOT NULL,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "f8a5fa74-ffdf-4135-a006-8c8f958ddefa",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"primaryKey": false,
|
|
59
59
|
"notNull": true
|
|
60
60
|
},
|
|
61
|
-
"
|
|
62
|
-
"name": "
|
|
61
|
+
"abort_on_dependency_failure": {
|
|
62
|
+
"name": "abort_on_dependency_failure",
|
|
63
63
|
"type": "boolean",
|
|
64
64
|
"primaryKey": false,
|
|
65
65
|
"notNull": true
|
|
@@ -463,8 +463,8 @@
|
|
|
463
463
|
"primaryKey": false,
|
|
464
464
|
"notNull": true
|
|
465
465
|
},
|
|
466
|
-
"
|
|
467
|
-
"name": "
|
|
466
|
+
"abort_on_dependency_failure": {
|
|
467
|
+
"name": "abort_on_dependency_failure",
|
|
468
468
|
"type": "boolean",
|
|
469
469
|
"primaryKey": false,
|
|
470
470
|
"notNull": true
|
|
@@ -71,7 +71,7 @@ import { distinct, toArray } from '../../utils/array/array.js';
|
|
|
71
71
|
import { currentTimestamp } from '../../utils/date-time.js';
|
|
72
72
|
import { Timer } from '../../utils/timer.js';
|
|
73
73
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
74
|
-
import { isDefined, isNotNull, isNull, isNumber, isString, isUndefined } from '../../utils/type-guards.js';
|
|
74
|
+
import { isArray, isDefined, isNotNull, isNull, isNumber, isString, isUndefined } from '../../utils/type-guards.js';
|
|
75
75
|
import { millisecondsPerMinute, millisecondsPerSecond } from '../../utils/units.js';
|
|
76
76
|
import { defaultQueueConfig, TaskDependencyType, TaskQueue, TaskStatus } from '../task-queue.js';
|
|
77
77
|
import { PostgresTaskQueueModuleConfig } from './module.js';
|
|
@@ -122,7 +122,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
122
122
|
tags: sql `excluded.tags`,
|
|
123
123
|
unresolvedScheduleDependencies: taskTable.unresolvedScheduleDependencies,
|
|
124
124
|
unresolvedCompleteDependencies: taskTable.unresolvedCompleteDependencies,
|
|
125
|
-
|
|
125
|
+
abortOnDependencyFailure: sql `excluded.abort_on_dependency_failure`,
|
|
126
126
|
tries: 0,
|
|
127
127
|
creationTimestamp: TRANSACTION_TIMESTAMP,
|
|
128
128
|
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
@@ -157,41 +157,39 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
157
157
|
scheduleAfter: isDefined(item.scheduleAfter) ? Array.from(new Map(item.scheduleAfter.map((s) => [isNumber(s) || isString(s) ? s : JSON.stringify(s), s])).values()) : undefined,
|
|
158
158
|
completeAfter: isDefined(item.completeAfter) ? Array.from(new Map(item.completeAfter.map((s) => [isNumber(s) || isString(s) ? s : JSON.stringify(s), s])).values()) : undefined,
|
|
159
159
|
}));
|
|
160
|
-
const entitiesWithIndex = itemsWithDistinctDependencies.map((item, index) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
});
|
|
160
|
+
const entitiesWithIndex = itemsWithDistinctDependencies.map((item, index) => ({
|
|
161
|
+
index,
|
|
162
|
+
entity: {
|
|
163
|
+
namespace: this.#namespace,
|
|
164
|
+
type: item.type,
|
|
165
|
+
status: TaskStatus.Pending,
|
|
166
|
+
token: null,
|
|
167
|
+
priority: item.priority ?? 1000,
|
|
168
|
+
idempotencyKey: item.idempotencyKey ?? null,
|
|
169
|
+
traceId: null,
|
|
170
|
+
tags: item.tags ?? [],
|
|
171
|
+
unresolvedScheduleDependencies: 0,
|
|
172
|
+
unresolvedCompleteDependencies: 0,
|
|
173
|
+
abortOnDependencyFailure: item.abortOnDependencyFailure ?? false,
|
|
174
|
+
parentId: item.parentId ?? null,
|
|
175
|
+
tries: 0,
|
|
176
|
+
progress: 0,
|
|
177
|
+
creationTimestamp: TRANSACTION_TIMESTAMP,
|
|
178
|
+
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
179
|
+
scheduleTimestamp: item.scheduleTimestamp ?? TRANSACTION_TIMESTAMP,
|
|
180
|
+
startTimestamp: null,
|
|
181
|
+
timeToLive: item.timeToLive ?? sql `${TRANSACTION_TIMESTAMP} + ${interval(this.defaultTimeToLive, 'milliseconds')}`,
|
|
182
|
+
visibilityDeadline: null,
|
|
183
|
+
completeTimestamp: null,
|
|
184
|
+
data: item.data,
|
|
185
|
+
state: null,
|
|
186
|
+
result: null,
|
|
187
|
+
error: null,
|
|
188
|
+
},
|
|
189
|
+
}));
|
|
192
190
|
const itemsWithIdempotency = entitiesWithIndex.filter((e) => isNotNull(e.entity.idempotencyKey));
|
|
193
191
|
const itemsWithoutIdempotency = entitiesWithIndex.filter((e) => isNull(e.entity.idempotencyKey));
|
|
194
|
-
const hasDependencies = itemsWithDistinctDependencies.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.
|
|
192
|
+
const hasDependencies = itemsWithDistinctDependencies.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.blockParent != false) && !(isArray(item.blockParent) && (item.blockParent.length == 0))));
|
|
195
193
|
const mustUseTransaction = (entitiesWithIndex.length > 1) || hasDependencies;
|
|
196
194
|
const newTransaction = __addDisposableResource(env_1, (mustUseTransaction && isUndefined(options?.transaction)) ? await this.#repository.startTransaction() : undefined, true);
|
|
197
195
|
const transaction = newTransaction ?? options?.transaction;
|
|
@@ -260,12 +258,14 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
260
258
|
if (!processedTaskIds.has(task.id)) {
|
|
261
259
|
continue;
|
|
262
260
|
}
|
|
263
|
-
if (isDefined(item.parentId) && (item.
|
|
261
|
+
if (isDefined(item.parentId) && (item.blockParent != false) && !(isArray(item.blockParent) && (item.blockParent.length == 0))) {
|
|
264
262
|
dependencies.push({
|
|
265
263
|
taskId: item.parentId,
|
|
266
264
|
dependencyTaskId: task.id,
|
|
267
265
|
type: TaskDependencyType.Child,
|
|
268
|
-
requiredStatuses:
|
|
266
|
+
requiredStatuses: isArray(item.blockParent)
|
|
267
|
+
? item.blockParent
|
|
268
|
+
: [TaskStatus.Completed],
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
if (isDefined(item.scheduleAfter)) {
|
|
@@ -335,7 +335,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
335
335
|
.from(taskArchiveTable)
|
|
336
336
|
.where(inArray(taskArchiveTable.id, distinctDependencyIds)));
|
|
337
337
|
if (dependencyStatuses.length > 0) {
|
|
338
|
-
await this.resolveDependenciesMany(dependencyStatuses.map((s) => ({ id: s.id, status: s.status })), { transaction
|
|
338
|
+
await this.resolveDependenciesMany(dependencyStatuses.map((s) => ({ id: s.id, status: s.status })), { transaction });
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
}
|
|
@@ -932,7 +932,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
932
932
|
await this.resolveDependenciesMany([{ id, status, namespace: options?.namespace }], options);
|
|
933
933
|
}
|
|
934
934
|
async resolveDependenciesMany(tasks, options) {
|
|
935
|
-
this.#logger.debug(`Resolving dependencies for ${tasks.length} tasks`);
|
|
936
935
|
if (tasks.length == 0) {
|
|
937
936
|
return;
|
|
938
937
|
}
|
|
@@ -953,7 +952,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
953
952
|
return;
|
|
954
953
|
}
|
|
955
954
|
const resolvedEdges = [];
|
|
956
|
-
const
|
|
955
|
+
const abortOnDependencyFailureTaskIds = new Set();
|
|
957
956
|
for (const dep of dependents) {
|
|
958
957
|
const status = taskStatusMap.get(dep.dependencyTaskId);
|
|
959
958
|
const isMatched = dep.requiredStatuses.includes(status);
|
|
@@ -961,7 +960,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
961
960
|
if (isMatched || isTerminal) {
|
|
962
961
|
resolvedEdges.push(dep);
|
|
963
962
|
if (!isMatched) {
|
|
964
|
-
|
|
963
|
+
abortOnDependencyFailureTaskIds.add(dep.taskId);
|
|
965
964
|
}
|
|
966
965
|
}
|
|
967
966
|
}
|
|
@@ -989,17 +988,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
989
988
|
`);
|
|
990
989
|
const terminalTasks = [];
|
|
991
990
|
const skippedTaskIds = new Set();
|
|
992
|
-
if (
|
|
993
|
-
const
|
|
991
|
+
if (abortOnDependencyFailureTaskIds.size > 0) {
|
|
992
|
+
const sortedAbortIds = [...abortOnDependencyFailureTaskIds].toSorted();
|
|
994
993
|
const dependentTasks = await tx.pgTransaction
|
|
995
|
-
.select({ id: taskTable.id, namespace: taskTable.namespace,
|
|
994
|
+
.select({ id: taskTable.id, namespace: taskTable.namespace, abortOnDependencyFailure: taskTable.abortOnDependencyFailure, status: taskTable.status })
|
|
996
995
|
.from(taskTable)
|
|
997
|
-
.where(inArray(taskTable.id,
|
|
996
|
+
.where(inArray(taskTable.id, sortedAbortIds))
|
|
998
997
|
.orderBy(asc(taskTable.id))
|
|
999
998
|
.for('update');
|
|
1000
999
|
const tasksToSkip = [];
|
|
1001
1000
|
for (const task of dependentTasks) {
|
|
1002
|
-
if (task.
|
|
1001
|
+
if (task.abortOnDependencyFailure && !terminalStatuses.includes(task.status)) {
|
|
1003
1002
|
tasksToSkip.push(task.id);
|
|
1004
1003
|
skippedTaskIds.add(task.id);
|
|
1005
1004
|
}
|
|
@@ -1010,7 +1009,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1010
1009
|
.set({
|
|
1011
1010
|
status: TaskStatus.Skipped,
|
|
1012
1011
|
token: null,
|
|
1013
|
-
error: jsonbBuildObject({ code: 'DependencyFailed', message: 'One or more dependencies failed and
|
|
1012
|
+
error: jsonbBuildObject({ code: 'DependencyFailed', message: 'One or more dependencies failed and abortOnDependencyFailure is enabled' }),
|
|
1014
1013
|
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
1015
1014
|
})
|
|
1016
1015
|
.where(inArray(taskTable.id, tasksToSkip))
|
|
@@ -16,7 +16,7 @@ export declare abstract class PostgresTaskBase<Data extends ObjectLiteral = Obje
|
|
|
16
16
|
traceId: string | null;
|
|
17
17
|
parentId: string | null;
|
|
18
18
|
tags: string[];
|
|
19
|
-
|
|
19
|
+
abortOnDependencyFailure: boolean;
|
|
20
20
|
priority: number;
|
|
21
21
|
unresolvedScheduleDependencies: number;
|
|
22
22
|
unresolvedCompleteDependencies: number;
|
|
@@ -26,7 +26,7 @@ export class PostgresTaskBase extends BaseEntity {
|
|
|
26
26
|
traceId;
|
|
27
27
|
parentId;
|
|
28
28
|
tags;
|
|
29
|
-
|
|
29
|
+
abortOnDependencyFailure;
|
|
30
30
|
priority;
|
|
31
31
|
unresolvedScheduleDependencies;
|
|
32
32
|
unresolvedCompleteDependencies;
|
|
@@ -76,7 +76,7 @@ __decorate([
|
|
|
76
76
|
__decorate([
|
|
77
77
|
BooleanProperty(),
|
|
78
78
|
__metadata("design:type", Boolean)
|
|
79
|
-
], PostgresTaskBase.prototype, "
|
|
79
|
+
], PostgresTaskBase.prototype, "abortOnDependencyFailure", void 0);
|
|
80
80
|
__decorate([
|
|
81
81
|
Integer(),
|
|
82
82
|
__metadata("design:type", Number)
|
|
@@ -158,8 +158,8 @@ export type Task<Definitions extends TaskDefinitionMap = Record<string, {
|
|
|
158
158
|
unresolvedScheduleDependencies: number;
|
|
159
159
|
/** The number of unresolved completion dependencies. */
|
|
160
160
|
unresolvedCompleteDependencies: number;
|
|
161
|
-
/** Whether to skip the task if any of its dependencies fail. */
|
|
162
|
-
|
|
161
|
+
/** Whether to skip the task if any of its dependencies fail (dependency finalized with a status not in `blockParent`). */
|
|
162
|
+
abortOnDependencyFailure: boolean;
|
|
163
163
|
/** The data associated with the task. */
|
|
164
164
|
data: TaskData<Definitions, Type>;
|
|
165
165
|
/** The ID of the parent task, if any. */
|
|
@@ -240,9 +240,9 @@ export type EnqueueOptions = {
|
|
|
240
240
|
*/
|
|
241
241
|
completeAfter?: TaskDependencySpecification[];
|
|
242
242
|
/** Whether to skip the task if any of its dependencies fail. */
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
|
|
243
|
+
abortOnDependencyFailure?: boolean;
|
|
244
|
+
/** The statuses the parent task should wait for this task to reach. */
|
|
245
|
+
blockParent?: boolean | TaskStatus[];
|
|
246
246
|
/** The timestamp when the task should be processed. */
|
|
247
247
|
scheduleTimestamp?: number;
|
|
248
248
|
/** The duration (ms) before the task is considered expired. */
|
|
@@ -305,9 +305,9 @@ describe('Task Queue Branch Coverage Enhancement', () => {
|
|
|
305
305
|
const updated = await pruneQueue.getTask(task.id);
|
|
306
306
|
expect(updated?.status).toBe(TaskStatus.TimedOut);
|
|
307
307
|
});
|
|
308
|
-
it('should handle enqueueMany with parentId and
|
|
308
|
+
it('should handle enqueueMany with parentId and blockParent false', async () => {
|
|
309
309
|
const parent = await queue.enqueue('p', {});
|
|
310
|
-
const tasks = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id,
|
|
310
|
+
const tasks = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, blockParent: false }], { returnTasks: true });
|
|
311
311
|
expect(tasks).toHaveLength(1);
|
|
312
312
|
expect(tasks[0]?.parentId).toBe(parent.id);
|
|
313
313
|
const updatedParent = await queue.getTask(parent.id);
|
|
@@ -64,14 +64,14 @@ describe('Extensive Task Queue Dependency Tests', () => {
|
|
|
64
64
|
await completeTask('E');
|
|
65
65
|
await queue.waitForTasks([taskF.id], { statuses: [TaskStatus.Pending] });
|
|
66
66
|
});
|
|
67
|
-
it('should respect
|
|
67
|
+
it('should respect abortOnDependencyFailure = false (continue other branches)', async () => {
|
|
68
68
|
const taskA = await queue.enqueue('A', {});
|
|
69
|
-
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id],
|
|
69
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], abortOnDependencyFailure: false });
|
|
70
70
|
const taskC = await queue.enqueue('C', {}); // Independent
|
|
71
71
|
const dA = await queue.dequeue({ types: ['A'] });
|
|
72
72
|
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
73
73
|
// taskB should stay Waiting (default requiredStatus is Completed)
|
|
74
|
-
// If
|
|
74
|
+
// If abortOnDependencyFailure is false, it should transition to Pending once the dependency is terminal, even if it failed.
|
|
75
75
|
await timeout(100);
|
|
76
76
|
const uB = await queue.getTask(taskB.id);
|
|
77
77
|
expect(uB?.status).toBe(TaskStatus.Pending);
|
|
@@ -163,12 +163,12 @@ describe('Extensive Task Queue Dependency Tests', () => {
|
|
|
163
163
|
await completeTask('D2');
|
|
164
164
|
await queue.waitForTasks([taskE.id], { statuses: [TaskStatus.Pending] });
|
|
165
165
|
});
|
|
166
|
-
it('should handle
|
|
166
|
+
it('should handle abort cascade', async () => {
|
|
167
167
|
const taskA = await queue.enqueue('A', {});
|
|
168
|
-
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id],
|
|
169
|
-
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id],
|
|
170
|
-
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskA.id],
|
|
171
|
-
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id],
|
|
168
|
+
const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], abortOnDependencyFailure: true });
|
|
169
|
+
const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id], abortOnDependencyFailure: true });
|
|
170
|
+
const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskA.id], abortOnDependencyFailure: true });
|
|
171
|
+
const taskE = await queue.enqueue('E', {}, { scheduleAfter: [taskC.id, taskD.id], abortOnDependencyFailure: true });
|
|
172
172
|
const dA = await queue.dequeue({ types: ['A'] });
|
|
173
173
|
await queue.fail(dA, new Error('fatal'), { fatal: true });
|
|
174
174
|
await queue.waitForTasks([taskB.id, taskC.id, taskD.id, taskE.id], { statuses: [TaskStatus.Skipped] });
|
|
@@ -61,11 +61,11 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
61
61
|
const d2 = await queue.dequeue({ types: ['dependent'] });
|
|
62
62
|
expect(d2?.id).toBe(dependent.id);
|
|
63
63
|
});
|
|
64
|
-
it('should
|
|
64
|
+
it('should abort if dependency fails', async () => {
|
|
65
65
|
const prereq = await queue.enqueue('prereq', {});
|
|
66
66
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
67
67
|
scheduleAfter: [prereq.id],
|
|
68
|
-
|
|
68
|
+
abortOnDependencyFailure: true,
|
|
69
69
|
});
|
|
70
70
|
const dequeued = await queue.dequeue({ types: ['prereq'] });
|
|
71
71
|
// Fail fatally
|
|
@@ -75,10 +75,10 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
75
75
|
expect(updatedDependent?.status).toBe(TaskStatus.Skipped);
|
|
76
76
|
expect(updatedDependent?.error?.code).toBe('DependencyFailed');
|
|
77
77
|
});
|
|
78
|
-
it('should NOT overwrite terminal states during cancellation (
|
|
78
|
+
it('should NOT overwrite terminal states during cancellation (abortOnDependencyFailure + complete)', async () => {
|
|
79
79
|
const dep = await queue.enqueue('dep', {});
|
|
80
80
|
// completeAfter allows 'main' to be Running while 'dep' is not finished
|
|
81
|
-
const main = await queue.enqueue('main', {}, { completeAfter: [dep.id],
|
|
81
|
+
const main = await queue.enqueue('main', {}, { completeAfter: [dep.id], abortOnDependencyFailure: true });
|
|
82
82
|
const runningDep = await queue.dequeue();
|
|
83
83
|
expect(runningDep?.id).toBe(dep.id);
|
|
84
84
|
const runningMain = await queue.dequeue();
|
|
@@ -110,13 +110,13 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
110
110
|
await queueA.clear();
|
|
111
111
|
await queueB.clear();
|
|
112
112
|
});
|
|
113
|
-
it('should correctly skip dependent tasks across namespaces when
|
|
113
|
+
it('should correctly skip dependent tasks across namespaces when abortOnDependencyFailure is triggered', async () => {
|
|
114
114
|
const queueProvider = injector.resolve(TaskQueueProvider);
|
|
115
115
|
const queueA = queueProvider.get(`QueueA-${crypto.randomUUID()}`);
|
|
116
116
|
const queueB = queueProvider.get(`QueueB-${crypto.randomUUID()}`);
|
|
117
117
|
const taskA = await queueA.enqueue('test', { value: 'A' });
|
|
118
118
|
const taskB = await queueB.enqueue('test', { value: 'B' }, {
|
|
119
|
-
|
|
119
|
+
abortOnDependencyFailure: true,
|
|
120
120
|
scheduleAfter: [{ id: taskA.id }],
|
|
121
121
|
});
|
|
122
122
|
const dequeuedA = await queueA.dequeue();
|
|
@@ -132,7 +132,7 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
132
132
|
const prereq = await queue.enqueue('prereq', {});
|
|
133
133
|
// Wait for prereq to be Dead or Completed
|
|
134
134
|
const dependent = await queue.enqueue('dependent', {}, {
|
|
135
|
-
scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }]
|
|
135
|
+
scheduleAfter: [{ id: prereq.id, requiredStatuses: [TaskStatus.Dead, TaskStatus.Completed] }],
|
|
136
136
|
});
|
|
137
137
|
// Fail fatally -> Dead
|
|
138
138
|
const d1 = await queue.dequeue({ types: ['prereq'] });
|
|
@@ -234,7 +234,7 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
234
234
|
});
|
|
235
235
|
it('should NOT resolve completed tasks as Cancelled when calling cancelMany (Issue 3)', async () => {
|
|
236
236
|
const taskA = await queue.enqueue('test', { name: 'A' });
|
|
237
|
-
const taskB = await queue.enqueue('test', { name: 'B' }, { scheduleAfter: [taskA.id],
|
|
237
|
+
const taskB = await queue.enqueue('test', { name: 'B' }, { scheduleAfter: [taskA.id], abortOnDependencyFailure: true });
|
|
238
238
|
const dequeuedA = await queue.dequeue();
|
|
239
239
|
await queue.complete(dequeuedA);
|
|
240
240
|
// B should have unresolvedScheduleDependencies = 0 and be Pending because A completed
|
|
@@ -243,7 +243,7 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
243
243
|
expect(freshB?.status).toBe(TaskStatus.Pending);
|
|
244
244
|
// Cancel A. Since A is Completed, it shouldn't be touched in DB.
|
|
245
245
|
// Importantly, it shouldn't trigger dependency resolution for B as 'Cancelled'.
|
|
246
|
-
// In the old code, this would have caused B to be transitioned to 'Skipped' because A was resolved as 'Cancelled' but B required 'Completed' (and
|
|
246
|
+
// In the old code, this would have caused B to be transitioned to 'Skipped' because A was resolved as 'Cancelled' but B required 'Completed' (and abortOnDependencyFailure is true).
|
|
247
247
|
await queue.cancelMany([taskA.id]);
|
|
248
248
|
const stillFreshA = await queue.getTask(taskA.id);
|
|
249
249
|
expect(stillFreshA?.status).toBe(TaskStatus.Completed);
|
|
@@ -280,7 +280,7 @@ describe('Queue Dependencies & Tree Tests', () => {
|
|
|
280
280
|
await queue.enqueueMany([
|
|
281
281
|
{ type: 'b', data: { val: 1 } },
|
|
282
282
|
{ type: 'b', data: { val: 2 } },
|
|
283
|
-
{ type: 'b', data: { val: 3 } }
|
|
283
|
+
{ type: 'b', data: { val: 3 } },
|
|
284
284
|
]);
|
|
285
285
|
const token = new CancellationToken();
|
|
286
286
|
const batchConsumer = queue.getBatchConsumer(2, token);
|
|
@@ -45,11 +45,11 @@ describe('Fan-Out Spawning', () => {
|
|
|
45
45
|
const fParent = await queue.getTask(parent.id);
|
|
46
46
|
expect(fParent?.status).toBe(TaskStatus.Completed);
|
|
47
47
|
});
|
|
48
|
-
it('should NOT transition parent to Waiting if
|
|
48
|
+
it('should NOT transition parent to Waiting if blockParent is false', async () => {
|
|
49
49
|
const parent = await queue.enqueue('parent', {});
|
|
50
50
|
const dParent = await queue.dequeue();
|
|
51
|
-
// Spawn child with
|
|
52
|
-
await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id,
|
|
51
|
+
// Spawn child with blockParent: false
|
|
52
|
+
await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, blockParent: false }], { returnTasks: true });
|
|
53
53
|
await queue.complete(dParent);
|
|
54
54
|
const uParent = await queue.getTask(parent.id);
|
|
55
55
|
expect(uParent?.status).toBe(TaskStatus.Completed); // Finished immediately
|
|
@@ -195,7 +195,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
195
195
|
const queueB = queueProvider.get(nameB);
|
|
196
196
|
const parent = await queueA.enqueue('test', { value: 'parent' });
|
|
197
197
|
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
198
|
-
await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id,
|
|
198
|
+
await queueB.enqueue('test', { value: 'child' }, { parentId: parent.id, blockParent: true });
|
|
199
199
|
const updatedParent = await queueA.getTask(parent.id);
|
|
200
200
|
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
201
201
|
await database.update(taskTable).set({ parentId: null }).where(or(eq(taskTable.namespace, nameA), eq(taskTable.namespace, nameB)));
|
|
@@ -254,7 +254,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
254
254
|
await q.enqueue('test', {}, {
|
|
255
255
|
idempotencyKey,
|
|
256
256
|
parentId: parent.id,
|
|
257
|
-
|
|
257
|
+
blockParent: true,
|
|
258
258
|
});
|
|
259
259
|
// Dequeue and complete parent
|
|
260
260
|
const dequeuedParent = await q.dequeue();
|
|
@@ -263,10 +263,10 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
263
263
|
expect(updatedParent?.status).toBe(TaskStatus.Completed);
|
|
264
264
|
await q.clear();
|
|
265
265
|
});
|
|
266
|
-
it('should increment unresolvedCompleteDependencies for children with
|
|
266
|
+
it('should increment unresolvedCompleteDependencies for children with blockParent: true (Bug 6-2)', async () => {
|
|
267
267
|
const parent = await queue.enqueue('parent', {});
|
|
268
268
|
expect(parent.unresolvedCompleteDependencies).toBe(0);
|
|
269
|
-
await queue.enqueue('child', {}, { parentId: parent.id,
|
|
269
|
+
await queue.enqueue('child', {}, { parentId: parent.id, blockParent: true });
|
|
270
270
|
const updatedParent = await queue.getTask(parent.id);
|
|
271
271
|
expect(updatedParent?.unresolvedCompleteDependencies).toBe(1);
|
|
272
272
|
});
|
|
@@ -21,11 +21,11 @@ describe('Zombie Parent Deadlock', () => {
|
|
|
21
21
|
afterAll(async () => {
|
|
22
22
|
await injector?.dispose();
|
|
23
23
|
});
|
|
24
|
-
it('should resolve parent even if child fails (
|
|
24
|
+
it('should resolve parent even if child fails (abortOnDependencyFailure: false)', async () => {
|
|
25
25
|
const parent = await queue.enqueue('parent', {});
|
|
26
26
|
const dParent = await queue.dequeue();
|
|
27
|
-
// Spawn a child that will fail. Parent has
|
|
28
|
-
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id,
|
|
27
|
+
// Spawn a child that will fail. Parent has abortOnDependencyFailure: false by default.
|
|
28
|
+
const [child] = await queue.enqueueMany([{ type: 'child', data: {}, parentId: parent.id, abortOnDependencyFailure: false }], { returnTasks: true });
|
|
29
29
|
await queue.complete(dParent);
|
|
30
30
|
// Parent should be WaitingChildren
|
|
31
31
|
const uParent = await queue.getTask(parent.id);
|