bunqueue 2.8.38 → 2.8.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/backgroundTasks.js +9 -0
- package/dist/application/cleanupTasks.js +31 -11
- package/dist/application/contextFactory.d.ts +3 -0
- package/dist/application/contextFactory.js +6 -0
- package/dist/application/dependencyResultTracker.d.ts +17 -0
- package/dist/application/dependencyResultTracker.js +71 -0
- package/dist/application/operations/ack.d.ts +2 -0
- package/dist/application/operations/ack.js +6 -0
- package/dist/application/operations/ackHelpers.d.ts +2 -1
- package/dist/application/operations/ackHelpers.js +6 -0
- package/dist/application/operations/jobManagement.d.ts +2 -0
- package/dist/application/operations/jobManagement.js +10 -4
- package/dist/application/operations/jobStateTransitions.js +1 -0
- package/dist/application/operations/pull.d.ts +2 -2
- package/dist/application/operations/pull.js +43 -10
- package/dist/application/operations/push.d.ts +2 -0
- package/dist/application/operations/pushInsert.d.ts +4 -0
- package/dist/application/operations/pushInsert.js +6 -0
- package/dist/application/operations/queryOperations.d.ts +2 -0
- package/dist/application/operations/queryOperations.js +5 -1
- package/dist/application/operations/queueControl.d.ts +2 -0
- package/dist/application/operations/queueControl.js +4 -0
- package/dist/application/queueManager.d.ts +5 -4
- package/dist/application/queueManager.js +37 -11
- package/dist/application/types.d.ts +2 -0
- package/dist/cli/client.js +6 -47
- package/dist/cli/commandRegistry.d.ts +27 -0
- package/dist/cli/commandRegistry.js +40 -0
- package/dist/cli/commandRouter.d.ts +3 -0
- package/dist/cli/commandRouter.js +44 -0
- package/dist/cli/commands/backup.js +1 -1
- package/dist/cli/commands/core.js +54 -2
- package/dist/cli/commands/doctor.d.ts +50 -5
- package/dist/cli/commands/doctor.js +125 -108
- package/dist/cli/commands/types.js +5 -1
- package/dist/cli/globalOptions.d.ts +16 -0
- package/dist/cli/globalOptions.js +192 -0
- package/dist/cli/help.d.ts +9 -0
- package/dist/cli/help.js +42 -21
- package/dist/cli/index.d.ts +1 -25
- package/dist/cli/index.js +84 -348
- package/dist/cli/localOutput.d.ts +22 -0
- package/dist/cli/localOutput.js +45 -0
- package/dist/client/queue/dlqOps.js +1 -3
- package/dist/client/tcp/client.js +10 -8
- package/dist/domain/queue/shard.d.ts +2 -2
- package/dist/domain/queue/shard.js +3 -3
- package/dist/domain/queue/waiterManager.d.ts +2 -2
- package/dist/domain/queue/waiterManager.js +26 -5
- package/dist/infrastructure/cloud/wsSender.js +4 -4
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +7 -2
- package/dist/infrastructure/persistence/sqlite.d.ts +5 -1
- package/dist/infrastructure/persistence/sqlite.js +10 -2
- package/dist/infrastructure/persistence/sqliteSerializer.js +1 -1
- package/dist/infrastructure/persistence/statements.d.ts +2 -1
- package/dist/infrastructure/persistence/statements.js +3 -2
- package/dist/infrastructure/server/handlers/core.js +4 -4
- package/dist/infrastructure/server/tcp.d.ts +1 -0
- package/dist/infrastructure/server/tcp.js +23 -12
- package/dist/infrastructure/server/types.d.ts +2 -0
- package/dist/shared/msgpack.d.ts +3 -0
- package/dist/shared/msgpack.js +70 -0
- package/package.json +1 -1
|
@@ -208,6 +208,9 @@ export function recover(ctx) {
|
|
|
208
208
|
if (qs.stallConfig) {
|
|
209
209
|
ctx.shards[shardIndex(qs.name)].setStallConfig(qs.name, qs.stallConfig);
|
|
210
210
|
}
|
|
211
|
+
if (qs.dlqConfig) {
|
|
212
|
+
ctx.shards[shardIndex(qs.name)].setDlqConfig(qs.name, qs.dlqConfig);
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
215
|
const now = Date.now();
|
|
213
216
|
// === PHASE 1: Recover active jobs (were processing when server stopped) ===
|
|
@@ -324,6 +327,12 @@ export function recover(ctx) {
|
|
|
324
327
|
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
325
328
|
}
|
|
326
329
|
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
330
|
+
ctx.dependencyResults.registerConsumer(job.id, job.dependsOn);
|
|
331
|
+
for (const dependencyId of job.dependsOn) {
|
|
332
|
+
if (ctx.jobResults.has(dependencyId)) {
|
|
333
|
+
ctx.dependencyResults.retain(dependencyId, ctx.jobResults.get(dependencyId));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
327
336
|
// Restore customId mapping for deduplication (fixes idempotency on restart)
|
|
328
337
|
if (job.customId) {
|
|
329
338
|
ctx.customIdMap.set(job.customId, job.id);
|
|
@@ -24,7 +24,7 @@ export async function cleanup(ctx) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
await cleanOrphanedProcessingEntries(ctx, now, stallTimeout);
|
|
27
|
-
cleanStaleWaitingDependencies(ctx, now);
|
|
27
|
+
await cleanStaleWaitingDependencies(ctx, now);
|
|
28
28
|
cleanUniqueKeysAndGroups(ctx);
|
|
29
29
|
cleanStalledCandidates(ctx);
|
|
30
30
|
await cleanOrphanedJobIndex(ctx);
|
|
@@ -58,23 +58,43 @@ async function cleanOrphanedProcessingEntries(ctx, now, stallTimeout) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
function cleanStaleWaitingDependencies(ctx, now) {
|
|
61
|
+
async function cleanStaleWaitingDependencies(ctx, now) {
|
|
62
62
|
const depTimeout = 60 * 60 * 1000; // 1 hour
|
|
63
63
|
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
64
64
|
const shard = ctx.shards[i];
|
|
65
|
-
const
|
|
65
|
+
const staleIds = [];
|
|
66
66
|
for (const [_id, job] of shard.waitingDeps) {
|
|
67
67
|
if (now - job.createdAt > depTimeout) {
|
|
68
|
-
|
|
68
|
+
staleIds.push(job.id);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
if (staleIds.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
const removed = await withWriteLock(ctx.shardLocks[i], () => {
|
|
74
|
+
let count = 0;
|
|
75
|
+
for (const id of staleIds) {
|
|
76
|
+
const job = shard.waitingDeps.get(id);
|
|
77
|
+
if (!job || now - job.createdAt <= depTimeout)
|
|
78
|
+
continue;
|
|
79
|
+
// Persistence first: deleteJob also removes a pending buffered insert.
|
|
80
|
+
// If SQLite throws, the in-memory lifecycle remains live and coherent.
|
|
81
|
+
ctx.storage?.deleteJob(job.id);
|
|
82
|
+
shard.waitingDeps.delete(job.id);
|
|
83
|
+
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
84
|
+
if (job.uniqueKey && shard.getUniqueKeyEntry(job.queue, job.uniqueKey)?.jobId === job.id) {
|
|
85
|
+
shard.releaseUniqueKey(job.queue, job.uniqueKey);
|
|
86
|
+
}
|
|
87
|
+
if (job.customId && ctx.customIdMap.get(job.customId) === job.id) {
|
|
88
|
+
ctx.customIdMap.delete(job.customId);
|
|
89
|
+
}
|
|
90
|
+
ctx.dependencyResults?.releaseConsumer(job.id);
|
|
91
|
+
ctx.jobIndex.delete(job.id);
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
});
|
|
96
|
+
if (removed > 0) {
|
|
97
|
+
ctx.dashboardEmit?.('cleanup:stale-deps-removed', { count: removed });
|
|
78
98
|
}
|
|
79
99
|
}
|
|
80
100
|
}
|
|
@@ -12,6 +12,7 @@ import type { WebhookManager } from './webhookManager';
|
|
|
12
12
|
import type { WorkerManager } from './workerManager';
|
|
13
13
|
import type { EventsManager } from './eventsManager';
|
|
14
14
|
import type { MonitoringState } from './monitoringChecks';
|
|
15
|
+
import type { DependencyResultTracker } from './dependencyResultTracker';
|
|
15
16
|
import type { LockContext, BackgroundContext, StatsContext } from './types';
|
|
16
17
|
import type { PushContext } from './operations/push';
|
|
17
18
|
import type { PullContext } from './operations/pull';
|
|
@@ -36,6 +37,7 @@ export interface ContextDependencies {
|
|
|
36
37
|
depCompletions?: BoundedSet<JobId>;
|
|
37
38
|
timedOutJobs?: BoundedSet<JobId>;
|
|
38
39
|
jobResults: LRUMap<JobId, unknown>;
|
|
40
|
+
dependencyResults: DependencyResultTracker;
|
|
39
41
|
customIdMap: LRUMap<string, JobId>;
|
|
40
42
|
jobLogs: LRUMap<JobId, JobLogEntry[]>;
|
|
41
43
|
jobLocks: Map<JobId, JobLock>;
|
|
@@ -111,6 +113,7 @@ export declare class ContextFactory {
|
|
|
111
113
|
completedJobs: BoundedSet<JobId>;
|
|
112
114
|
completedJobsData: BoundedMap<JobId, Job>;
|
|
113
115
|
jobResults: LRUMap<JobId, unknown>;
|
|
116
|
+
dependencyResults: DependencyResultTracker;
|
|
114
117
|
jobLogs: LRUMap<JobId, JobLogEntry[]>;
|
|
115
118
|
storage: SqliteStorage | null;
|
|
116
119
|
};
|
|
@@ -37,6 +37,7 @@ export class ContextFactory {
|
|
|
37
37
|
depCompletions: this.deps.depCompletions,
|
|
38
38
|
timedOutJobs: this.deps.timedOutJobs,
|
|
39
39
|
jobResults: this.deps.jobResults,
|
|
40
|
+
dependencyResults: this.deps.dependencyResults,
|
|
40
41
|
customIdMap: this.deps.customIdMap,
|
|
41
42
|
jobLogs: this.deps.jobLogs,
|
|
42
43
|
jobLocks: this.deps.jobLocks,
|
|
@@ -86,6 +87,7 @@ export class ContextFactory {
|
|
|
86
87
|
depCompletions: this.deps.depCompletions,
|
|
87
88
|
timedOutJobs: this.deps.timedOutJobs,
|
|
88
89
|
jobResults: this.deps.jobResults,
|
|
90
|
+
dependencyResults: this.deps.dependencyResults,
|
|
89
91
|
customIdMap: this.deps.customIdMap,
|
|
90
92
|
jobIndex: this.deps.jobIndex,
|
|
91
93
|
totalPushed: this.deps.metrics.totalPushed,
|
|
@@ -117,6 +119,7 @@ export class ContextFactory {
|
|
|
117
119
|
completedJobsData: this.deps.completedJobsData,
|
|
118
120
|
depCompletions: this.deps.depCompletions,
|
|
119
121
|
jobResults: this.deps.jobResults,
|
|
122
|
+
dependencyResults: this.deps.dependencyResults,
|
|
120
123
|
jobIndex: this.deps.jobIndex,
|
|
121
124
|
customIdMap: this.deps.customIdMap,
|
|
122
125
|
totalCompleted: this.deps.metrics.totalCompleted,
|
|
@@ -143,6 +146,7 @@ export class ContextFactory {
|
|
|
143
146
|
jobIndex: this.deps.jobIndex,
|
|
144
147
|
jobLocks: this.deps.jobLocks,
|
|
145
148
|
clientJobs: this.deps.clientJobs,
|
|
149
|
+
dependencyResults: this.deps.dependencyResults,
|
|
146
150
|
webhookManager: this.deps.webhookManager,
|
|
147
151
|
eventsManager: this.deps.eventsManager,
|
|
148
152
|
repeatChain: this.deps.repeatChain,
|
|
@@ -159,6 +163,7 @@ export class ContextFactory {
|
|
|
159
163
|
completedJobs: this.deps.completedJobs,
|
|
160
164
|
completedJobsData: this.deps.completedJobsData,
|
|
161
165
|
jobResults: this.deps.jobResults,
|
|
166
|
+
dependencyResults: this.deps.dependencyResults,
|
|
162
167
|
customIdMap: this.deps.customIdMap,
|
|
163
168
|
};
|
|
164
169
|
}
|
|
@@ -196,6 +201,7 @@ export class ContextFactory {
|
|
|
196
201
|
completedJobs: this.deps.completedJobs,
|
|
197
202
|
completedJobsData: this.deps.completedJobsData,
|
|
198
203
|
jobResults: this.deps.jobResults,
|
|
204
|
+
dependencyResults: this.deps.dependencyResults,
|
|
199
205
|
jobLogs: this.deps.jobLogs,
|
|
200
206
|
storage: this.deps.storage,
|
|
201
207
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JobId } from '../domain/types/job';
|
|
2
|
+
/**
|
|
3
|
+
* Retains results while at least one live dependency consumer may still read
|
|
4
|
+
* them. Ordinary completed-job results remain governed by the configured LRU.
|
|
5
|
+
*/
|
|
6
|
+
export declare class DependencyResultTracker {
|
|
7
|
+
private readonly consumersByDependency;
|
|
8
|
+
private readonly dependenciesByConsumer;
|
|
9
|
+
private readonly protectedResults;
|
|
10
|
+
registerConsumer(consumerId: JobId, dependencyIds: readonly JobId[]): void;
|
|
11
|
+
retain(dependencyId: JobId, result: unknown): void;
|
|
12
|
+
has(dependencyId: JobId): boolean;
|
|
13
|
+
get(dependencyId: JobId): unknown;
|
|
14
|
+
releaseConsumer(consumerId: JobId): void;
|
|
15
|
+
releaseDependency(consumerId: JobId, dependencyId: JobId): void;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retains results while at least one live dependency consumer may still read
|
|
3
|
+
* them. Ordinary completed-job results remain governed by the configured LRU.
|
|
4
|
+
*/
|
|
5
|
+
export class DependencyResultTracker {
|
|
6
|
+
consumersByDependency = new Map();
|
|
7
|
+
dependenciesByConsumer = new Map();
|
|
8
|
+
protectedResults = new Map();
|
|
9
|
+
registerConsumer(consumerId, dependencyIds) {
|
|
10
|
+
if (dependencyIds.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
let dependencies = this.dependenciesByConsumer.get(consumerId);
|
|
13
|
+
if (!dependencies) {
|
|
14
|
+
dependencies = new Set();
|
|
15
|
+
this.dependenciesByConsumer.set(consumerId, dependencies);
|
|
16
|
+
}
|
|
17
|
+
for (const dependencyId of dependencyIds) {
|
|
18
|
+
dependencies.add(dependencyId);
|
|
19
|
+
let consumers = this.consumersByDependency.get(dependencyId);
|
|
20
|
+
if (!consumers) {
|
|
21
|
+
consumers = new Set();
|
|
22
|
+
this.consumersByDependency.set(dependencyId, consumers);
|
|
23
|
+
}
|
|
24
|
+
consumers.add(consumerId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
retain(dependencyId, result) {
|
|
28
|
+
if (this.consumersByDependency.has(dependencyId)) {
|
|
29
|
+
this.protectedResults.set(dependencyId, result);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
has(dependencyId) {
|
|
33
|
+
return this.protectedResults.has(dependencyId);
|
|
34
|
+
}
|
|
35
|
+
get(dependencyId) {
|
|
36
|
+
return this.protectedResults.get(dependencyId);
|
|
37
|
+
}
|
|
38
|
+
releaseConsumer(consumerId) {
|
|
39
|
+
const dependencies = this.dependenciesByConsumer.get(consumerId);
|
|
40
|
+
if (!dependencies)
|
|
41
|
+
return;
|
|
42
|
+
for (const dependencyId of dependencies) {
|
|
43
|
+
const consumers = this.consumersByDependency.get(dependencyId);
|
|
44
|
+
if (!consumers)
|
|
45
|
+
continue;
|
|
46
|
+
consumers.delete(consumerId);
|
|
47
|
+
if (consumers.size === 0) {
|
|
48
|
+
this.consumersByDependency.delete(dependencyId);
|
|
49
|
+
this.protectedResults.delete(dependencyId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.dependenciesByConsumer.delete(consumerId);
|
|
53
|
+
}
|
|
54
|
+
releaseDependency(consumerId, dependencyId) {
|
|
55
|
+
const dependencies = this.dependenciesByConsumer.get(consumerId);
|
|
56
|
+
dependencies?.delete(dependencyId);
|
|
57
|
+
if (dependencies?.size === 0)
|
|
58
|
+
this.dependenciesByConsumer.delete(consumerId);
|
|
59
|
+
const consumers = this.consumersByDependency.get(dependencyId);
|
|
60
|
+
consumers?.delete(consumerId);
|
|
61
|
+
if (consumers?.size === 0) {
|
|
62
|
+
this.consumersByDependency.delete(dependencyId);
|
|
63
|
+
this.protectedResults.delete(dependencyId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
clear() {
|
|
67
|
+
this.consumersByDependency.clear();
|
|
68
|
+
this.dependenciesByConsumer.clear();
|
|
69
|
+
this.protectedResults.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { Shard } from '../../domain/queue/shard';
|
|
|
8
8
|
import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
9
9
|
import type { RWLock } from '../../shared/lock';
|
|
10
10
|
import type { SetLike, MapLike } from '../../shared/lru';
|
|
11
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
11
12
|
/** Ack operation context */
|
|
12
13
|
export interface AckContext {
|
|
13
14
|
storage: SqliteStorage | null;
|
|
@@ -20,6 +21,7 @@ export interface AckContext {
|
|
|
20
21
|
/** Bare completion ids for removeOnComplete jobs so dependents can unblock */
|
|
21
22
|
depCompletions?: SetLike<JobId>;
|
|
22
23
|
jobResults: MapLike<JobId, unknown>;
|
|
24
|
+
dependencyResults: DependencyResultTracker;
|
|
23
25
|
jobIndex: Map<JobId, JobLocation>;
|
|
24
26
|
customIdMap?: MapLike<string, JobId>;
|
|
25
27
|
totalCompleted: {
|
|
@@ -60,6 +60,9 @@ export async function ackJob(jobId, result, ctx) {
|
|
|
60
60
|
// without making the job appear in state/stats queries.
|
|
61
61
|
ctx.depCompletions?.add(jobId);
|
|
62
62
|
}
|
|
63
|
+
if (result !== undefined)
|
|
64
|
+
ctx.dependencyResults.retain(jobId, result);
|
|
65
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
63
66
|
ctx.totalCompleted.value++;
|
|
64
67
|
if (ctx.perQueueMetrics) {
|
|
65
68
|
const pq = ctx.perQueueMetrics.get(job.queue);
|
|
@@ -205,6 +208,9 @@ export async function failJob(jobId, error, ctx, unrecoverable = false, stack) {
|
|
|
205
208
|
prev: 'failed',
|
|
206
209
|
});
|
|
207
210
|
}
|
|
211
|
+
else {
|
|
212
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
213
|
+
}
|
|
208
214
|
// BullMQ v5: failParentOnFailure — propagate terminal failure to parent
|
|
209
215
|
if (!wasRetried && job.failParentOnFailure && job.parentId && ctx.onChildTerminalFailure) {
|
|
210
216
|
ctx.onChildTerminalFailure(job, error);
|
|
@@ -7,7 +7,7 @@ import type { Shard } from '../../domain/queue/shard';
|
|
|
7
7
|
import type { RWLock } from '../../shared/lock';
|
|
8
8
|
import type { SetLike, MapLike } from '../../shared/lru';
|
|
9
9
|
import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
10
|
-
|
|
10
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
11
11
|
export interface ExtractedJob<T = unknown> {
|
|
12
12
|
id: JobId;
|
|
13
13
|
job: Job;
|
|
@@ -58,6 +58,7 @@ export interface FinalizeContext {
|
|
|
58
58
|
/** Bare completion ids for removeOnComplete jobs so dependents can unblock */
|
|
59
59
|
depCompletions?: SetLike<JobId>;
|
|
60
60
|
jobResults: MapLike<JobId, unknown>;
|
|
61
|
+
dependencyResults?: DependencyResultTracker;
|
|
61
62
|
jobIndex: Map<JobId, JobLocation>;
|
|
62
63
|
customIdMap?: MapLike<string, JobId>;
|
|
63
64
|
totalCompleted: {
|
|
@@ -171,6 +171,12 @@ export function finalizeBatchAck(extractedJobs, ctx, includeResults) {
|
|
|
171
171
|
ctx.depCompletions?.add(jobId);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
for (let i = 0; i < jobCount; i++) {
|
|
175
|
+
const { id, result } = extractedJobs[i];
|
|
176
|
+
if (includeResults && result !== undefined)
|
|
177
|
+
ctx.dependencyResults?.retain(id, result);
|
|
178
|
+
ctx.dependencyResults?.releaseConsumer(id);
|
|
179
|
+
}
|
|
174
180
|
// Broadcast events
|
|
175
181
|
if (needsBroadcast) {
|
|
176
182
|
for (let i = 0; i < jobCount; i++) {
|
|
@@ -9,6 +9,7 @@ import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
|
9
9
|
import type { WebhookManager } from '../webhookManager';
|
|
10
10
|
import type { EventsManager } from '../eventsManager';
|
|
11
11
|
import { type RWLock } from '../../shared/lock';
|
|
12
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
12
13
|
export { discardJob, moveJobToDelayed } from './jobMoveOperations';
|
|
13
14
|
/** Context for job management operations */
|
|
14
15
|
export interface JobManagementContext {
|
|
@@ -23,6 +24,7 @@ export interface JobManagementContext {
|
|
|
23
24
|
webhookManager: WebhookManager;
|
|
24
25
|
eventsManager: EventsManager;
|
|
25
26
|
repeatChain?: Map<JobId, JobId>;
|
|
27
|
+
dependencyResults: DependencyResultTracker;
|
|
26
28
|
}
|
|
27
29
|
/** Cancel a job (remove from queue) */
|
|
28
30
|
export declare function cancelJob(jobId: JobId, ctx: JobManagementContext): Promise<boolean>;
|
|
@@ -22,6 +22,7 @@ export async function cancelJob(jobId, ctx) {
|
|
|
22
22
|
shard.releaseUniqueKey(location.queueName, job.uniqueKey);
|
|
23
23
|
ctx.jobIndex.delete(jobId);
|
|
24
24
|
ctx.storage?.deleteJob(jobId);
|
|
25
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
25
26
|
return { success: true, queueName: location.queueName };
|
|
26
27
|
}
|
|
27
28
|
// Not in the run queue — it may be parked in waitingChildren (moved via
|
|
@@ -32,6 +33,7 @@ export async function cancelJob(jobId, ctx) {
|
|
|
32
33
|
shard.waitingChildren.delete(jobId);
|
|
33
34
|
ctx.jobIndex.delete(jobId);
|
|
34
35
|
ctx.storage?.deleteJob(jobId);
|
|
36
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
35
37
|
return { success: true, queueName: location.queueName };
|
|
36
38
|
}
|
|
37
39
|
// Or parked in waitingDeps (flow-chain dependent inserted by push.ts while
|
|
@@ -47,6 +49,7 @@ export async function cancelJob(jobId, ctx) {
|
|
|
47
49
|
shard.releaseUniqueKey(location.queueName, waiting.uniqueKey);
|
|
48
50
|
ctx.jobIndex.delete(jobId);
|
|
49
51
|
ctx.storage?.deleteJob(jobId);
|
|
52
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
50
53
|
return { success: true, queueName: location.queueName };
|
|
51
54
|
}
|
|
52
55
|
return { success: false, queueName: location.queueName };
|
|
@@ -75,10 +78,13 @@ export async function updateJobProgress(jobId, progress, ctx, message) {
|
|
|
75
78
|
const job = ctx.processingShards[procIdx].get(jobId);
|
|
76
79
|
if (!job)
|
|
77
80
|
return false;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
const clampedProgress = Math.max(0, Math.min(100, progress));
|
|
82
|
+
const effectiveMessage = message === undefined ? job.progressMessage : message;
|
|
83
|
+
const heartbeat = Date.now();
|
|
84
|
+
ctx.storage?.updateJobProgress(jobId, clampedProgress, effectiveMessage, heartbeat);
|
|
85
|
+
job.progress = clampedProgress;
|
|
86
|
+
job.progressMessage = effectiveMessage;
|
|
87
|
+
job.lastHeartbeat = heartbeat;
|
|
82
88
|
// Broadcast progress event to internal subscribers
|
|
83
89
|
ctx.eventsManager.broadcast({
|
|
84
90
|
eventType: 'progress',
|
|
@@ -37,6 +37,7 @@ export async function moveActiveToWait(jobId, ctx) {
|
|
|
37
37
|
ctx.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
38
38
|
shard.notify(job.queue);
|
|
39
39
|
});
|
|
40
|
+
ctx.storage?.updateRunAt(jobId, job.runAt);
|
|
40
41
|
ctx.eventsManager.broadcast({
|
|
41
42
|
eventType: 'waiting',
|
|
42
43
|
jobId,
|
|
@@ -29,8 +29,8 @@ export interface PullContext {
|
|
|
29
29
|
/**
|
|
30
30
|
* Pull next job from queue
|
|
31
31
|
*/
|
|
32
|
-
export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext): Promise<Job | null>;
|
|
32
|
+
export declare function pullJob(queue: string, timeoutMs: number, ctx: PullContext, signal?: AbortSignal): Promise<Job | null>;
|
|
33
33
|
/**
|
|
34
34
|
* Pull multiple jobs from queue
|
|
35
35
|
*/
|
|
36
|
-
export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext): Promise<Job[]>;
|
|
36
|
+
export declare function pullJobBatch(queue: string, count: number, timeoutMs: number, ctx: PullContext, signal?: AbortSignal): Promise<Job[]>;
|
|
@@ -122,10 +122,11 @@ function tryDequeueNextJob(shard, queue, now, ctx) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
/** Wait until either a notification arrives, the next delay matures, or the pull deadline. */
|
|
125
|
-
async function waitForNextCandidate(
|
|
125
|
+
async function waitForNextCandidate(options) {
|
|
126
|
+
const { shard, queue, deadline, now, nextRunAt, signal } = options;
|
|
126
127
|
const remaining = deadline - now;
|
|
127
128
|
const untilNextRun = nextRunAt === null ? remaining : Math.max(1, nextRunAt - now);
|
|
128
|
-
await shard.waitForJob(queue, Math.min(remaining, untilNextRun));
|
|
129
|
+
await shard.waitForJob(queue, Math.min(remaining, untilNextRun), signal);
|
|
129
130
|
}
|
|
130
131
|
/**
|
|
131
132
|
* Finish the handoff of a dequeued job: persist active state and broadcast.
|
|
@@ -217,14 +218,20 @@ async function requeueJob(job, queue, idx, ctx) {
|
|
|
217
218
|
/**
|
|
218
219
|
* Pull next job from queue
|
|
219
220
|
*/
|
|
220
|
-
export async function pullJob(queue, timeoutMs, ctx) {
|
|
221
|
+
export async function pullJob(queue, timeoutMs, ctx, signal) {
|
|
221
222
|
const startNs = Bun.nanoseconds();
|
|
222
223
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
223
224
|
const idx = shardIndex(queue);
|
|
224
225
|
while (true) {
|
|
225
|
-
|
|
226
|
+
if (signal?.aborted)
|
|
227
|
+
return null;
|
|
228
|
+
const attempt = await tryPullFromShard(queue, idx, ctx, signal);
|
|
226
229
|
const job = attempt.job;
|
|
227
230
|
if (job) {
|
|
231
|
+
if (signal?.aborted) {
|
|
232
|
+
await requeueJob(job, queue, idx, ctx);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
228
235
|
let delivered = false;
|
|
229
236
|
try {
|
|
230
237
|
delivered = finalizeProcessing(job, queue, ctx);
|
|
@@ -246,14 +253,23 @@ export async function pullJob(queue, timeoutMs, ctx) {
|
|
|
246
253
|
if (deadline === 0 || now >= deadline) {
|
|
247
254
|
return null;
|
|
248
255
|
}
|
|
249
|
-
await waitForNextCandidate(
|
|
256
|
+
await waitForNextCandidate({
|
|
257
|
+
shard: ctx.shards[idx],
|
|
258
|
+
queue,
|
|
259
|
+
deadline,
|
|
260
|
+
now,
|
|
261
|
+
nextRunAt: attempt.nextRunAt,
|
|
262
|
+
signal,
|
|
263
|
+
});
|
|
250
264
|
}
|
|
251
265
|
}
|
|
252
266
|
/**
|
|
253
267
|
* Try to pull a job from a specific shard
|
|
254
268
|
*/
|
|
255
|
-
async function tryPullFromShard(queue, idx, ctx) {
|
|
269
|
+
async function tryPullFromShard(queue, idx, ctx, signal) {
|
|
256
270
|
return await withWriteLock(ctx.shardLocks[idx], () => {
|
|
271
|
+
if (signal?.aborted)
|
|
272
|
+
return { job: null, nextRunAt: null };
|
|
257
273
|
const shard = ctx.shards[idx];
|
|
258
274
|
const state = shard.getState(queue);
|
|
259
275
|
if (state.paused) {
|
|
@@ -269,14 +285,22 @@ async function tryPullFromShard(queue, idx, ctx) {
|
|
|
269
285
|
/**
|
|
270
286
|
* Pull multiple jobs from queue
|
|
271
287
|
*/
|
|
272
|
-
export async function pullJobBatch(queue, count, timeoutMs, ctx) {
|
|
288
|
+
export async function pullJobBatch(queue, count, timeoutMs, ctx, signal) {
|
|
273
289
|
const startNs = Bun.nanoseconds();
|
|
274
290
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
275
291
|
const idx = shardIndex(queue);
|
|
276
292
|
while (true) {
|
|
277
|
-
|
|
293
|
+
if (signal?.aborted)
|
|
294
|
+
return [];
|
|
295
|
+
const attempt = await tryPullBatchFromShard(queue, idx, count, ctx, signal);
|
|
278
296
|
const jobs = attempt.jobs;
|
|
279
297
|
if (jobs.length > 0) {
|
|
298
|
+
if (signal?.aborted) {
|
|
299
|
+
for (const job of jobs) {
|
|
300
|
+
await requeueJob(job, queue, idx, ctx);
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
280
304
|
let delivered;
|
|
281
305
|
try {
|
|
282
306
|
delivered = finalizeProcessingBatch(jobs, queue, ctx);
|
|
@@ -305,14 +329,23 @@ export async function pullJobBatch(queue, count, timeoutMs, ctx) {
|
|
|
305
329
|
if (deadline === 0 || now >= deadline) {
|
|
306
330
|
return [];
|
|
307
331
|
}
|
|
308
|
-
await waitForNextCandidate(
|
|
332
|
+
await waitForNextCandidate({
|
|
333
|
+
shard: ctx.shards[idx],
|
|
334
|
+
queue,
|
|
335
|
+
deadline,
|
|
336
|
+
now,
|
|
337
|
+
nextRunAt: attempt.nextRunAt,
|
|
338
|
+
signal,
|
|
339
|
+
});
|
|
309
340
|
}
|
|
310
341
|
}
|
|
311
342
|
/**
|
|
312
343
|
* Try to pull multiple jobs from a shard
|
|
313
344
|
*/
|
|
314
|
-
async function tryPullBatchFromShard(queue, idx, count, ctx) {
|
|
345
|
+
async function tryPullBatchFromShard(queue, idx, count, ctx, signal) {
|
|
315
346
|
return await withWriteLock(ctx.shardLocks[idx], () => {
|
|
347
|
+
if (signal?.aborted)
|
|
348
|
+
return { jobs: [], nextRunAt: null };
|
|
316
349
|
const shard = ctx.shards[idx];
|
|
317
350
|
const state = shard.getState(queue);
|
|
318
351
|
const jobs = [];
|
|
@@ -8,6 +8,7 @@ import type { Shard } from '../../domain/queue/shard';
|
|
|
8
8
|
import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
9
9
|
import type { RWLock } from '../../shared/lock';
|
|
10
10
|
import type { SetLike, MapLike } from '../../shared/lru';
|
|
11
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
11
12
|
/** Push operation context */
|
|
12
13
|
export interface PushContext {
|
|
13
14
|
storage: SqliteStorage | null;
|
|
@@ -20,6 +21,7 @@ export interface PushContext {
|
|
|
20
21
|
/** Timeout markers — cleared on custom-id reuse so a recycled id starts clean */
|
|
21
22
|
timedOutJobs?: SetLike<JobId>;
|
|
22
23
|
jobResults: MapLike<JobId, unknown>;
|
|
24
|
+
dependencyResults: DependencyResultTracker;
|
|
23
25
|
customIdMap: MapLike<string, JobId>;
|
|
24
26
|
jobIndex: Map<JobId, JobLocation>;
|
|
25
27
|
totalPushed: {
|
|
@@ -2,9 +2,13 @@ import type { Shard } from '../../domain/queue/shard';
|
|
|
2
2
|
import type { Job, JobId } from '../../domain/types/job';
|
|
3
3
|
import type { JobLocation } from '../../domain/types/queue';
|
|
4
4
|
import type { SetLike } from '../../shared/lru';
|
|
5
|
+
import type { MapLike } from '../../shared/lru';
|
|
6
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
5
7
|
export interface PushInsertContext {
|
|
6
8
|
completedJobs: SetLike<JobId>;
|
|
7
9
|
depCompletions?: SetLike<JobId>;
|
|
10
|
+
jobResults: MapLike<JobId, unknown>;
|
|
11
|
+
dependencyResults: DependencyResultTracker;
|
|
8
12
|
jobIndex: Map<JobId, JobLocation>;
|
|
9
13
|
dashboardEmit?: (event: string, data: Record<string, unknown>) => void;
|
|
10
14
|
}
|
|
@@ -22,4 +22,10 @@ export function insertJobToShard(job, queue, shard, shardIdx, ctx) {
|
|
|
22
22
|
job.timeline.push({ state, timestamp: now });
|
|
23
23
|
}
|
|
24
24
|
ctx.jobIndex.set(job.id, { type: 'queue', shardIdx, queueName: queue });
|
|
25
|
+
ctx.dependencyResults.registerConsumer(job.id, job.dependsOn);
|
|
26
|
+
for (const dependencyId of job.dependsOn) {
|
|
27
|
+
if (ctx.jobResults.has(dependencyId)) {
|
|
28
|
+
ctx.dependencyResults.retain(dependencyId, ctx.jobResults.get(dependencyId));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
25
31
|
}
|
|
@@ -9,6 +9,7 @@ import type { Shard } from '../../domain/queue/shard';
|
|
|
9
9
|
import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
10
10
|
import { type RWLock } from '../../shared/lock';
|
|
11
11
|
import type { SetLike, MapLike } from '../../shared/lru';
|
|
12
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
12
13
|
/** Context for query operations */
|
|
13
14
|
export interface QueryContext {
|
|
14
15
|
storage: SqliteStorage | null;
|
|
@@ -20,6 +21,7 @@ export interface QueryContext {
|
|
|
20
21
|
completedJobs: SetLike<JobId>;
|
|
21
22
|
completedJobsData: MapLike<JobId, Job>;
|
|
22
23
|
jobResults: MapLike<JobId, unknown>;
|
|
24
|
+
dependencyResults: DependencyResultTracker;
|
|
23
25
|
customIdMap: MapLike<string, JobId>;
|
|
24
26
|
}
|
|
25
27
|
/** Get job by ID */
|
|
@@ -78,7 +78,11 @@ export async function getJob(jobId, ctx) {
|
|
|
78
78
|
}
|
|
79
79
|
/** Get job result */
|
|
80
80
|
export function getJobResult(jobId, ctx) {
|
|
81
|
-
|
|
81
|
+
if (ctx.jobResults.has(jobId))
|
|
82
|
+
return ctx.jobResults.get(jobId);
|
|
83
|
+
if (ctx.dependencyResults.has(jobId))
|
|
84
|
+
return ctx.dependencyResults.get(jobId);
|
|
85
|
+
return ctx.storage?.getResult(jobId);
|
|
82
86
|
}
|
|
83
87
|
/** Get job by custom ID */
|
|
84
88
|
export function getJobByCustomId(customId, ctx) {
|
|
@@ -6,6 +6,7 @@ import type { Job, JobId } from '../../domain/types/job';
|
|
|
6
6
|
import type { Shard } from '../../domain/queue/shard';
|
|
7
7
|
import type { SqliteStorage } from '../../infrastructure/persistence/sqlite';
|
|
8
8
|
import type { MapLike, SetLike } from '../../shared/lru';
|
|
9
|
+
import type { DependencyResultTracker } from '../dependencyResultTracker';
|
|
9
10
|
/** Context for queue control operations */
|
|
10
11
|
export interface QueueControlContext {
|
|
11
12
|
shards: Shard[];
|
|
@@ -19,6 +20,7 @@ export interface QueueControlContext {
|
|
|
19
20
|
completedJobsData?: MapLike<JobId, Job>;
|
|
20
21
|
jobResults?: MapLike<JobId, unknown>;
|
|
21
22
|
jobLogs?: MapLike<JobId, unknown>;
|
|
23
|
+
dependencyResults: DependencyResultTracker;
|
|
22
24
|
storage: SqliteStorage | null;
|
|
23
25
|
}
|
|
24
26
|
/** Pause a queue */
|
|
@@ -28,6 +28,7 @@ export function drainQueue(queue, ctx) {
|
|
|
28
28
|
// (not-yet-flushed) add cannot land on disk after the drain.
|
|
29
29
|
for (const jobId of jobIds) {
|
|
30
30
|
ctx.jobIndex.delete(jobId);
|
|
31
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
31
32
|
safeDeleteJob(ctx, jobId);
|
|
32
33
|
}
|
|
33
34
|
return count;
|
|
@@ -84,6 +85,7 @@ function cleanWaitingLike(queue, graceMs, ctx, maxJobs) {
|
|
|
84
85
|
shard.decrementQueued(jobId);
|
|
85
86
|
shard.removeFromTemporalIndex(jobId);
|
|
86
87
|
ctx.jobIndex.delete(jobId);
|
|
88
|
+
ctx.dependencyResults.releaseConsumer(jobId);
|
|
87
89
|
safeDeleteJob(ctx, jobId);
|
|
88
90
|
removed.push(jobId);
|
|
89
91
|
}
|
|
@@ -112,6 +114,7 @@ function cleanCompleted(queue, graceMs, ctx, maxJobs) {
|
|
|
112
114
|
ctx.jobResults?.delete(jid);
|
|
113
115
|
ctx.jobLogs?.delete(jid);
|
|
114
116
|
ctx.jobIndex.delete(jid);
|
|
117
|
+
ctx.dependencyResults.releaseConsumer(jid);
|
|
115
118
|
safeDeleteJob(ctx, jid);
|
|
116
119
|
}
|
|
117
120
|
return toRemove;
|
|
@@ -133,6 +136,7 @@ function cleanFailed(queue, graceMs, ctx, maxJobs) {
|
|
|
133
136
|
for (const jid of toRemove) {
|
|
134
137
|
shard.removeFromDlq(queue, jid);
|
|
135
138
|
ctx.jobIndex.delete(jid);
|
|
139
|
+
ctx.dependencyResults.releaseConsumer(jid);
|
|
136
140
|
ctx.jobResults?.delete(jid);
|
|
137
141
|
ctx.jobLogs?.delete(jid);
|
|
138
142
|
safeDeleteDlqEntry(ctx, jid);
|