bunqueue 2.8.53 → 2.8.54
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 +5 -1
- package/dist/application/cleanupTasks.js +1 -1
- package/dist/application/clientTracking.js +1 -1
- package/dist/application/contextFactory.js +1 -0
- package/dist/application/dlqManager.d.ts +7 -1
- package/dist/application/dlqManager.js +16 -5
- package/dist/application/lockManager.js +3 -3
- package/dist/application/operations/ack.js +2 -2
- package/dist/application/operations/ackHelpers.js +1 -1
- package/dist/application/operations/jobManagement.js +6 -4
- package/dist/application/operations/jobMoveOperations.js +3 -3
- package/dist/application/operations/jobStateTransitions.d.ts +1 -1
- package/dist/application/operations/jobStateTransitions.js +16 -2
- package/dist/application/queueManager.d.ts +22 -2
- package/dist/application/queueManager.js +48 -6
- package/dist/application/stallDetection.js +3 -3
- package/dist/client/flowJobDependencies.js +3 -1
- package/dist/client/flowJobMoveMethods.js +7 -2
- package/dist/client/jobConversion.d.ts +3 -3
- package/dist/client/jobConversion.js +10 -6
- package/dist/client/jobConversionHelpers.d.ts +1 -1
- package/dist/client/jobConversionHelpers.js +3 -6
- package/dist/client/jobConversionTypes.d.ts +4 -0
- package/dist/client/jobDeduplication.d.ts +6 -0
- package/dist/client/jobDeduplication.js +14 -0
- package/dist/client/queue/bullmqCompat.d.ts +18 -8
- package/dist/client/queue/bullmqCompat.js +39 -71
- package/dist/client/queue/deduplication.d.ts +2 -0
- package/dist/client/queue/deduplication.js +20 -11
- package/dist/client/queue/dlq.d.ts +11 -6
- package/dist/client/queue/dlq.js +47 -4
- package/dist/client/queue/dlqJobMethods.d.ts +9 -0
- package/dist/client/queue/dlqJobMethods.js +176 -0
- package/dist/client/queue/dlqOps.d.ts +2 -1
- package/dist/client/queue/dlqOps.js +2 -2
- package/dist/client/queue/jobMove.js +6 -1
- package/dist/client/queue/jobProxy.js +10 -5
- package/dist/client/queue/operations/add.js +17 -0
- package/dist/client/queue/operations/index.d.ts +1 -0
- package/dist/client/queue/operations/index.js +1 -0
- package/dist/client/queue/operations/management.d.ts +0 -4
- package/dist/client/queue/operations/management.js +33 -5
- package/dist/client/queue/operations/query.d.ts +1 -20
- package/dist/client/queue/operations/query.js +18 -53
- package/dist/client/queue/operations/queryStates.d.ts +13 -0
- package/dist/client/queue/operations/queryStates.js +32 -0
- package/dist/client/queue/operations/queryTcpPages.d.ts +14 -0
- package/dist/client/queue/operations/queryTcpPages.js +40 -0
- package/dist/client/queue/queue.d.ts +3 -0
- package/dist/client/queue/queue.js +23 -11
- package/dist/client/queue/rateLimit.d.ts +4 -4
- package/dist/client/queue/rateLimit.js +17 -8
- package/dist/client/queue/workers.d.ts +12 -0
- package/dist/client/queue/workers.js +20 -2
- package/dist/client/queueGroup.d.ts +11 -0
- package/dist/client/queueGroup.js +28 -1
- package/dist/client/sandboxed/worker.js +1 -1
- package/dist/client/worker/processor.js +1 -1
- package/dist/client/worker/processorHandlers.d.ts +1 -5
- package/dist/client/worker/processorHandlers.js +5 -7
- package/dist/client/worker/worker.d.ts +1 -1
- package/dist/client/worker/worker.js +37 -13
- package/dist/domain/queue/limiterManager.d.ts +11 -0
- package/dist/domain/queue/limiterManager.js +28 -0
- package/dist/domain/queue/shard.d.ts +10 -2
- package/dist/domain/queue/shard.js +23 -4
- package/dist/domain/queue/uniqueKeyManager.d.ts +3 -1
- package/dist/domain/queue/uniqueKeyManager.js +9 -1
- package/dist/domain/types/command.d.ts +34 -1
- package/dist/domain/types/queue.d.ts +5 -0
- package/dist/domain/types/queue.js +13 -0
- package/dist/infrastructure/persistence/sqlite.d.ts +4 -0
- package/dist/infrastructure/persistence/sqlite.js +16 -0
- package/dist/infrastructure/server/handlerRoutes.js +14 -1
- package/dist/infrastructure/server/handlers/dlq.d.ts +3 -0
- package/dist/infrastructure/server/handlers/dlq.js +18 -4
- package/dist/infrastructure/server/handlers/introspection.d.ts +19 -0
- package/dist/infrastructure/server/handlers/introspection.js +21 -0
- package/dist/infrastructure/server/handlers/query.js +1 -1
- package/package.json +1 -1
|
@@ -326,7 +326,11 @@ export function recover(ctx) {
|
|
|
326
326
|
const needsWaitingDeps = hasDependencies &&
|
|
327
327
|
!wasAlreadyPromoted &&
|
|
328
328
|
!job.dependsOn.every((depId) => ctx.completedJobs.has(depId) || completedInDb.has(depId));
|
|
329
|
-
|
|
329
|
+
const manuallyWaitingChildren = recoveredState === 'waiting-children' && !hasDependencies;
|
|
330
|
+
if (manuallyWaitingChildren) {
|
|
331
|
+
shard.waitingChildren.set(job.id, job);
|
|
332
|
+
}
|
|
333
|
+
else if (needsWaitingDeps) {
|
|
330
334
|
// Job is waiting for dependencies - don't add to main queue
|
|
331
335
|
shard.waitingDeps.set(job.id, job);
|
|
332
336
|
shard.registerDependencies(job.id, job.dependsOn);
|
|
@@ -85,7 +85,7 @@ async function cleanStaleWaitingDependencies(ctx, now) {
|
|
|
85
85
|
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
86
86
|
released.push(...job.dependsOn);
|
|
87
87
|
if (job.uniqueKey && shard.getUniqueKeyEntry(job.queue, job.uniqueKey)?.jobId === job.id) {
|
|
88
|
-
shard.
|
|
88
|
+
shard.releaseUniqueKeyIfOwned(job.queue, job.uniqueKey, job.id);
|
|
89
89
|
}
|
|
90
90
|
if (job.customId && ctx.customIdMap.get(job.customId) === job.id) {
|
|
91
91
|
ctx.customIdMap.delete(job.customId);
|
|
@@ -179,7 +179,7 @@ function releaseJobToQueue(opts) {
|
|
|
179
179
|
// Release lock if exists
|
|
180
180
|
ctx.jobLocks.delete(jobId);
|
|
181
181
|
// Release all job resources (concurrency, uniqueKey, groupId)
|
|
182
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
182
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
183
183
|
// Discard cron jobs with preventOverlap instead of re-queuing (#73).
|
|
184
184
|
// The cron scheduler will re-create them at the next scheduled tick.
|
|
185
185
|
// Re-queuing them causes the "starts right away on reconnect" bug.
|
|
@@ -47,9 +47,15 @@ export interface RetryCompletedContext extends DlqContext {
|
|
|
47
47
|
has(id: JobId): boolean;
|
|
48
48
|
delete(id: JobId): boolean;
|
|
49
49
|
} & Iterable<JobId>;
|
|
50
|
+
completedJobsData: {
|
|
51
|
+
get(id: JobId): Job | undefined;
|
|
52
|
+
};
|
|
50
53
|
jobResults: {
|
|
51
54
|
delete(id: JobId): boolean;
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
/** Retry completed jobs */
|
|
55
|
-
export declare function retryCompletedJobs(queue: string, ctx: RetryCompletedContext, jobId?: JobId
|
|
58
|
+
export declare function retryCompletedJobs(queue: string, ctx: RetryCompletedContext, jobId?: JobId, options?: {
|
|
59
|
+
limit?: number;
|
|
60
|
+
timestamp?: number;
|
|
61
|
+
}): number;
|
|
@@ -260,20 +260,31 @@ export function getDlqConfig(queue, ctx) {
|
|
|
260
260
|
return ctx.shards[idx].getDlqConfig(queue);
|
|
261
261
|
}
|
|
262
262
|
/** Retry completed jobs */
|
|
263
|
-
export function retryCompletedJobs(queue, ctx, jobId) {
|
|
263
|
+
export function retryCompletedJobs(queue, ctx, jobId, options = {}) {
|
|
264
264
|
if (jobId) {
|
|
265
265
|
if (!ctx.completedJobs.has(jobId))
|
|
266
266
|
return 0;
|
|
267
|
-
const job = ctx.storage?.getJob(jobId);
|
|
267
|
+
const job = ctx.completedJobsData.get(jobId) ?? ctx.storage?.getJob(jobId);
|
|
268
268
|
if (job?.queue !== queue)
|
|
269
269
|
return 0;
|
|
270
270
|
return requeueCompletedJob(job, ctx);
|
|
271
271
|
}
|
|
272
|
+
const limit = options.limit === undefined
|
|
273
|
+
? Number.POSITIVE_INFINITY
|
|
274
|
+
: Number.isFinite(options.limit) && options.limit > 0
|
|
275
|
+
? Math.floor(options.limit)
|
|
276
|
+
: 0;
|
|
277
|
+
if (limit === 0)
|
|
278
|
+
return 0;
|
|
279
|
+
const timestamp = options.timestamp ?? Number.POSITIVE_INFINITY;
|
|
272
280
|
let count = 0;
|
|
273
|
-
for (const id of ctx.completedJobs) {
|
|
274
|
-
|
|
275
|
-
|
|
281
|
+
for (const id of [...ctx.completedJobs]) {
|
|
282
|
+
if (count >= limit)
|
|
283
|
+
break;
|
|
284
|
+
const job = ctx.completedJobsData.get(id) ?? ctx.storage?.getJob(id);
|
|
285
|
+
if (job?.queue === queue && job.completedAt !== null && job.completedAt <= timestamp) {
|
|
276
286
|
count += requeueCompletedJob(job, ctx);
|
|
287
|
+
}
|
|
277
288
|
}
|
|
278
289
|
return count;
|
|
279
290
|
}
|
|
@@ -81,7 +81,7 @@ function processExpiredLockInner(jobId, lock, job, shardIdx, procIdx, ctx, now)
|
|
|
81
81
|
// the job finishes. Re-queuing would cause "starts right away on reconnect".
|
|
82
82
|
// The cron scheduler will re-create the job at the next scheduled tick.
|
|
83
83
|
if (job.uniqueKey?.startsWith('cron:')) {
|
|
84
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
84
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
85
85
|
ctx.jobIndex.delete(jobId);
|
|
86
86
|
ctx.storage?.deleteJob(jobId);
|
|
87
87
|
ctx.jobLocks.delete(jobId);
|
|
@@ -124,7 +124,7 @@ function handleRecoveryBoundExceeded(opts) {
|
|
|
124
124
|
// Release the concurrency slot (+group+uniqueKey) acquired at pull before
|
|
125
125
|
// moving to DLQ — otherwise the slot leaks (mirrors
|
|
126
126
|
// stallDetection.moveStalliedJobToDlq).
|
|
127
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
127
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
128
128
|
const entry = shard.addToDlq(job, attemptsExhausted ? "max_attempts_exceeded" /* FailureReason.MaxAttemptsExceeded */ : "stalled" /* FailureReason.Stalled */, attemptsExhausted
|
|
129
129
|
? `Lock expired at max attempts (${job.maxAttempts})`
|
|
130
130
|
: `Lock expired after ${lock.renewalCount} renewals`);
|
|
@@ -153,7 +153,7 @@ function requeueExpiredJob(opts) {
|
|
|
153
153
|
// Release the concurrency slot (+group+uniqueKey) acquired at pull before
|
|
154
154
|
// re-pushing — otherwise the slot leaks and the queue wedges (mirrors
|
|
155
155
|
// stallDetection.retryStalliedJob).
|
|
156
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
156
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
157
157
|
queue.push(job);
|
|
158
158
|
const isDelayed = job.runAt > now;
|
|
159
159
|
shard.incrementQueued(jobId, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
@@ -28,7 +28,7 @@ export async function ackJob(jobId, result, ctx) {
|
|
|
28
28
|
const idx = shardIndex(job.queue);
|
|
29
29
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
30
30
|
const shard = ctx.shards[idx];
|
|
31
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
31
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
32
32
|
// Releasing either queue concurrency or an active FIFO group can make a
|
|
33
33
|
// parked job eligible. Wake only waiters for this queue.
|
|
34
34
|
shard.notify(job.queue);
|
|
@@ -176,7 +176,7 @@ export async function failJob(jobId, error, ctx, unrecoverable = false, stack) {
|
|
|
176
176
|
const flowFailure = willRetry ? null : flowFailureRecord(job, error);
|
|
177
177
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
178
178
|
const shard = ctx.shards[idx];
|
|
179
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
179
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
180
180
|
if (willRetry) {
|
|
181
181
|
const now = Date.now();
|
|
182
182
|
job.runAt = now + calculateBackoff(job);
|
|
@@ -97,7 +97,7 @@ export async function releaseResources(byQueueShard, ctx) {
|
|
|
97
97
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
98
98
|
const shard = ctx.shards[idx];
|
|
99
99
|
for (const job of jobs) {
|
|
100
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
100
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
103
|
}));
|
|
@@ -19,8 +19,9 @@ export async function cancelJob(jobId, ctx) {
|
|
|
19
19
|
if (job) {
|
|
20
20
|
// Update running counters for O(1) stats
|
|
21
21
|
shard.decrementQueued(jobId);
|
|
22
|
-
if (job.uniqueKey)
|
|
23
|
-
shard.
|
|
22
|
+
if (job.uniqueKey) {
|
|
23
|
+
shard.releaseUniqueKeyIfOwned(location.queueName, job.uniqueKey, job.id);
|
|
24
|
+
}
|
|
24
25
|
ctx.jobIndex.delete(jobId);
|
|
25
26
|
ctx.storage?.deleteJob(jobId);
|
|
26
27
|
ctx.dependencyResults.releaseConsumer(jobId);
|
|
@@ -46,8 +47,9 @@ export async function cancelJob(jobId, ctx) {
|
|
|
46
47
|
if (waiting) {
|
|
47
48
|
shard.waitingDeps.delete(jobId);
|
|
48
49
|
shard.unregisterDependencies(jobId, waiting.dependsOn);
|
|
49
|
-
if (waiting.uniqueKey)
|
|
50
|
-
shard.
|
|
50
|
+
if (waiting.uniqueKey) {
|
|
51
|
+
shard.releaseUniqueKeyIfOwned(location.queueName, waiting.uniqueKey, waiting.id);
|
|
52
|
+
}
|
|
51
53
|
ctx.jobIndex.delete(jobId);
|
|
52
54
|
ctx.storage?.deleteJob(jobId);
|
|
53
55
|
ctx.dependencyResults.releaseConsumer(jobId);
|
|
@@ -27,7 +27,7 @@ export async function moveJobToDelayed(jobId, delay, ctx) {
|
|
|
27
27
|
const queueName = job.queue;
|
|
28
28
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
29
29
|
const shard = ctx.shards[idx];
|
|
30
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
30
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
31
31
|
shard.getQueue(job.queue).push(job);
|
|
32
32
|
const isDelayed = job.runAt > now;
|
|
33
33
|
shard.incrementQueued(jobId, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
@@ -78,11 +78,11 @@ export async function discardJob(jobId, ctx) {
|
|
|
78
78
|
const entry = await withWriteLock(ctx.shardLocks[idx], () => {
|
|
79
79
|
const shard = ctx.shards[idx];
|
|
80
80
|
if (fromProcessing) {
|
|
81
|
-
shard.releaseJobResources(validJob.queue, validJob.uniqueKey, validJob.groupId);
|
|
81
|
+
shard.releaseJobResources(validJob.queue, validJob.uniqueKey, validJob.groupId, validJob.id);
|
|
82
82
|
shard.notify(validJob.queue);
|
|
83
83
|
}
|
|
84
84
|
else if (validJob.uniqueKey) {
|
|
85
|
-
shard.
|
|
85
|
+
shard.releaseUniqueKeyIfOwned(validJob.queue, validJob.uniqueKey, validJob.id);
|
|
86
86
|
}
|
|
87
87
|
const dlqEntry = shard.addToDlq(validJob);
|
|
88
88
|
ctx.jobIndex.set(jobId, { type: 'dlq', queueName: validJob.queue });
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - changeWaitingDelay: Waiting -> Delayed
|
|
6
6
|
* - moveToWaitingChildren: Active -> WaitingChildren
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type JobId } from '../../domain/types/job';
|
|
9
9
|
import type { JobManagementContext } from './jobManagement';
|
|
10
10
|
/** Move active job back to waiting */
|
|
11
11
|
export declare function moveActiveToWait(jobId: JobId, ctx: JobManagementContext): Promise<boolean>;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - changeWaitingDelay: Waiting -> Delayed
|
|
6
6
|
* - moveToWaitingChildren: Active -> WaitingChildren
|
|
7
7
|
*/
|
|
8
|
+
import { MAX_TIMELINE_ENTRIES } from '../../domain/types/job';
|
|
8
9
|
import { shardIndex, processingShardIndex } from '../../shared/hash';
|
|
9
10
|
import { withWriteLock } from '../../shared/lock';
|
|
10
11
|
import { releaseClaimedJobOwnership } from './jobClaim';
|
|
@@ -31,7 +32,7 @@ export async function moveActiveToWait(jobId, ctx) {
|
|
|
31
32
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
32
33
|
const shard = ctx.shards[idx];
|
|
33
34
|
// Release concurrency/uniqueKey/group slots before re-queueing
|
|
34
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
35
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
35
36
|
shard.getQueue(job.queue).push(job);
|
|
36
37
|
shard.incrementQueued(jobId, false, job.createdAt, job.queue, job.runAt);
|
|
37
38
|
ctx.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
@@ -82,13 +83,26 @@ export async function moveToWaitingChildren(jobId, ctx) {
|
|
|
82
83
|
});
|
|
83
84
|
if (!job)
|
|
84
85
|
return false;
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
job.startedAt = null;
|
|
88
|
+
if (job.timeline.length < MAX_TIMELINE_ENTRIES) {
|
|
89
|
+
job.timeline.push({ state: 'waiting-children', timestamp: now });
|
|
90
|
+
}
|
|
85
91
|
const idx = shardIndex(job.queue);
|
|
86
92
|
await withWriteLock(ctx.shardLocks[idx], () => {
|
|
87
93
|
const shard = ctx.shards[idx];
|
|
88
94
|
// Release concurrency/uniqueKey/group slots before moving to waitingChildren
|
|
89
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
95
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
90
96
|
shard.waitingChildren.set(jobId, job);
|
|
91
97
|
ctx.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
92
98
|
});
|
|
99
|
+
ctx.storage?.markWaitingChildren(jobId, job.timeline);
|
|
100
|
+
ctx.eventsManager.broadcast({
|
|
101
|
+
eventType: "waiting-children" /* EventType.WaitingChildren */,
|
|
102
|
+
jobId,
|
|
103
|
+
queue: job.queue,
|
|
104
|
+
timestamp: now,
|
|
105
|
+
prev: 'active',
|
|
106
|
+
});
|
|
93
107
|
return true;
|
|
94
108
|
}
|
|
@@ -202,8 +202,12 @@ export declare class QueueManager {
|
|
|
202
202
|
getDlqCount(queue: string): number;
|
|
203
203
|
getDlqStats(queue: string): DlqStats;
|
|
204
204
|
retryDlq(queue: string, jobId?: JobId, limit?: number): number;
|
|
205
|
+
retryDlqByFilter(queue: string, filter: DlqFilter): number;
|
|
205
206
|
purgeDlq(queue: string): number;
|
|
206
|
-
retryCompleted(queue: string, jobId?: JobId
|
|
207
|
+
retryCompleted(queue: string, jobId?: JobId, options?: {
|
|
208
|
+
limit?: number;
|
|
209
|
+
timestamp?: number;
|
|
210
|
+
}): number;
|
|
207
211
|
setRateLimit(queue: string, limit: number, durationMs?: number, ttlMs?: number): void;
|
|
208
212
|
clearRateLimit(queue: string): void;
|
|
209
213
|
setConcurrency(queue: string, limit: number): void;
|
|
@@ -214,11 +218,27 @@ export declare class QueueManager {
|
|
|
214
218
|
* Reads the post-mutation state from the owning shard and UPSERTs the row.
|
|
215
219
|
*/
|
|
216
220
|
private persistQueueState;
|
|
217
|
-
/** Get
|
|
221
|
+
/** Get configured scalar limits (retained for internal/cloud compatibility). */
|
|
218
222
|
getQueueLimits(queue: string): {
|
|
219
223
|
rateLimit: number | null;
|
|
220
224
|
concurrencyLimit: number | null;
|
|
221
225
|
};
|
|
226
|
+
/** Get the full live limit status exposed by public Queue getters. */
|
|
227
|
+
getQueueLimitStatus(queue: string, maxJobs?: number): {
|
|
228
|
+
rateLimit: {
|
|
229
|
+
max: number;
|
|
230
|
+
duration: number;
|
|
231
|
+
} | null;
|
|
232
|
+
rateLimitTtl: number;
|
|
233
|
+
concurrencyLimit: number | null;
|
|
234
|
+
maxed: boolean;
|
|
235
|
+
};
|
|
236
|
+
/** Resolve the current owner of a queue-scoped deduplication key. */
|
|
237
|
+
getDeduplicationJobId(queue: string, key: string): string | null;
|
|
238
|
+
/** Release the current owner of a queue-scoped deduplication key. */
|
|
239
|
+
removeDeduplicationKey(queue: string, key: string): Promise<number>;
|
|
240
|
+
/** Release only the deduplication key still owned by a specific job. */
|
|
241
|
+
removeJobDeduplicationKey(id: string): Promise<boolean>;
|
|
222
242
|
/** Get all job results (for cloud telemetry) */
|
|
223
243
|
getAllJobResults(): Map<JobId, unknown>;
|
|
224
244
|
/** Get all job logs (for cloud telemetry) */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Queue Manager
|
|
3
3
|
* Core orchestrator for all queue operations
|
|
4
4
|
*/
|
|
5
|
-
import { DEFAULT_LOCK_TTL } from '../domain/types/job';
|
|
5
|
+
import { DEFAULT_LOCK_TTL, jobId } from '../domain/types/job';
|
|
6
6
|
import { DEFAULT_STALL_CONFIG } from '../domain/types/stall';
|
|
7
7
|
import { DEFAULT_DLQ_CONFIG } from '../domain/types/dlq';
|
|
8
8
|
import { Shard } from '../domain/queue/shard';
|
|
@@ -550,7 +550,7 @@ export class QueueManager {
|
|
|
550
550
|
job = pq.remove(jId);
|
|
551
551
|
if (job) {
|
|
552
552
|
shard.decrementQueued(jId);
|
|
553
|
-
shard.releaseJobResources(queueName, job.uniqueKey, job.groupId);
|
|
553
|
+
shard.releaseJobResources(queueName, job.uniqueKey, job.groupId, job.id);
|
|
554
554
|
}
|
|
555
555
|
});
|
|
556
556
|
if (!job) {
|
|
@@ -974,11 +974,14 @@ export class QueueManager {
|
|
|
974
974
|
retryDlq(queue, jobId, limit) {
|
|
975
975
|
return dlqOps.retryDlqJobs(queue, this.contextFactory.getDlqContext(), jobId, limit);
|
|
976
976
|
}
|
|
977
|
+
retryDlqByFilter(queue, filter) {
|
|
978
|
+
return dlqOps.retryDlqByFilter(queue, this.contextFactory.getDlqContext(), filter);
|
|
979
|
+
}
|
|
977
980
|
purgeDlq(queue) {
|
|
978
981
|
return dlqOps.purgeDlqJobs(queue, this.contextFactory.getDlqContext());
|
|
979
982
|
}
|
|
980
|
-
retryCompleted(queue, jobId) {
|
|
981
|
-
return dlqOps.retryCompletedJobs(queue, this.contextFactory.getRetryCompletedContext(), jobId);
|
|
983
|
+
retryCompleted(queue, jobId, options) {
|
|
984
|
+
return dlqOps.retryCompletedJobs(queue, this.contextFactory.getRetryCompletedContext(), jobId, options);
|
|
982
985
|
}
|
|
983
986
|
// ============ Rate Limiting ============
|
|
984
987
|
setRateLimit(queue, limit, durationMs, ttlMs) {
|
|
@@ -1039,14 +1042,53 @@ export class QueueManager {
|
|
|
1039
1042
|
dlqConfig,
|
|
1040
1043
|
});
|
|
1041
1044
|
}
|
|
1042
|
-
/** Get
|
|
1045
|
+
/** Get configured scalar limits (retained for internal/cloud compatibility). */
|
|
1043
1046
|
getQueueLimits(queue) {
|
|
1044
1047
|
const shard = this.shards[shardIndex(queue)];
|
|
1045
|
-
// Lazy TTL expiry so reads never report a limit that no longer throttles.
|
|
1046
1048
|
shard.expireRateLimitIfNeeded(queue);
|
|
1047
1049
|
const state = shard.getState(queue);
|
|
1048
1050
|
return { rateLimit: state.rateLimit, concurrencyLimit: state.concurrencyLimit };
|
|
1049
1051
|
}
|
|
1052
|
+
/** Get the full live limit status exposed by public Queue getters. */
|
|
1053
|
+
getQueueLimitStatus(queue, maxJobs) {
|
|
1054
|
+
const shard = this.shards[shardIndex(queue)];
|
|
1055
|
+
return {
|
|
1056
|
+
rateLimit: shard.getRateLimit(queue),
|
|
1057
|
+
rateLimitTtl: shard.getRateLimitTtl(queue, maxJobs),
|
|
1058
|
+
concurrencyLimit: shard.getConcurrency(queue),
|
|
1059
|
+
maxed: shard.isConcurrencyMaxed(queue),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/** Resolve the current owner of a queue-scoped deduplication key. */
|
|
1063
|
+
getDeduplicationJobId(queue, key) {
|
|
1064
|
+
return this.shards[shardIndex(queue)].getUniqueKeyEntry(queue, key)?.jobId ?? null;
|
|
1065
|
+
}
|
|
1066
|
+
/** Release the current owner of a queue-scoped deduplication key. */
|
|
1067
|
+
async removeDeduplicationKey(queue, key) {
|
|
1068
|
+
const idx = shardIndex(queue);
|
|
1069
|
+
return withWriteLock(this.shardLocks[idx], () => {
|
|
1070
|
+
const shard = this.shards[idx];
|
|
1071
|
+
const owner = shard.getUniqueKeyEntry(queue, key)?.jobId;
|
|
1072
|
+
if (!owner || !shard.releaseUniqueKeyIfOwned(queue, key, owner))
|
|
1073
|
+
return 0;
|
|
1074
|
+
this.storage?.clearJobUniqueKey(owner);
|
|
1075
|
+
return 1;
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
/** Release only the deduplication key still owned by a specific job. */
|
|
1079
|
+
async removeJobDeduplicationKey(id) {
|
|
1080
|
+
const jid = jobId(id);
|
|
1081
|
+
const job = await this.getJob(jid);
|
|
1082
|
+
if (!job?.uniqueKey)
|
|
1083
|
+
return false;
|
|
1084
|
+
const idx = shardIndex(job.queue);
|
|
1085
|
+
return withWriteLock(this.shardLocks[idx], () => {
|
|
1086
|
+
const removed = this.shards[idx].releaseUniqueKeyIfOwned(job.queue, job.uniqueKey, jid);
|
|
1087
|
+
if (removed)
|
|
1088
|
+
this.storage?.clearJobUniqueKey(jid);
|
|
1089
|
+
return removed;
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1050
1092
|
/** Get all job results (for cloud telemetry) */
|
|
1051
1093
|
getAllJobResults() {
|
|
1052
1094
|
const map = new Map();
|
|
@@ -108,7 +108,7 @@ async function handleStalledJob(job, action, ctx) {
|
|
|
108
108
|
/** Move stalled job to DLQ */
|
|
109
109
|
function moveStalliedJobToDlq(job, ctx, shard, procIdx, attemptsExhausted) {
|
|
110
110
|
ctx.processingShards[procIdx].delete(job.id);
|
|
111
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
111
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
112
112
|
// Discard cron jobs with preventOverlap instead of sending to DLQ (#73).
|
|
113
113
|
if (job.uniqueKey?.startsWith('cron:')) {
|
|
114
114
|
ctx.jobIndex.delete(job.id);
|
|
@@ -132,7 +132,7 @@ function retryStalliedJob(job, ctx, shard, procIdx, idx) {
|
|
|
132
132
|
// The cron scheduler will re-create them at the next scheduled tick.
|
|
133
133
|
if (job.uniqueKey?.startsWith('cron:')) {
|
|
134
134
|
ctx.processingShards[procIdx].delete(job.id);
|
|
135
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
135
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
136
136
|
ctx.jobIndex.delete(job.id);
|
|
137
137
|
ctx.storage?.deleteJob(job.id);
|
|
138
138
|
return;
|
|
@@ -153,7 +153,7 @@ function retryStalliedJob(job, ctx, shard, procIdx, idx) {
|
|
|
153
153
|
job.timeline.push({ state: 'waiting', timestamp: stallNow, attempt: job.attempts + 1 });
|
|
154
154
|
}
|
|
155
155
|
ctx.processingShards[procIdx].delete(job.id);
|
|
156
|
-
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId);
|
|
156
|
+
shard.releaseJobResources(job.queue, job.uniqueKey, job.groupId, job.id);
|
|
157
157
|
shard.getQueue(job.queue).push(job);
|
|
158
158
|
const isDelayed = job.runAt > Date.now();
|
|
159
159
|
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
@@ -5,12 +5,14 @@ async function fetchChildIds(id, embedded, tcp) {
|
|
|
5
5
|
if (embedded) {
|
|
6
6
|
const job = await getSharedManager().getJob(jobId(id));
|
|
7
7
|
if (!job)
|
|
8
|
-
|
|
8
|
+
return [];
|
|
9
9
|
return job.childrenIds.map(String);
|
|
10
10
|
}
|
|
11
11
|
if (!tcp)
|
|
12
12
|
return [];
|
|
13
13
|
const response = await tcp.send({ cmd: 'GetJob', id });
|
|
14
|
+
if (response.ok !== true && response.error === 'Job not found')
|
|
15
|
+
return [];
|
|
14
16
|
assertFlowTcpOk(response, 'GetJob');
|
|
15
17
|
const parent = response.job;
|
|
16
18
|
return (parent?.childrenIds ?? []).map(String);
|
|
@@ -2,6 +2,7 @@ import { jobId } from '../domain/types/job';
|
|
|
2
2
|
import { getSharedManager } from './manager';
|
|
3
3
|
import { buildFailCommand, failEmbeddedArgs } from './queue/failWire';
|
|
4
4
|
import { assertFlowTcpOk } from './flowJobTypes';
|
|
5
|
+
import { removeJobDeduplicationKey } from './jobDeduplication';
|
|
5
6
|
/** Lifecycle and failure-inspection methods exposed by a FlowProducer Job. */
|
|
6
7
|
export function buildFlowJobMoveMethods(runtime) {
|
|
7
8
|
const { id, embedded, tcp } = runtime;
|
|
@@ -42,7 +43,11 @@ export function buildFlowJobMoveMethods(runtime) {
|
|
|
42
43
|
moveToWaitingChildren: async () => {
|
|
43
44
|
if (embedded)
|
|
44
45
|
return getSharedManager().moveToWaitingChildren(jobId(id));
|
|
45
|
-
|
|
46
|
+
if (!tcp)
|
|
47
|
+
return false;
|
|
48
|
+
const response = await tcp.send({ cmd: 'MoveToWaitingChildren', id });
|
|
49
|
+
assertFlowTcpOk(response, 'MoveToWaitingChildren');
|
|
50
|
+
return true;
|
|
46
51
|
},
|
|
47
52
|
waitUntilFinished: async (_queueEvents, ttl) => {
|
|
48
53
|
const timeout = ttl ?? 30_000;
|
|
@@ -100,7 +105,7 @@ export function buildFlowJobMoveMethods(runtime) {
|
|
|
100
105
|
assertFlowTcpOk(response, 'RemoveChildDependency');
|
|
101
106
|
return response.removed ?? false;
|
|
102
107
|
},
|
|
103
|
-
removeDeduplicationKey: () =>
|
|
108
|
+
removeDeduplicationKey: () => removeJobDeduplicationKey(id, embedded, tcp),
|
|
104
109
|
removeUnprocessedChildren: async () => {
|
|
105
110
|
if (embedded)
|
|
106
111
|
return void (await getSharedManager().removeUnprocessedChildren(jobId(id)));
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DlqEntry as InternalDlqEntry } from '../domain/types/dlq';
|
|
6
6
|
import type { Job, DlqEntry } from './types';
|
|
7
|
-
import type { CreatePublicJobOptions, ToPublicJobOptions } from './jobConversionTypes';
|
|
8
|
-
export type { CreatePublicJobOptions, ToPublicJobOptions } from './jobConversionTypes';
|
|
7
|
+
import type { CreatePublicJobOptions, PublicJobMethodContext, ToPublicJobOptions } from './jobConversionTypes';
|
|
8
|
+
export type { CreatePublicJobOptions, PublicJobMethodContext, ToPublicJobOptions, } from './jobConversionTypes';
|
|
9
9
|
/** Convert internal job to public job (with methods) */
|
|
10
10
|
export declare function createPublicJob<T>(opts: CreatePublicJobOptions): Job<T>;
|
|
11
11
|
/** Simple public job without methods (for Queue.getJob) */
|
|
12
12
|
export declare function toPublicJob<T>(opts: ToPublicJobOptions): Job<T>;
|
|
13
13
|
/** Convert internal DLQ entry to public DLQ entry */
|
|
14
|
-
export declare function toDlqEntry<T>(entry: InternalDlqEntry): DlqEntry<T>;
|
|
14
|
+
export declare function toDlqEntry<T>(entry: InternalDlqEntry, methods: PublicJobMethodContext): DlqEntry<T>;
|
|
@@ -64,7 +64,7 @@ export function createPublicJob(opts) {
|
|
|
64
64
|
}
|
|
65
65
|
/** Simple public job without methods (for Queue.getJob) */
|
|
66
66
|
export function toPublicJob(opts) {
|
|
67
|
-
const { job, name, getState, remove, retry, getChildrenValues, updateData, promote, changeDelay, changePriority, extendLock, clearLogs, getDependencies, getDependenciesCount, moveToCompleted, moveToFailed, moveToWait, moveToDelayed, moveToWaitingChildren, waitUntilFinished, discard, getFailedChildrenValues, getIgnoredChildrenFailures, removeChildDependency, removeDeduplicationKey, removeUnprocessedChildren, stacktrace, } = opts;
|
|
67
|
+
const { job, name, updateProgress, log, getState, remove, retry, getChildrenValues, updateData, promote, changeDelay, changePriority, extendLock, clearLogs, getDependencies, getDependenciesCount, moveToCompleted, moveToFailed, moveToWait, moveToDelayed, moveToWaitingChildren, waitUntilFinished, discard, getFailedChildrenValues, getIgnoredChildrenFailures, removeChildDependency, removeDeduplicationKey, removeUnprocessedChildren, stacktrace, } = opts;
|
|
68
68
|
const id = String(job.id);
|
|
69
69
|
const jobOpts = buildJobOpts(job);
|
|
70
70
|
const props = buildJobProperties(job, name, stacktrace, undefined, undefined);
|
|
@@ -74,9 +74,9 @@ export function toPublicJob(opts) {
|
|
|
74
74
|
...props,
|
|
75
75
|
...stateChecks,
|
|
76
76
|
...serialization,
|
|
77
|
-
// Core methods
|
|
78
|
-
updateProgress:
|
|
79
|
-
log:
|
|
77
|
+
// Core methods
|
|
78
|
+
updateProgress: (progress, message) => updateProgress ? updateProgress(id, progress, message) : Promise.resolve(),
|
|
79
|
+
log: (message) => (log ? log(id, message) : Promise.resolve()),
|
|
80
80
|
getState: () => (getState ? getState(id) : Promise.resolve('unknown')),
|
|
81
81
|
remove: () => (remove ? remove(id) : Promise.resolve()),
|
|
82
82
|
retry: () => (retry ? retry(id) : Promise.resolve()),
|
|
@@ -121,10 +121,14 @@ export function toPublicJob(opts) {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
/** Convert internal DLQ entry to public DLQ entry */
|
|
124
|
-
export function toDlqEntry(entry) {
|
|
124
|
+
export function toDlqEntry(entry, methods) {
|
|
125
125
|
const jobData = entry.job.data;
|
|
126
126
|
return {
|
|
127
|
-
job:
|
|
127
|
+
job: createPublicJob({
|
|
128
|
+
job: entry.job,
|
|
129
|
+
name: jobData?.name ?? 'default',
|
|
130
|
+
...methods,
|
|
131
|
+
}),
|
|
128
132
|
enteredAt: entry.enteredAt,
|
|
129
133
|
reason: entry.reason,
|
|
130
134
|
error: entry.error,
|
|
@@ -7,6 +7,6 @@ import type { Job, JobStateType, JobOptions, GetDependenciesOpts, JobDependencie
|
|
|
7
7
|
/** Build common job properties from internal job */
|
|
8
8
|
export declare function buildJobProperties<T>(job: InternalJob, name: string, stacktrace?: string[] | null, token?: string, processedBy?: string): Pick<Job<T>, 'id' | 'name' | 'data' | 'queueName' | 'attemptsMade' | 'timestamp' | 'progress' | 'parent' | 'delay' | 'processedOn' | 'finishedOn' | 'stacktrace' | 'stalledCounter' | 'priority' | 'parentKey' | 'opts' | 'token' | 'processedBy' | 'deduplicationId' | 'repeatJobKey' | 'attemptsStarted'>;
|
|
9
9
|
/** Build state check methods */
|
|
10
|
-
export declare function buildStateCheckMethods(id: string, getState?: (id: string) => Promise<JobStateType>,
|
|
10
|
+
export declare function buildStateCheckMethods(id: string, getState?: (id: string) => Promise<JobStateType>, _getDependenciesCount?: (id: string, opts?: GetDependenciesOpts) => Promise<JobDependenciesCount>): Pick<Job, 'isWaiting' | 'isActive' | 'isDelayed' | 'isCompleted' | 'isFailed' | 'isWaitingChildren'>;
|
|
11
11
|
/** Build serialization methods */
|
|
12
12
|
export declare function buildSerializationMethods<T>(job: InternalJob, id: string, name: string, jobOpts: JobOptions, stacktrace?: string[] | null): Pick<Job<T>, 'toJSON' | 'asJSON'>;
|
|
@@ -38,7 +38,7 @@ export function buildJobProperties(job, name, stacktrace, token, processedBy) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
/** Build state check methods */
|
|
41
|
-
export function buildStateCheckMethods(id, getState,
|
|
41
|
+
export function buildStateCheckMethods(id, getState, _getDependenciesCount) {
|
|
42
42
|
return {
|
|
43
43
|
isWaiting: async () => {
|
|
44
44
|
const state = await (getState ? getState(id) : Promise.resolve('unknown'));
|
|
@@ -61,11 +61,8 @@ export function buildStateCheckMethods(id, getState, getDependenciesCount) {
|
|
|
61
61
|
return state === 'failed';
|
|
62
62
|
},
|
|
63
63
|
isWaitingChildren: async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return counts.unprocessed > 0;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
64
|
+
const state = await (getState ? getState(id) : Promise.resolve('unknown'));
|
|
65
|
+
return state === 'waiting-children';
|
|
69
66
|
},
|
|
70
67
|
};
|
|
71
68
|
}
|
|
@@ -43,10 +43,14 @@ export interface CreatePublicJobOptions {
|
|
|
43
43
|
processedBy?: string;
|
|
44
44
|
stacktrace?: string[] | null;
|
|
45
45
|
}
|
|
46
|
+
/** Complete operation set required when exposing a live job from another view. */
|
|
47
|
+
export type PublicJobMethodContext = Required<Omit<CreatePublicJobOptions, 'job' | 'name' | 'token' | 'processedBy' | 'stacktrace'>>;
|
|
46
48
|
/** Options for creating a simple public job */
|
|
47
49
|
export interface ToPublicJobOptions {
|
|
48
50
|
job: InternalJob;
|
|
49
51
|
name: string;
|
|
52
|
+
updateProgress?: (id: string, progress: number, message?: string) => Promise<void>;
|
|
53
|
+
log?: (id: string, message: string) => Promise<void>;
|
|
50
54
|
getState?: (id: string) => Promise<JobStateType>;
|
|
51
55
|
remove?: (id: string) => Promise<void>;
|
|
52
56
|
retry?: (id: string) => Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface CommandTransport {
|
|
2
|
+
send(command: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
3
|
+
}
|
|
4
|
+
/** Remove only the deduplication key currently owned by the given job. */
|
|
5
|
+
export declare function removeJobDeduplicationKey(id: string, embedded: boolean, transport: CommandTransport | null | undefined): Promise<boolean>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jobId } from '../domain/types/job';
|
|
2
|
+
import { getSharedManager } from './manager';
|
|
3
|
+
/** Remove only the deduplication key currently owned by the given job. */
|
|
4
|
+
export async function removeJobDeduplicationKey(id, embedded, transport) {
|
|
5
|
+
if (embedded)
|
|
6
|
+
return getSharedManager().removeJobDeduplicationKey(jobId(id));
|
|
7
|
+
if (!transport)
|
|
8
|
+
return false;
|
|
9
|
+
const response = await transport.send({ cmd: 'RemoveJobDeduplicationKey', id });
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(response.error ?? 'Failed to remove deduplication key');
|
|
12
|
+
}
|
|
13
|
+
return response.data.removed;
|
|
14
|
+
}
|