bunqueue 2.8.49 → 2.8.51
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 +62 -9
- package/dist/application/backgroundTasks.js +21 -4
- package/dist/application/cleanupTasks.js +7 -3
- package/dist/application/contextFactory.d.ts +6 -3
- package/dist/application/contextFactory.js +8 -0
- package/dist/application/dependencyCompletions.d.ts +53 -0
- package/dist/application/dependencyCompletions.js +123 -0
- package/dist/application/dependencyProcessor.d.ts +5 -0
- package/dist/application/dependencyProcessor.js +25 -13
- package/dist/application/flowFailureRecovery.d.ts +20 -0
- package/dist/application/flowFailureRecovery.js +94 -0
- package/dist/application/flowParentBackpatch.d.ts +27 -0
- package/dist/application/flowParentBackpatch.js +120 -0
- package/dist/application/operations/ack.d.ts +6 -3
- package/dist/application/operations/ack.js +36 -10
- package/dist/application/operations/ackHelpers.d.ts +5 -4
- package/dist/application/operations/ackHelpers.js +4 -10
- package/dist/application/operations/customId.d.ts +3 -0
- package/dist/application/operations/customId.js +10 -0
- package/dist/application/operations/flowPush.d.ts +9 -0
- package/dist/application/operations/flowPush.js +112 -0
- package/dist/application/operations/flowTopologyValidation.d.ts +3 -0
- package/dist/application/operations/flowTopologyValidation.js +101 -0
- package/dist/application/operations/flowValidation.d.ts +3 -0
- package/dist/application/operations/flowValidation.js +166 -0
- package/dist/application/operations/jobManagement.d.ts +3 -0
- package/dist/application/operations/jobManagement.js +10 -4
- package/dist/application/operations/push.d.ts +6 -1
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/pushInsert.d.ts +11 -2
- package/dist/application/operations/pushInsert.js +20 -8
- package/dist/application/operations/pushLocks.d.ts +1 -0
- package/dist/application/operations/pushLocks.js +5 -1
- package/dist/application/operations/queryOperations.js +7 -6
- package/dist/application/queueManager.d.ts +7 -0
- package/dist/application/queueManager.js +261 -67
- package/dist/application/types.d.ts +3 -1
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flow.d.ts +3 -6
- package/dist/client/flow.js +71 -210
- package/dist/client/flowAtomic.d.ts +9 -0
- package/dist/client/flowAtomic.js +24 -0
- package/dist/client/flowJobCoreMethods.d.ts +28 -0
- package/dist/client/flowJobCoreMethods.js +151 -0
- package/dist/client/flowJobDependencies.d.ts +7 -0
- package/dist/client/flowJobDependencies.js +64 -0
- package/dist/client/flowJobFactory.d.ts +11 -24
- package/dist/client/flowJobFactory.js +91 -360
- package/dist/client/flowJobMoveMethods.d.ts +16 -0
- package/dist/client/flowJobMoveMethods.js +112 -0
- package/dist/client/flowJobTypes.d.ts +29 -0
- package/dist/client/flowJobTypes.js +6 -0
- package/dist/client/flowLegacyPlan.d.ts +16 -0
- package/dist/client/flowLegacyPlan.js +118 -0
- package/dist/client/flowOptions.d.ts +10 -0
- package/dist/client/flowOptions.js +44 -0
- package/dist/client/flowPlan.d.ts +16 -0
- package/dist/client/flowPlan.js +101 -0
- package/dist/client/flowPush.js +4 -1
- package/dist/client/flowReader.d.ts +10 -0
- package/dist/client/flowReader.js +147 -0
- package/dist/client/flowTypes.d.ts +2 -2
- package/dist/client/jobHelpers.js +2 -0
- package/dist/client/workflow/clock.d.ts +3 -1
- package/dist/client/workflow/clock.js +18 -1
- package/dist/client/workflow/compensationChild.d.ts +30 -0
- package/dist/client/workflow/compensationChild.js +65 -0
- package/dist/client/workflow/compensationPass.d.ts +16 -0
- package/dist/client/workflow/compensationPass.js +96 -0
- package/dist/client/workflow/compensationSupport.d.ts +24 -0
- package/dist/client/workflow/compensationSupport.js +57 -0
- package/dist/client/workflow/compensator.d.ts +11 -50
- package/dist/client/workflow/compensator.js +22 -374
- package/dist/client/workflow/definitionGuard.d.ts +11 -0
- package/dist/client/workflow/definitionGuard.js +22 -0
- package/dist/client/workflow/engine.d.ts +2 -2
- package/dist/client/workflow/engine.js +8 -8
- package/dist/client/workflow/eventTypes.d.ts +44 -0
- package/dist/client/workflow/eventTypes.js +2 -0
- package/dist/client/workflow/executionTypes.d.ts +133 -0
- package/dist/client/workflow/executionTypes.js +2 -0
- package/dist/client/workflow/executor.d.ts +4 -17
- package/dist/client/workflow/executor.js +50 -208
- package/dist/client/workflow/executorLifecycle.d.ts +18 -0
- package/dist/client/workflow/executorLifecycle.js +69 -0
- package/dist/client/workflow/executorNodes.d.ts +17 -0
- package/dist/client/workflow/executorNodes.js +138 -0
- package/dist/client/workflow/identity.d.ts +2 -0
- package/dist/client/workflow/identity.js +8 -0
- package/dist/client/workflow/index.d.ts +1 -1
- package/dist/client/workflow/loops.d.ts +3 -5
- package/dist/client/workflow/loops.js +25 -23
- package/dist/client/workflow/mapRunner.d.ts +4 -0
- package/dist/client/workflow/mapRunner.js +45 -0
- package/dist/client/workflow/recovery.js +10 -3
- package/dist/client/workflow/runner.d.ts +2 -21
- package/dist/client/workflow/runner.js +39 -104
- package/dist/client/workflow/runnerTiming.d.ts +14 -0
- package/dist/client/workflow/runnerTiming.js +66 -0
- package/dist/client/workflow/stepTypes.d.ts +171 -0
- package/dist/client/workflow/stepTypes.js +3 -0
- package/dist/client/workflow/store.d.ts +17 -17
- package/dist/client/workflow/store.js +65 -100
- package/dist/client/workflow/storeExecutionCodec.d.ts +11 -0
- package/dist/client/workflow/storeExecutionCodec.js +34 -0
- package/dist/client/workflow/storeListing.d.ts +11 -0
- package/dist/client/workflow/storeListing.js +45 -0
- package/dist/client/workflow/storeMaintenance.d.ts +4 -0
- package/dist/client/workflow/storeMaintenance.js +40 -0
- package/dist/client/workflow/storeSignals.d.ts +9 -0
- package/dist/client/workflow/storeSignals.js +38 -2
- package/dist/client/workflow/subWorkflowRunner.d.ts +13 -0
- package/dist/client/workflow/subWorkflowRunner.js +40 -0
- package/dist/client/workflow/types.d.ts +4 -356
- package/dist/client/workflow/types.js +1 -3
- package/dist/client/workflow/waitFor.js +39 -26
- package/dist/client/workflow/workflow.d.ts +16 -59
- package/dist/client/workflow/workflow.js +53 -179
- package/dist/client/workflow/workflowDecisions.d.ts +11 -0
- package/dist/client/workflow/workflowDecisions.js +27 -0
- package/dist/client/workflow/workflowDefinition.d.ts +16 -0
- package/dist/client/workflow/workflowDefinition.js +123 -0
- package/dist/client/workflow/workflowIntrospection.d.ts +5 -0
- package/dist/client/workflow/workflowIntrospection.js +46 -0
- package/dist/client/workflow/workflowValidation.d.ts +44 -0
- package/dist/client/workflow/workflowValidation.js +143 -0
- package/dist/domain/types/command.d.ts +7 -1
- package/dist/domain/types/flow.d.ts +25 -0
- package/dist/domain/types/flow.js +1 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.d.ts +6 -0
- package/dist/infrastructure/persistence/dependencyCompletionSchema.js +16 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.d.ts +38 -0
- package/dist/infrastructure/persistence/dependencyCompletionStore.js +105 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -5
- package/dist/infrastructure/persistence/schema.js +57 -3
- package/dist/infrastructure/persistence/sqlite.d.ts +35 -0
- package/dist/infrastructure/persistence/sqlite.js +238 -12
- package/dist/infrastructure/persistence/sqliteBatch.js +10 -4
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts +5 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +28 -5
- package/dist/infrastructure/persistence/statements.d.ts +4 -0
- package/dist/infrastructure/persistence/statements.js +8 -2
- package/dist/infrastructure/server/handlerRoutes.js +3 -0
- package/dist/infrastructure/server/handlers/advanced.js +2 -2
- package/dist/infrastructure/server/handlers/flow.d.ts +7 -0
- package/dist/infrastructure/server/handlers/flow.js +11 -0
- package/dist/infrastructure/server/handlers/index.d.ts +1 -0
- package/dist/infrastructure/server/handlers/index.js +1 -0
- package/package.json +4 -2
|
@@ -13,8 +13,9 @@ import { WorkerManager } from './workerManager';
|
|
|
13
13
|
import { EventsManager } from './eventsManager';
|
|
14
14
|
import { createMonitoringState } from './monitoringChecks';
|
|
15
15
|
import { RWLock, withWriteLock } from '../shared/lock';
|
|
16
|
-
import { shardIndex, SHARD_COUNT } from '../shared/hash';
|
|
16
|
+
import { processingShardIndex, shardIndex, SHARD_COUNT } from '../shared/hash';
|
|
17
17
|
import { pushJob, pushJobBatch } from './operations/push';
|
|
18
|
+
import { pushFlowBatch } from './operations/flowPush';
|
|
18
19
|
import { pullJob, pullJobBatch } from './operations/pull';
|
|
19
20
|
import { ackJob, ackJobBatch, ackJobBatchWithResults, failJob } from './operations/ack';
|
|
20
21
|
import * as queueControl from './operations/queueControl';
|
|
@@ -35,6 +36,9 @@ import { ContextFactory } from './contextFactory';
|
|
|
35
36
|
import { processPendingDependencies } from './dependencyProcessor';
|
|
36
37
|
import { handleTaskError, handleTaskSuccess } from './taskErrorTracking';
|
|
37
38
|
import { DependencyResultTracker } from './dependencyResultTracker';
|
|
39
|
+
import { recoverFlowFailures } from './flowFailureRecovery';
|
|
40
|
+
import { assertFlowParentOwnership, backpatchDeclaredFlowChild, canAcceptRemovedFlowChild, flowChildFailureError, isDeclaredFlowChild, } from './flowParentBackpatch';
|
|
41
|
+
import { commitRemovedCompletion, DependencyCompletionTracker, reconcileDependencyCompletionPins, releaseDependencyCompletionPins, } from './dependencyCompletions';
|
|
38
42
|
/**
|
|
39
43
|
* QueueManager - Central coordinator
|
|
40
44
|
*/
|
|
@@ -44,15 +48,15 @@ export class QueueManager {
|
|
|
44
48
|
// Sharded data structures
|
|
45
49
|
shards = [];
|
|
46
50
|
shardLocks = [];
|
|
51
|
+
customIdLock = new RWLock();
|
|
47
52
|
processingShards = [];
|
|
48
53
|
processingLocks = [];
|
|
49
54
|
// Global indexes (bounded with LRU eviction)
|
|
50
55
|
jobIndex = new Map();
|
|
51
56
|
completedJobs;
|
|
52
57
|
completedJobsData;
|
|
53
|
-
// Bare
|
|
54
|
-
//
|
|
55
|
-
// completedJobs; entries are pruned by the dependency processor once consumed.
|
|
58
|
+
// Bare removeOnComplete evidence: bounded recent IDs plus IDs pinned by live
|
|
59
|
+
// dependency edges. It is never surfaced as completed job data or statistics.
|
|
56
60
|
depCompletions;
|
|
57
61
|
// Ids of jobs failed by the timeout sweep. A late ACK whose lock token no
|
|
58
62
|
// longer matches (the job was requeued for retry) is discarded for these,
|
|
@@ -125,7 +129,9 @@ export class QueueManager {
|
|
|
125
129
|
this.jobIndex.delete(jobId);
|
|
126
130
|
this.completedJobsData.delete(jobId);
|
|
127
131
|
});
|
|
128
|
-
this.depCompletions = new
|
|
132
|
+
this.depCompletions = new DependencyCompletionTracker(this.config.maxCompletedJobs, (jobId) => {
|
|
133
|
+
this.storage?.deleteDependencyCompletion(jobId);
|
|
134
|
+
});
|
|
129
135
|
this.timedOutJobs = new BoundedSet(this.config.maxCompletedJobs);
|
|
130
136
|
this.perQueueMetrics = new LRUMap(this.config.maxCustomIds);
|
|
131
137
|
this.jobResults = new LRUMap(this.config.maxJobResults);
|
|
@@ -162,6 +168,19 @@ export class QueueManager {
|
|
|
162
168
|
this.contextFactory = new ContextFactory(this.getContextDependencies(), this.getContextCallbacks());
|
|
163
169
|
// Load and start
|
|
164
170
|
bgTasks.recover(this.contextFactory.getBackgroundContext());
|
|
171
|
+
if (this.storage) {
|
|
172
|
+
recoverFlowFailures({
|
|
173
|
+
storage: this.storage,
|
|
174
|
+
shards: this.shards,
|
|
175
|
+
jobIndex: this.jobIndex,
|
|
176
|
+
completedJobs: this.completedJobs,
|
|
177
|
+
depCompletions: this.depCompletions,
|
|
178
|
+
maxDependencyCompletions: this.config.maxCompletedJobs,
|
|
179
|
+
dependencyResults: this.dependencyResults,
|
|
180
|
+
failedChildrenValues: this.failedChildrenValues,
|
|
181
|
+
ignoredChildrenFailures: this.ignoredChildrenFailures,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
165
184
|
this.recoveryStats = { queues: this.queueNamesCache.size, jobs: this.jobIndex.size };
|
|
166
185
|
if (this.storage) {
|
|
167
186
|
this.cronScheduler.load(this.storage.loadCronJobs());
|
|
@@ -175,6 +194,7 @@ export class QueueManager {
|
|
|
175
194
|
storage: this.storage,
|
|
176
195
|
shards: this.shards,
|
|
177
196
|
shardLocks: this.shardLocks,
|
|
197
|
+
customIdLock: this.customIdLock,
|
|
178
198
|
processingShards: this.processingShards,
|
|
179
199
|
processingLocks: this.processingLocks,
|
|
180
200
|
jobIndex: this.jobIndex,
|
|
@@ -208,6 +228,7 @@ export class QueueManager {
|
|
|
208
228
|
registerQueueName: this.registerQueueName.bind(this),
|
|
209
229
|
unregisterQueueName: this.unregisterQueueName.bind(this),
|
|
210
230
|
onJobCompleted: this.onJobCompleted.bind(this),
|
|
231
|
+
onJobFailed: this.onJobFailed.bind(this),
|
|
211
232
|
onJobsCompleted: this.onJobsCompleted.bind(this),
|
|
212
233
|
hasPendingDeps: this.hasPendingDeps.bind(this),
|
|
213
234
|
onRepeat: this.handleRepeat.bind(this),
|
|
@@ -260,6 +281,9 @@ export class QueueManager {
|
|
|
260
281
|
this.registerQueueName(queue);
|
|
261
282
|
return pushJobBatch(queue, inputs, this.contextFactory.getPushContext());
|
|
262
283
|
}
|
|
284
|
+
async pushFlow(batch) {
|
|
285
|
+
return pushFlowBatch(batch, this.contextFactory.getPushContext());
|
|
286
|
+
}
|
|
263
287
|
async pull(queue, timeoutMs = 0, signal) {
|
|
264
288
|
return pullJob(queue, timeoutMs, this.contextFactory.getPullContext(), signal);
|
|
265
289
|
}
|
|
@@ -545,8 +569,8 @@ export class QueueManager {
|
|
|
545
569
|
ctx.storage?.markCompleted(jId, Date.now(), job.timeline);
|
|
546
570
|
}
|
|
547
571
|
else {
|
|
572
|
+
commitRemovedCompletion(job, ctx);
|
|
548
573
|
ctx.jobIndex.delete(jId);
|
|
549
|
-
ctx.storage?.deleteJob(jId);
|
|
550
574
|
}
|
|
551
575
|
if (result !== undefined)
|
|
552
576
|
ctx.dependencyResults.retain(jId, result);
|
|
@@ -665,49 +689,112 @@ export class QueueManager {
|
|
|
665
689
|
* Used by FlowProducer when creating flows where children need to reference parent.
|
|
666
690
|
*/
|
|
667
691
|
async updateJobParent(childJobId, parentJobId) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
692
|
+
if (childJobId === parentJobId)
|
|
693
|
+
throw new Error('A flow job cannot be its own parent');
|
|
694
|
+
let childJob = await this.getJob(childJobId);
|
|
671
695
|
const parentJob = await this.getJob(parentJobId);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
696
|
+
if (!childJob) {
|
|
697
|
+
if (canAcceptRemovedFlowChild(parentJob, childJobId, this.depCompletions))
|
|
698
|
+
return;
|
|
699
|
+
throw new Error(`Child job not found: ${String(childJobId)}`);
|
|
700
|
+
}
|
|
701
|
+
if (!parentJob)
|
|
702
|
+
throw new Error(`Parent job not found: ${String(parentJobId)}`);
|
|
703
|
+
assertFlowParentOwnership(childJob, parentJobId);
|
|
704
|
+
if (isDeclaredFlowChild(parentJob, childJobId)) {
|
|
705
|
+
childJob = await backpatchDeclaredFlowChild(childJob, parentJob, {
|
|
706
|
+
storage: this.storage,
|
|
707
|
+
customIdLock: this.customIdLock,
|
|
708
|
+
shards: this.shards,
|
|
709
|
+
shardLocks: this.shardLocks,
|
|
710
|
+
processingShards: this.processingShards,
|
|
711
|
+
processingLocks: this.processingLocks,
|
|
712
|
+
jobIndex: this.jobIndex,
|
|
713
|
+
completedJobsData: this.completedJobsData,
|
|
714
|
+
depCompletions: this.depCompletions,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const parentLocation = this.jobIndex.get(parentJobId);
|
|
719
|
+
if (parentLocation?.type !== 'queue') {
|
|
720
|
+
throw new Error(`Parent job ${String(parentJobId)} is not linkable`);
|
|
685
721
|
}
|
|
686
|
-
|
|
687
|
-
|
|
722
|
+
const childLocation = this.jobIndex.get(childJobId);
|
|
723
|
+
const shardIndexes = [
|
|
724
|
+
...new Set([shardIndex(childJob.queue), shardIndex(parentJob.queue)]),
|
|
725
|
+
].sort((a, b) => a - b);
|
|
726
|
+
const processingIndexes = childLocation?.type === 'processing' ? [processingShardIndex(childJobId)] : [];
|
|
727
|
+
const guards = [await this.customIdLock.acquireWrite()];
|
|
728
|
+
try {
|
|
729
|
+
for (const index of shardIndexes)
|
|
730
|
+
guards.push(await this.shardLocks[index].acquireWrite());
|
|
731
|
+
for (const index of processingIndexes) {
|
|
732
|
+
guards.push(await this.processingLocks[index].acquireWrite());
|
|
733
|
+
}
|
|
734
|
+
if (this.jobIndex.get(parentJobId)?.type !== 'queue') {
|
|
735
|
+
throw new Error(`Parent job ${String(parentJobId)} changed state while linking`);
|
|
736
|
+
}
|
|
737
|
+
const childData = {
|
|
738
|
+
...childJob.data,
|
|
739
|
+
__parentId: String(parentJobId),
|
|
740
|
+
__parentQueue: parentJob.queue,
|
|
741
|
+
};
|
|
742
|
+
const childrenIds = [...parentJob.childrenIds, childJobId];
|
|
743
|
+
const dependsOn = parentJob.dependsOn.includes(childJobId)
|
|
744
|
+
? [...parentJob.dependsOn]
|
|
745
|
+
: [...parentJob.dependsOn, childJobId];
|
|
746
|
+
const parentData = {
|
|
747
|
+
...parentJob.data,
|
|
748
|
+
__childrenIds: childrenIds.map(String),
|
|
749
|
+
};
|
|
750
|
+
const linkedChild = { ...childJob, data: childData, parentId: parentJobId };
|
|
751
|
+
const linkedParent = { ...parentJob, data: parentData, childrenIds, dependsOn };
|
|
752
|
+
const childFinished = this.completedJobs.has(childJobId) || this.depCompletions.has(childJobId);
|
|
753
|
+
const parentState = childFinished
|
|
754
|
+
? parentJob.runAt > Date.now()
|
|
755
|
+
? 'delayed'
|
|
756
|
+
: parentJob.priority > 0
|
|
757
|
+
? 'prioritized'
|
|
758
|
+
: 'waiting'
|
|
759
|
+
: 'waiting-children';
|
|
760
|
+
this.storage?.updateFlowLink(linkedChild, linkedParent, parentState);
|
|
761
|
+
childJob.parentId = parentJobId;
|
|
762
|
+
childJob.data = childData;
|
|
763
|
+
parentJob.childrenIds = childrenIds;
|
|
764
|
+
parentJob.dependsOn = dependsOn;
|
|
765
|
+
parentJob.data = parentData;
|
|
766
|
+
if (!childFinished) {
|
|
767
|
+
const shard = this.shards[shardIndex(parentJob.queue)];
|
|
768
|
+
if (!shard.waitingDeps.has(parentJobId)) {
|
|
769
|
+
const removed = shard.getQueue(parentJob.queue).remove(parentJobId);
|
|
770
|
+
if (removed)
|
|
771
|
+
shard.decrementQueued(parentJobId);
|
|
772
|
+
shard.waitingDeps.set(parentJobId, parentJob);
|
|
773
|
+
}
|
|
774
|
+
shard.registerDependencies(parentJobId, [childJobId]);
|
|
775
|
+
this.dependencyResults.registerConsumer(parentJobId, dependsOn);
|
|
776
|
+
}
|
|
688
777
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
this.storage.updateJobData(childJobId, childJob.data);
|
|
693
|
-
if (parentJob) {
|
|
694
|
-
this.storage.updateJobChildrenIds(parentJobId, parentJob.childrenIds);
|
|
778
|
+
finally {
|
|
779
|
+
for (let index = guards.length - 1; index >= 0; index--)
|
|
780
|
+
guards[index].release();
|
|
695
781
|
}
|
|
696
782
|
}
|
|
697
783
|
// Handle race condition: child may have already terminally failed
|
|
698
784
|
// before parent linkage was established (parentId was 'pending').
|
|
699
785
|
// If so, propagate failParentOnFailure now with the real parent ID.
|
|
700
786
|
const childLoc = this.jobIndex.get(childJobId);
|
|
787
|
+
const childFailureError = childLoc?.type === 'dlq'
|
|
788
|
+
? (flowChildFailureError(childJob, this.shards) ?? 'Child job failed')
|
|
789
|
+
: 'Child job failed';
|
|
701
790
|
if (childLoc?.type === 'dlq' && childJob.failParentOnFailure) {
|
|
702
|
-
this.moveParentToFailed(parentJobId, childJob,
|
|
703
|
-
// Best-effort: parent may already be in DLQ or removed
|
|
704
|
-
});
|
|
791
|
+
await this.moveParentToFailed(parentJobId, childJob, childFailureError);
|
|
705
792
|
}
|
|
706
793
|
if (childLoc?.type === 'dlq' &&
|
|
707
794
|
(childJob.removeDependencyOnFailure ||
|
|
708
795
|
childJob.ignoreDependencyOnFailure ||
|
|
709
796
|
childJob.continueParentOnFailure)) {
|
|
710
|
-
this.onChildDependencyOption(childJob,
|
|
797
|
+
await this.onChildDependencyOption(childJob, childFailureError);
|
|
711
798
|
}
|
|
712
799
|
}
|
|
713
800
|
getJobByCustomId(customId) {
|
|
@@ -803,6 +890,12 @@ export class QueueManager {
|
|
|
803
890
|
}
|
|
804
891
|
for (const cid of customIdsToDelete)
|
|
805
892
|
this.customIdMap.delete(cid);
|
|
893
|
+
// removeOnComplete jobs have no jobIndex entry, but their bounded durable
|
|
894
|
+
// dependency proofs still belong to the obliterated queue.
|
|
895
|
+
const removedCompletions = this.storage?.deleteDependencyCompletionsForQueue(queue) ?? [];
|
|
896
|
+
for (const jobId of removedCompletions)
|
|
897
|
+
this.depCompletions.delete(jobId);
|
|
898
|
+
this.reconcileCompletionPins();
|
|
806
899
|
// Per-queue cumulative counters are keyed by queue name and never expire on
|
|
807
900
|
// their own; obliterate is the documented way to reclaim ALL state for a
|
|
808
901
|
// queue, so drop its metrics entry too (prevents unbounded growth for
|
|
@@ -834,6 +927,22 @@ export class QueueManager {
|
|
|
834
927
|
unregisterQueueName(queue) {
|
|
835
928
|
this.queueNamesCache.delete(queue);
|
|
836
929
|
}
|
|
930
|
+
releaseCompletionPins(dependencyIds) {
|
|
931
|
+
releaseDependencyCompletionPins(dependencyIds, {
|
|
932
|
+
storage: this.storage,
|
|
933
|
+
shards: this.shards,
|
|
934
|
+
depCompletions: this.depCompletions,
|
|
935
|
+
maxDependencyCompletions: this.config.maxCompletedJobs,
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
reconcileCompletionPins() {
|
|
939
|
+
reconcileDependencyCompletionPins({
|
|
940
|
+
storage: this.storage,
|
|
941
|
+
shards: this.shards,
|
|
942
|
+
depCompletions: this.depCompletions,
|
|
943
|
+
maxDependencyCompletions: this.config.maxCompletedJobs,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
837
946
|
clean(queue, graceMs, state, limit) {
|
|
838
947
|
return queueControl.cleanQueue(queue, graceMs, this.contextFactory.getQueueControlContext(), state, limit);
|
|
839
948
|
}
|
|
@@ -1221,10 +1330,17 @@ export class QueueManager {
|
|
|
1221
1330
|
// leaked an entry permanently.
|
|
1222
1331
|
this.failedChildrenValues.delete(completedId);
|
|
1223
1332
|
this.ignoredChildrenFailures.delete(completedId);
|
|
1333
|
+
this.storage?.deleteFlowFailure(completedId);
|
|
1224
1334
|
this.pendingDepChecks.add(completedId);
|
|
1225
1335
|
this.scheduleDependencyFlush();
|
|
1226
1336
|
void this.checkFlowCompleted(completedId);
|
|
1227
1337
|
}
|
|
1338
|
+
/** Release failure metadata owned by a parent that reached terminal failure. */
|
|
1339
|
+
onJobFailed(failedId) {
|
|
1340
|
+
this.failedChildrenValues.delete(failedId);
|
|
1341
|
+
this.ignoredChildrenFailures.delete(failedId);
|
|
1342
|
+
this.storage?.deleteFlowFailure(failedId);
|
|
1343
|
+
}
|
|
1228
1344
|
/** Check if completing this job completes an entire flow */
|
|
1229
1345
|
async checkFlowCompleted(completedId) {
|
|
1230
1346
|
const job = await this.getJob(completedId);
|
|
@@ -1246,13 +1362,11 @@ export class QueueManager {
|
|
|
1246
1362
|
* BullMQ v5: failParentOnFailure — when a child terminally fails
|
|
1247
1363
|
* and has failParentOnFailure: true, also fail the parent job.
|
|
1248
1364
|
*/
|
|
1249
|
-
failParentOnChildFailure(childJob, error) {
|
|
1365
|
+
async failParentOnChildFailure(childJob, error) {
|
|
1250
1366
|
const parentId = childJob.parentId;
|
|
1251
1367
|
if (!parentId)
|
|
1252
1368
|
return;
|
|
1253
|
-
this.moveParentToFailed(parentId, childJob, error)
|
|
1254
|
-
// Best-effort: parent may already be in DLQ or removed
|
|
1255
|
-
});
|
|
1369
|
+
await this.moveParentToFailed(parentId, childJob, error);
|
|
1256
1370
|
}
|
|
1257
1371
|
/** Move a parent job to DLQ/failed state because a child failed */
|
|
1258
1372
|
async moveParentToFailed(parentId, childJob, error) {
|
|
@@ -1266,6 +1380,7 @@ export class QueueManager {
|
|
|
1266
1380
|
if (parentLoc.type !== 'queue')
|
|
1267
1381
|
return;
|
|
1268
1382
|
const idx = shardIndex(parentJob.queue);
|
|
1383
|
+
let releasedDependencies = [];
|
|
1269
1384
|
await withWriteLock(this.shardLocks[idx], () => {
|
|
1270
1385
|
// Re-check inside lock to prevent duplicate DLQ entries (TOCTOU guard)
|
|
1271
1386
|
if (this.jobIndex.get(parentId)?.type !== 'queue')
|
|
@@ -1273,6 +1388,7 @@ export class QueueManager {
|
|
|
1273
1388
|
const shard = this.shards[idx];
|
|
1274
1389
|
// Remove from waitingDeps if present
|
|
1275
1390
|
if (shard.waitingDeps.has(parentId)) {
|
|
1391
|
+
releasedDependencies = [...parentJob.dependsOn];
|
|
1276
1392
|
shard.waitingDeps.delete(parentId);
|
|
1277
1393
|
shard.unregisterDependencies(parentId, parentJob.dependsOn);
|
|
1278
1394
|
}
|
|
@@ -1292,7 +1408,9 @@ export class QueueManager {
|
|
|
1292
1408
|
this.jobIndex.set(parentId, { type: 'dlq', queueName: parentJob.queue });
|
|
1293
1409
|
this.storage?.saveDlqEntry(entry);
|
|
1294
1410
|
this.storage?.deleteJob(parentId);
|
|
1411
|
+
this.storage?.deleteFlowFailure(parentId, childJob.id);
|
|
1295
1412
|
});
|
|
1413
|
+
this.releaseCompletionPins(releasedDependencies);
|
|
1296
1414
|
// Parent reached a terminal (DLQ) state — release its flow-failure tracking.
|
|
1297
1415
|
this.failedChildrenValues.delete(parentId);
|
|
1298
1416
|
this.ignoredChildrenFailures.delete(parentId);
|
|
@@ -1316,19 +1434,15 @@ export class QueueManager {
|
|
|
1316
1434
|
/**
|
|
1317
1435
|
* Handle child dependency options: removeDependencyOnFailure, ignoreDependencyOnFailure, continueParentOnFailure
|
|
1318
1436
|
*/
|
|
1319
|
-
onChildDependencyOption(childJob, error) {
|
|
1437
|
+
async onChildDependencyOption(childJob, error) {
|
|
1320
1438
|
if (!childJob.parentId)
|
|
1321
1439
|
return;
|
|
1322
1440
|
if (childJob.continueParentOnFailure) {
|
|
1323
|
-
this.continueParentOnChildFailure(childJob, error)
|
|
1324
|
-
// Best-effort parent propagation; child failure is already durable.
|
|
1325
|
-
});
|
|
1441
|
+
await this.continueParentOnChildFailure(childJob, error);
|
|
1326
1442
|
}
|
|
1327
1443
|
else {
|
|
1328
1444
|
// removeDependencyOnFailure or ignoreDependencyOnFailure
|
|
1329
|
-
this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure)
|
|
1330
|
-
// Best-effort parent propagation; child failure is already durable.
|
|
1331
|
-
});
|
|
1445
|
+
await this.removeChildFromParentDeps(childJob, error, childJob.ignoreDependencyOnFailure);
|
|
1332
1446
|
}
|
|
1333
1447
|
}
|
|
1334
1448
|
/**
|
|
@@ -1351,6 +1465,21 @@ export class QueueManager {
|
|
|
1351
1465
|
existing[childKey] = error ?? 'unknown error';
|
|
1352
1466
|
this.failedChildrenValues.set(parentId, existing);
|
|
1353
1467
|
const idx = shardIndex(parentJob.queue);
|
|
1468
|
+
let releasedDependencies = [];
|
|
1469
|
+
await withWriteLock(this.shardLocks[idx], () => {
|
|
1470
|
+
if (this.jobIndex.get(parentId)?.type !== 'queue')
|
|
1471
|
+
return;
|
|
1472
|
+
const shard = this.shards[idx];
|
|
1473
|
+
const dependencies = [...parentJob.dependsOn];
|
|
1474
|
+
releasedDependencies = dependencies;
|
|
1475
|
+
shard.unregisterDependencies(parentId, dependencies);
|
|
1476
|
+
for (const dependency of dependencies) {
|
|
1477
|
+
this.dependencyResults.releaseDependency(parentId, dependency);
|
|
1478
|
+
}
|
|
1479
|
+
parentJob.dependsOn = [];
|
|
1480
|
+
this.storage?.updateFlowParentResolution(parentJob);
|
|
1481
|
+
});
|
|
1482
|
+
this.releaseCompletionPins(releasedDependencies);
|
|
1354
1483
|
await this.promoteParentAfterChildFailure(parentId, parentJob, idx);
|
|
1355
1484
|
}
|
|
1356
1485
|
/**
|
|
@@ -1365,6 +1494,7 @@ export class QueueManager {
|
|
|
1365
1494
|
timer.unref?.();
|
|
1366
1495
|
});
|
|
1367
1496
|
let promoted = false;
|
|
1497
|
+
let releasedDependencies = [];
|
|
1368
1498
|
await withWriteLock(this.shardLocks[idx], () => {
|
|
1369
1499
|
// TOCTOU guard
|
|
1370
1500
|
if (this.jobIndex.get(parentId)?.type !== 'queue')
|
|
@@ -1372,6 +1502,7 @@ export class QueueManager {
|
|
|
1372
1502
|
const shard = this.shards[idx];
|
|
1373
1503
|
// Remove from waitingDeps
|
|
1374
1504
|
if (shard.waitingDeps.has(parentId)) {
|
|
1505
|
+
releasedDependencies = [...parentJob.dependsOn];
|
|
1375
1506
|
shard.waitingDeps.delete(parentId);
|
|
1376
1507
|
shard.unregisterDependencies(parentId, parentJob.dependsOn);
|
|
1377
1508
|
}
|
|
@@ -1387,10 +1518,12 @@ export class QueueManager {
|
|
|
1387
1518
|
queue.push(parentJob);
|
|
1388
1519
|
shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
|
|
1389
1520
|
this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
|
|
1521
|
+
this.storage?.updateFlowParentResolution(parentJob);
|
|
1390
1522
|
shard.notify(parentJob.queue);
|
|
1391
1523
|
promoted = true;
|
|
1392
1524
|
}
|
|
1393
1525
|
});
|
|
1526
|
+
this.releaseCompletionPins(releasedDependencies);
|
|
1394
1527
|
if (promoted) {
|
|
1395
1528
|
this.eventsManager.broadcast({
|
|
1396
1529
|
eventType: 'waiting',
|
|
@@ -1426,6 +1559,7 @@ export class QueueManager {
|
|
|
1426
1559
|
// Remove the failed child from the parent's pending deps synchronously so
|
|
1427
1560
|
// dependency tracking stays consistent; defer only the promotion decision.
|
|
1428
1561
|
let readyToPromote = false;
|
|
1562
|
+
let releasedDependency = null;
|
|
1429
1563
|
await withWriteLock(this.shardLocks[idx], () => {
|
|
1430
1564
|
if (this.jobIndex.get(parentId)?.type !== 'queue')
|
|
1431
1565
|
return;
|
|
@@ -1438,16 +1572,22 @@ export class QueueManager {
|
|
|
1438
1572
|
if (depIndex !== -1) {
|
|
1439
1573
|
parentJob.dependsOn.splice(depIndex, 1);
|
|
1440
1574
|
shard.unregisterDependencies(parentId, [childJob.id]);
|
|
1575
|
+
releasedDependency = childJob.id;
|
|
1441
1576
|
this.dependencyResults.releaseDependency(parentId, childJob.id);
|
|
1577
|
+
this.storage?.updateFlowParentResolution(parentJob);
|
|
1442
1578
|
}
|
|
1443
1579
|
// If no more pending deps, the parent is ready to be promoted
|
|
1444
1580
|
readyToPromote =
|
|
1445
1581
|
parentJob.dependsOn.length === 0 ||
|
|
1446
1582
|
parentJob.dependsOn.every((dep) => this.completedJobs.has(dep));
|
|
1447
1583
|
});
|
|
1584
|
+
if (releasedDependency)
|
|
1585
|
+
this.releaseCompletionPins([releasedDependency]);
|
|
1448
1586
|
if (readyToPromote) {
|
|
1449
1587
|
await this.promoteParentAfterChildFailure(parentId, parentJob, idx);
|
|
1450
1588
|
}
|
|
1589
|
+
if (!storeIgnored)
|
|
1590
|
+
this.storage?.deleteFlowFailure(parentId, childJob.id);
|
|
1451
1591
|
}
|
|
1452
1592
|
/**
|
|
1453
1593
|
* Get failed children values for a parent job (populated by continueParentOnFailure).
|
|
@@ -1479,34 +1619,88 @@ export class QueueManager {
|
|
|
1479
1619
|
const parentLoc = this.jobIndex.get(parentId);
|
|
1480
1620
|
if (parentLoc?.type !== 'queue')
|
|
1481
1621
|
return false;
|
|
1482
|
-
const
|
|
1622
|
+
const childLoc = this.jobIndex.get(childJobId);
|
|
1623
|
+
if (childLoc?.type !== 'queue' && childLoc?.type !== 'processing')
|
|
1624
|
+
return false;
|
|
1625
|
+
const parentIdx = shardIndex(parentJob.queue);
|
|
1626
|
+
const shardIndexes = [...new Set([parentIdx, shardIndex(childJob.queue)])].sort((a, b) => a - b);
|
|
1627
|
+
const processingIndexes = childLoc.type === 'processing' ? [processingShardIndex(childJobId)] : [];
|
|
1628
|
+
const guards = [];
|
|
1629
|
+
let removed = false;
|
|
1483
1630
|
let promoted = false;
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1631
|
+
let releasedDependencies = [];
|
|
1632
|
+
try {
|
|
1633
|
+
for (const index of shardIndexes)
|
|
1634
|
+
guards.push(await this.shardLocks[index].acquireWrite());
|
|
1635
|
+
for (const index of processingIndexes) {
|
|
1636
|
+
guards.push(await this.processingLocks[index].acquireWrite());
|
|
1637
|
+
}
|
|
1638
|
+
if (this.jobIndex.get(parentId)?.type !== 'queue' ||
|
|
1639
|
+
this.jobIndex.get(childJobId)?.type !== childLoc.type) {
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
const shard = this.shards[parentIdx];
|
|
1643
|
+
if (!shard.waitingDeps.has(parentId) || !parentJob.dependsOn.includes(childJobId)) {
|
|
1644
|
+
return false;
|
|
1496
1645
|
}
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1646
|
+
const unresolved = parentJob.dependsOn.filter((dependency) => dependency !== childJobId &&
|
|
1647
|
+
!this.completedJobs.has(dependency) &&
|
|
1648
|
+
!this.depCompletions.has(dependency));
|
|
1649
|
+
const released = parentJob.dependsOn.filter((dependency) => !unresolved.includes(dependency));
|
|
1650
|
+
releasedDependencies = released;
|
|
1651
|
+
const childrenIds = parentJob.childrenIds.filter((childId) => childId !== childJobId);
|
|
1652
|
+
const childData = { ...childJob.data };
|
|
1653
|
+
delete childData.__parentId;
|
|
1654
|
+
delete childData.__parentQueue;
|
|
1655
|
+
const parentData = { ...parentJob.data };
|
|
1656
|
+
if (childrenIds.length > 0)
|
|
1657
|
+
parentData.__childrenIds = childrenIds.map(String);
|
|
1658
|
+
else
|
|
1659
|
+
delete parentData.__childrenIds;
|
|
1660
|
+
const runAt = unresolved.length === 0 ? Date.now() : parentJob.runAt;
|
|
1661
|
+
const parentState = unresolved.length > 0
|
|
1662
|
+
? 'waiting-children'
|
|
1663
|
+
: parentJob.priority > 0
|
|
1664
|
+
? 'prioritized'
|
|
1665
|
+
: 'waiting';
|
|
1666
|
+
const detachedChild = { ...childJob, parentId: null, data: childData };
|
|
1667
|
+
const detachedParent = {
|
|
1668
|
+
...parentJob,
|
|
1669
|
+
childrenIds,
|
|
1670
|
+
dependsOn: unresolved,
|
|
1671
|
+
data: parentData,
|
|
1672
|
+
runAt,
|
|
1673
|
+
};
|
|
1674
|
+
this.storage?.removeFlowLink(detachedChild, detachedParent, parentState);
|
|
1675
|
+
childJob.parentId = null;
|
|
1676
|
+
childJob.data = childData;
|
|
1677
|
+
parentJob.childrenIds = childrenIds;
|
|
1678
|
+
parentJob.dependsOn = unresolved;
|
|
1679
|
+
parentJob.data = parentData;
|
|
1680
|
+
parentJob.runAt = runAt;
|
|
1681
|
+
shard.unregisterDependencies(parentId, released);
|
|
1682
|
+
for (const dependency of released) {
|
|
1683
|
+
this.dependencyResults.releaseDependency(parentId, dependency);
|
|
1684
|
+
}
|
|
1685
|
+
if (unresolved.length === 0) {
|
|
1500
1686
|
shard.waitingDeps.delete(parentId);
|
|
1501
|
-
const now = Date.now();
|
|
1502
|
-
parentJob.runAt = now;
|
|
1503
1687
|
shard.getQueue(parentJob.queue).push(parentJob);
|
|
1504
|
-
shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue,
|
|
1505
|
-
this.jobIndex.set(parentId, {
|
|
1688
|
+
shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, runAt);
|
|
1689
|
+
this.jobIndex.set(parentId, {
|
|
1690
|
+
type: 'queue',
|
|
1691
|
+
shardIdx: parentIdx,
|
|
1692
|
+
queueName: parentJob.queue,
|
|
1693
|
+
});
|
|
1506
1694
|
shard.notify(parentJob.queue);
|
|
1507
1695
|
promoted = true;
|
|
1508
1696
|
}
|
|
1509
|
-
|
|
1697
|
+
removed = true;
|
|
1698
|
+
}
|
|
1699
|
+
finally {
|
|
1700
|
+
for (let index = guards.length - 1; index >= 0; index--)
|
|
1701
|
+
guards[index].release();
|
|
1702
|
+
}
|
|
1703
|
+
this.releaseCompletionPins(releasedDependencies);
|
|
1510
1704
|
if (promoted) {
|
|
1511
1705
|
this.eventsManager.broadcast({
|
|
1512
1706
|
eventType: 'waiting',
|
|
@@ -1516,7 +1710,7 @@ export class QueueManager {
|
|
|
1516
1710
|
prev: 'waiting-children',
|
|
1517
1711
|
});
|
|
1518
1712
|
}
|
|
1519
|
-
return
|
|
1713
|
+
return removed;
|
|
1520
1714
|
}
|
|
1521
1715
|
/**
|
|
1522
1716
|
* Remove all unprocessed (waiting/delayed) children of a parent job.
|
|
@@ -13,6 +13,7 @@ import type { WebhookManager } from './webhookManager';
|
|
|
13
13
|
import type { WorkerManager } from './workerManager';
|
|
14
14
|
import type { MonitoringState } from './monitoringChecks';
|
|
15
15
|
import type { DependencyResultTracker } from './dependencyResultTracker';
|
|
16
|
+
import type { DependencyCompletionTracker } from './dependencyCompletions';
|
|
16
17
|
/** Queue Manager configuration */
|
|
17
18
|
export interface QueueManagerConfig {
|
|
18
19
|
dataPath?: string;
|
|
@@ -108,7 +109,8 @@ export interface BackgroundContext extends QueueManagerState {
|
|
|
108
109
|
workerManager: WorkerManager;
|
|
109
110
|
monitoringState: MonitoringState;
|
|
110
111
|
completedJobsData: BoundedMap<JobId, Job>;
|
|
111
|
-
depCompletions?:
|
|
112
|
+
depCompletions?: DependencyCompletionTracker;
|
|
113
|
+
maxDependencyCompletions: number;
|
|
112
114
|
timedOutJobs?: BoundedSet<JobId>;
|
|
113
115
|
}
|
|
114
116
|
/** Context for stats operations */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const CLI_NETWORK_COMMANDS: readonly ("push" | "queue" | "dlq" | "job" | "cron" | "worker" | "concurrency" | "ping" | "stats" | "metrics" | "pull" | "ack" | "
|
|
1
|
+
export declare const CLI_NETWORK_COMMANDS: readonly ("push" | "fail" | "queue" | "dlq" | "job" | "cron" | "worker" | "concurrency" | "ping" | "stats" | "metrics" | "pull" | "ack" | "rate-limit" | "health" | "webhook")[];
|
|
2
2
|
/** Build the protocol command for a routed network CLI command. */
|
|
3
3
|
export declare function buildCommand(command: string, args: string[]): Promise<Record<string, unknown> | null>;
|
package/dist/client/flow.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare class FlowProducer extends EventEmitter {
|
|
|
34
34
|
private readonly embedded;
|
|
35
35
|
private readonly tcp;
|
|
36
36
|
private readonly useSharedPool;
|
|
37
|
+
private closed;
|
|
37
38
|
constructor(opts?: FlowProducerOptions);
|
|
38
39
|
/** Get push context for helper functions */
|
|
39
40
|
private get pushCtx();
|
|
@@ -70,10 +71,6 @@ export declare class FlowProducer extends EventEmitter {
|
|
|
70
71
|
getParentResults<R = unknown>(parentIds: string[]): Map<string, R>;
|
|
71
72
|
/** Build callbacks that wire flow job methods to actual management operations */
|
|
72
73
|
private buildCallbacks;
|
|
73
|
-
private
|
|
74
|
-
private
|
|
75
|
-
private buildJobNode;
|
|
76
|
-
private buildJobNodeFromTcp;
|
|
77
|
-
private addFlowNode;
|
|
78
|
-
private addTreeNode;
|
|
74
|
+
private buildPlannedNode;
|
|
75
|
+
private indexSnapshots;
|
|
79
76
|
}
|