bunqueue 2.8.31 → 2.8.33
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 +29 -26
- package/dist/application/backgroundTasks.js +17 -9
- package/dist/application/dependencyProcessor.js +6 -1
- package/dist/application/dlqManager.js +1 -1
- package/dist/application/lockManager.js +1 -1
- package/dist/application/operations/ack.js +23 -1
- package/dist/application/operations/jobManagement.js +29 -1
- package/dist/application/operations/jobStateTransitions.js +1 -1
- package/dist/application/operations/pull.js +125 -96
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/queryOperations.js +74 -70
- package/dist/application/queueManager.d.ts +6 -11
- package/dist/application/queueManager.js +16 -3
- package/dist/application/queueStatsAggregator.d.ts +23 -0
- package/dist/application/queueStatsAggregator.js +80 -0
- package/dist/application/statsManager.d.ts +2 -26
- package/dist/application/statsManager.js +8 -124
- package/dist/client/tcpPool.js +8 -1
- package/dist/domain/queue/shard.d.ts +3 -1
- package/dist/domain/queue/shard.js +15 -7
- package/dist/domain/queue/temporalIndex.d.ts +24 -0
- package/dist/domain/queue/temporalIndex.js +100 -0
- package/dist/domain/queue/temporalManager.d.ts +2 -11
- package/dist/domain/queue/temporalManager.js +27 -44
- package/dist/domain/queue/waiterManager.d.ts +11 -13
- package/dist/domain/queue/waiterManager.js +80 -49
- package/dist/infrastructure/cloud/commands.js +5 -2
- package/dist/infrastructure/cloud/snapshotCollector.js +5 -2
- package/dist/infrastructure/cloud/statsRefresh.js +5 -2
- package/dist/infrastructure/cloud/statsUpdate.js +5 -2
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +13 -1
- package/dist/infrastructure/persistence/sqlite.d.ts +18 -2
- package/dist/infrastructure/persistence/sqlite.js +49 -6
- package/dist/infrastructure/server/handlers/core.js +13 -7
- package/dist/infrastructure/server/handlers/pushBatchValidation.d.ts +23 -0
- package/dist/infrastructure/server/handlers/pushBatchValidation.js +70 -0
- package/dist/infrastructure/server/queueCountsScheduler.d.ts +13 -0
- package/dist/infrastructure/server/queueCountsScheduler.js +35 -0
- package/dist/infrastructure/server/sseHandler.d.ts +1 -0
- package/dist/infrastructure/server/sseHandler.js +18 -13
- package/dist/infrastructure/server/wsHandler.d.ts +1 -1
- package/dist/infrastructure/server/wsHandler.js +18 -15
- package/package.json +3 -3
|
@@ -1321,7 +1321,7 @@ export class QueueManager {
|
|
|
1321
1321
|
queue.push(parentJob);
|
|
1322
1322
|
shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
|
|
1323
1323
|
this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
|
|
1324
|
-
shard.notify();
|
|
1324
|
+
shard.notify(parentJob.queue);
|
|
1325
1325
|
promoted = true;
|
|
1326
1326
|
}
|
|
1327
1327
|
});
|
|
@@ -1435,7 +1435,7 @@ export class QueueManager {
|
|
|
1435
1435
|
shard.getQueue(parentJob.queue).push(parentJob);
|
|
1436
1436
|
shard.incrementQueued(parentId, false, parentJob.createdAt, parentJob.queue, now);
|
|
1437
1437
|
this.jobIndex.set(parentId, { type: 'queue', shardIdx: idx, queueName: parentJob.queue });
|
|
1438
|
-
shard.notify();
|
|
1438
|
+
shard.notify(parentJob.queue);
|
|
1439
1439
|
promoted = true;
|
|
1440
1440
|
}
|
|
1441
1441
|
});
|
|
@@ -1529,13 +1529,17 @@ export class QueueManager {
|
|
|
1529
1529
|
const ctx = this.contextFactory.getStatsContext();
|
|
1530
1530
|
const queues = this.listQueues();
|
|
1531
1531
|
const result = [];
|
|
1532
|
+
const countsByQueue = statsMgr.getAllQueueJobCounts(queues, ctx);
|
|
1532
1533
|
for (const name of queues) {
|
|
1533
|
-
const c =
|
|
1534
|
+
const c = countsByQueue.get(name);
|
|
1535
|
+
if (!c)
|
|
1536
|
+
continue;
|
|
1534
1537
|
result.push({
|
|
1535
1538
|
name,
|
|
1536
1539
|
paused: this.isPaused(name),
|
|
1537
1540
|
counts: {
|
|
1538
1541
|
waiting: c.waiting,
|
|
1542
|
+
prioritized: c.prioritized,
|
|
1539
1543
|
active: c.active,
|
|
1540
1544
|
completed: c.completed,
|
|
1541
1545
|
failed: c.failed,
|
|
@@ -1545,6 +1549,15 @@ export class QueueManager {
|
|
|
1545
1549
|
}
|
|
1546
1550
|
return result;
|
|
1547
1551
|
}
|
|
1552
|
+
/** Get counts for every registered queue with one global aggregation pass. */
|
|
1553
|
+
getAllQueueJobCounts() {
|
|
1554
|
+
return this.getQueueJobCountsBatch(this.queueNamesCache);
|
|
1555
|
+
}
|
|
1556
|
+
/** Aggregate a selected group of queues without repeating global scans. */
|
|
1557
|
+
getQueueJobCountsBatch(queueNames) {
|
|
1558
|
+
const ctx = this.contextFactory.getStatsContext();
|
|
1559
|
+
return statsMgr.getAllQueueJobCounts(queueNames, ctx);
|
|
1560
|
+
}
|
|
1548
1561
|
/** Get job counts for a specific queue */
|
|
1549
1562
|
getQueueJobCounts(queueName) {
|
|
1550
1563
|
return statsMgr.getQueueJobCounts(queueName, this.contextFactory.getStatsContext());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StatsContext } from './types';
|
|
2
|
+
export interface PerQueueStats {
|
|
3
|
+
waiting: number;
|
|
4
|
+
prioritized: number;
|
|
5
|
+
delayed: number;
|
|
6
|
+
active: number;
|
|
7
|
+
dlq: number;
|
|
8
|
+
}
|
|
9
|
+
export interface QueueJobCounts {
|
|
10
|
+
waiting: number;
|
|
11
|
+
prioritized: number;
|
|
12
|
+
delayed: number;
|
|
13
|
+
active: number;
|
|
14
|
+
completed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
'waiting-children': number;
|
|
17
|
+
totalCompleted: number;
|
|
18
|
+
totalFailed: number;
|
|
19
|
+
}
|
|
20
|
+
/** Aggregate all requested queues in one pass over every global collection. */
|
|
21
|
+
export declare function getAllQueueJobCounts(queueNames: Iterable<string>, ctx: StatsContext): Map<string, QueueJobCounts>;
|
|
22
|
+
export declare function getPerQueueStats(ctx: StatsContext, queueNames: Set<string>): Map<string, PerQueueStats>;
|
|
23
|
+
export declare function getQueueJobCounts(queueName: string, ctx: StatsContext): QueueJobCounts;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SHARD_COUNT, shardIndex } from '../shared/hash';
|
|
2
|
+
function createCounts(queue, ctx) {
|
|
3
|
+
const metrics = ctx.perQueueMetrics?.get(queue);
|
|
4
|
+
return {
|
|
5
|
+
waiting: 0,
|
|
6
|
+
prioritized: 0,
|
|
7
|
+
delayed: 0,
|
|
8
|
+
active: 0,
|
|
9
|
+
completed: 0,
|
|
10
|
+
failed: ctx.shards[shardIndex(queue)].getDlqCount(queue),
|
|
11
|
+
'waiting-children': 0,
|
|
12
|
+
totalCompleted: Number(metrics?.totalCompleted ?? 0n),
|
|
13
|
+
totalFailed: Number(metrics?.totalFailed ?? 0n),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Aggregate all requested queues in one pass over every global collection. */
|
|
17
|
+
export function getAllQueueJobCounts(queueNames, ctx) {
|
|
18
|
+
const result = new Map();
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const name of queueNames) {
|
|
21
|
+
const counts = createCounts(name, ctx);
|
|
22
|
+
const queue = ctx.shards[shardIndex(name)].queues.get(name);
|
|
23
|
+
if (queue) {
|
|
24
|
+
for (const job of queue.values()) {
|
|
25
|
+
if (job.runAt > now)
|
|
26
|
+
counts.delayed++;
|
|
27
|
+
else if (job.priority > 0)
|
|
28
|
+
counts.prioritized++;
|
|
29
|
+
else
|
|
30
|
+
counts.waiting++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
result.set(name, counts);
|
|
34
|
+
}
|
|
35
|
+
for (const processing of ctx.processingShards) {
|
|
36
|
+
for (const job of processing.values()) {
|
|
37
|
+
const counts = result.get(job.queue);
|
|
38
|
+
if (counts)
|
|
39
|
+
counts.active++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const [jobId, location] of ctx.jobIndex) {
|
|
43
|
+
if (location.type !== 'completed' || !ctx.completedJobs.has(jobId))
|
|
44
|
+
continue;
|
|
45
|
+
const counts = result.get(location.queueName);
|
|
46
|
+
if (counts)
|
|
47
|
+
counts.completed++;
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
50
|
+
const shard = ctx.shards[i];
|
|
51
|
+
for (const job of shard.waitingChildren.values()) {
|
|
52
|
+
const counts = result.get(job.queue);
|
|
53
|
+
if (counts)
|
|
54
|
+
counts['waiting-children']++;
|
|
55
|
+
}
|
|
56
|
+
for (const job of shard.waitingDeps.values()) {
|
|
57
|
+
const counts = result.get(job.queue);
|
|
58
|
+
if (counts)
|
|
59
|
+
counts['waiting-children']++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
export function getPerQueueStats(ctx, queueNames) {
|
|
65
|
+
const allCounts = getAllQueueJobCounts(queueNames, ctx);
|
|
66
|
+
const result = new Map();
|
|
67
|
+
for (const [name, counts] of allCounts) {
|
|
68
|
+
result.set(name, {
|
|
69
|
+
waiting: counts.waiting,
|
|
70
|
+
prioritized: counts.prioritized,
|
|
71
|
+
delayed: counts.delayed,
|
|
72
|
+
active: counts.active,
|
|
73
|
+
dlq: counts.failed,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
export function getQueueJobCounts(queueName, ctx) {
|
|
79
|
+
return getAllQueueJobCounts([queueName], ctx).get(queueName) ?? createCounts(queueName, ctx);
|
|
80
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Provides system metrics and memory compaction utilities
|
|
4
4
|
*/
|
|
5
5
|
import type { StatsContext } from './types';
|
|
6
|
+
export { getAllQueueJobCounts, getPerQueueStats, getQueueJobCounts, } from './queueStatsAggregator';
|
|
7
|
+
export type { PerQueueStats, QueueJobCounts } from './queueStatsAggregator';
|
|
6
8
|
export interface QueueStats {
|
|
7
9
|
waiting: number;
|
|
8
10
|
prioritized: number;
|
|
@@ -19,13 +21,6 @@ export interface QueueStats {
|
|
|
19
21
|
cronJobs: number;
|
|
20
22
|
cronPending: number;
|
|
21
23
|
}
|
|
22
|
-
export interface PerQueueStats {
|
|
23
|
-
waiting: number;
|
|
24
|
-
prioritized: number;
|
|
25
|
-
delayed: number;
|
|
26
|
-
active: number;
|
|
27
|
-
dlq: number;
|
|
28
|
-
}
|
|
29
24
|
export interface MemoryStats {
|
|
30
25
|
jobIndex: number;
|
|
31
26
|
completedJobs: number;
|
|
@@ -57,25 +52,6 @@ export declare function getStats(ctx: StatsContext, cronScheduler: {
|
|
|
57
52
|
* Returns counts of entries in all major collections.
|
|
58
53
|
*/
|
|
59
54
|
export declare function getMemoryStats(ctx: StatsContext): MemoryStats;
|
|
60
|
-
/**
|
|
61
|
-
* Get per-queue statistics by iterating shards to aggregate per queue name.
|
|
62
|
-
* Uses shard hashing to look up each queue in its designated shard.
|
|
63
|
-
*/
|
|
64
|
-
export declare function getPerQueueStats(ctx: StatsContext, queueNames: Set<string>): Map<string, PerQueueStats>;
|
|
65
|
-
/**
|
|
66
|
-
* Get job counts for a specific queue
|
|
67
|
-
*/
|
|
68
|
-
export declare function getQueueJobCounts(queueName: string, ctx: StatsContext): {
|
|
69
|
-
waiting: number;
|
|
70
|
-
prioritized: number;
|
|
71
|
-
delayed: number;
|
|
72
|
-
active: number;
|
|
73
|
-
completed: number;
|
|
74
|
-
failed: number;
|
|
75
|
-
'waiting-children': number;
|
|
76
|
-
totalCompleted: number;
|
|
77
|
-
totalFailed: number;
|
|
78
|
-
};
|
|
79
55
|
/**
|
|
80
56
|
* Force compact all collections to reduce memory usage.
|
|
81
57
|
* Use after large batch operations or when memory pressure is high.
|
|
@@ -2,17 +2,8 @@
|
|
|
2
2
|
* Stats Manager - Queue statistics and memory management
|
|
3
3
|
* Provides system metrics and memory compaction utilities
|
|
4
4
|
*/
|
|
5
|
-
import { SHARD_COUNT
|
|
6
|
-
|
|
7
|
-
function countByQueue(sources, queueName) {
|
|
8
|
-
let count = 0;
|
|
9
|
-
for (const src of sources) {
|
|
10
|
-
for (const job of src)
|
|
11
|
-
if (job.queue === queueName)
|
|
12
|
-
count++;
|
|
13
|
-
}
|
|
14
|
-
return count;
|
|
15
|
-
}
|
|
5
|
+
import { SHARD_COUNT } from '../shared/hash';
|
|
6
|
+
export { getAllQueueJobCounts, getPerQueueStats, getQueueJobCounts, } from './queueStatsAggregator';
|
|
16
7
|
/**
|
|
17
8
|
* Get queue statistics - uses running counters + priority scan
|
|
18
9
|
*/
|
|
@@ -21,7 +12,6 @@ export function getStats(ctx, cronScheduler) {
|
|
|
21
12
|
const now = Date.now();
|
|
22
13
|
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
23
14
|
const shardStats = ctx.shards[i].getStats();
|
|
24
|
-
delayed += shardStats.delayedJobs;
|
|
25
15
|
dlq += shardStats.dlqJobs;
|
|
26
16
|
active += ctx.processingShards[i].size;
|
|
27
17
|
// getJobState reports BOTH waitingChildren (flow parents) and waitingDeps
|
|
@@ -31,14 +21,12 @@ export function getStats(ctx, cronScheduler) {
|
|
|
31
21
|
// Scan queues to split waiting vs prioritized (BullMQ v5 compat)
|
|
32
22
|
for (const queue of ctx.shards[i].queues.values()) {
|
|
33
23
|
for (const job of queue.values()) {
|
|
34
|
-
if (job.runAt
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
24
|
+
if (job.runAt > now)
|
|
25
|
+
delayed++;
|
|
26
|
+
else if (job.priority > 0)
|
|
27
|
+
prioritized++;
|
|
28
|
+
else
|
|
29
|
+
waiting++;
|
|
42
30
|
}
|
|
43
31
|
}
|
|
44
32
|
}
|
|
@@ -103,110 +91,6 @@ export function getMemoryStats(ctx) {
|
|
|
103
91
|
delayedHeapTotal,
|
|
104
92
|
};
|
|
105
93
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Get per-queue statistics by iterating shards to aggregate per queue name.
|
|
108
|
-
* Uses shard hashing to look up each queue in its designated shard.
|
|
109
|
-
*/
|
|
110
|
-
export function getPerQueueStats(ctx, queueNames) {
|
|
111
|
-
const result = new Map();
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
for (const name of queueNames) {
|
|
114
|
-
const idx = shardIndex(name);
|
|
115
|
-
const shard = ctx.shards[idx];
|
|
116
|
-
const queue = shard.queues.get(name);
|
|
117
|
-
let waiting = 0;
|
|
118
|
-
let prioritized = 0;
|
|
119
|
-
let delayed = 0;
|
|
120
|
-
if (queue) {
|
|
121
|
-
for (const job of queue.values()) {
|
|
122
|
-
if (job.runAt > now) {
|
|
123
|
-
delayed++;
|
|
124
|
-
}
|
|
125
|
-
else if (job.priority > 0) {
|
|
126
|
-
prioritized++;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
waiting++;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const dlq = shard.getDlqCount(name);
|
|
134
|
-
result.set(name, { waiting, prioritized, delayed, active: 0, dlq });
|
|
135
|
-
}
|
|
136
|
-
// Count active jobs per queue from all processing shards
|
|
137
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
138
|
-
for (const job of ctx.processingShards[i].values()) {
|
|
139
|
-
const entry = result.get(job.queue);
|
|
140
|
-
if (entry) {
|
|
141
|
-
entry.active++;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Get job counts for a specific queue
|
|
149
|
-
*/
|
|
150
|
-
export function getQueueJobCounts(queueName, ctx) {
|
|
151
|
-
const idx = shardIndex(queueName);
|
|
152
|
-
const shard = ctx.shards[idx];
|
|
153
|
-
const queue = shard.queues.get(queueName);
|
|
154
|
-
const now = Date.now();
|
|
155
|
-
// Count waiting vs prioritized vs delayed jobs in the queue
|
|
156
|
-
let waiting = 0;
|
|
157
|
-
let prioritized = 0;
|
|
158
|
-
let delayed = 0;
|
|
159
|
-
if (queue) {
|
|
160
|
-
for (const job of queue.values()) {
|
|
161
|
-
if (job.runAt > now) {
|
|
162
|
-
delayed++;
|
|
163
|
-
}
|
|
164
|
-
else if (job.priority > 0) {
|
|
165
|
-
prioritized++;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
waiting++;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// Count active jobs (processing) for this queue
|
|
173
|
-
let active = 0;
|
|
174
|
-
for (const procShard of ctx.processingShards) {
|
|
175
|
-
for (const job of procShard.values()) {
|
|
176
|
-
if (job.queue === queueName) {
|
|
177
|
-
active++;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// Count completed jobs for this queue
|
|
182
|
-
let completed = 0;
|
|
183
|
-
for (const [jobId, loc] of ctx.jobIndex) {
|
|
184
|
-
if (loc.type === 'completed' && loc.queueName === queueName && ctx.completedJobs.has(jobId)) {
|
|
185
|
-
completed++;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Count failed (DLQ) jobs for this queue
|
|
189
|
-
const failed = shard.getDlq(queueName).length;
|
|
190
|
-
// Count waiting-children jobs. getJobState/getJobs treat BOTH waitingChildren
|
|
191
|
-
// (flow parents) and waitingDeps (jobs blocked on dependsOn) as 'waiting-children',
|
|
192
|
-
// so count both to stay consistent with state/list (#95 class).
|
|
193
|
-
const waitingChildrenCount = countByQueue([shard.waitingChildren.values(), shard.waitingDeps.values()], queueName);
|
|
194
|
-
// Per-queue cumulative counters
|
|
195
|
-
const perQueue = ctx.perQueueMetrics?.get(queueName);
|
|
196
|
-
const totalCompleted = Number(perQueue?.totalCompleted ?? 0n);
|
|
197
|
-
const totalFailed = Number(perQueue?.totalFailed ?? 0n);
|
|
198
|
-
return {
|
|
199
|
-
waiting,
|
|
200
|
-
prioritized,
|
|
201
|
-
delayed,
|
|
202
|
-
active,
|
|
203
|
-
completed,
|
|
204
|
-
failed,
|
|
205
|
-
'waiting-children': waitingChildrenCount,
|
|
206
|
-
totalCompleted,
|
|
207
|
-
totalFailed,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
94
|
/**
|
|
211
95
|
* Force compact all collections to reduce memory usage.
|
|
212
96
|
* Use after large batch operations or when memory pressure is high.
|
package/dist/client/tcpPool.js
CHANGED
|
@@ -50,6 +50,8 @@ export class TcpConnectionPool {
|
|
|
50
50
|
pingInterval: this.options.pingInterval,
|
|
51
51
|
maxPingFailures: this.options.maxPingFailures,
|
|
52
52
|
maxCommandTimeouts: this.options.maxCommandTimeouts,
|
|
53
|
+
pipelining: this.options.pipelining,
|
|
54
|
+
maxInFlight: this.options.maxInFlight,
|
|
53
55
|
});
|
|
54
56
|
// Socket-level errors (e.g. TLS handshake failures, protocol garbage) are
|
|
55
57
|
// emitted as 'error' events; without a listener EventEmitter would throw
|
|
@@ -190,7 +192,12 @@ function getPoolKey(options) {
|
|
|
190
192
|
// TLS config must differentiate pools too: a TLS pool and a plaintext pool
|
|
191
193
|
// to the same host:port are NOT interchangeable.
|
|
192
194
|
const tlsKey = options?.tls ? JSON.stringify(options.tls) : '0';
|
|
193
|
-
|
|
195
|
+
// pipelining/maxInFlight shape per-connection behavior: without them in the
|
|
196
|
+
// key, two Queues with different windows would silently share whichever
|
|
197
|
+
// pool was created first.
|
|
198
|
+
const pipelining = (options?.pipelining ?? true) ? '1' : '0';
|
|
199
|
+
const maxInFlight = options?.maxInFlight ?? 100;
|
|
200
|
+
return `${host}:${port}:${poolSize}:${tokenHash}:${tlsKey}:${pipelining}:${maxInFlight}`;
|
|
194
201
|
}
|
|
195
202
|
/** Get or create shared connection pool */
|
|
196
203
|
export function getSharedPool(options) {
|
|
@@ -48,9 +48,11 @@ export declare class Shard {
|
|
|
48
48
|
/** Active FIFO groups per queue */
|
|
49
49
|
readonly activeGroups: Map<string, Set<string>>;
|
|
50
50
|
constructor();
|
|
51
|
-
notify(): void;
|
|
51
|
+
notify(queue?: string): void;
|
|
52
52
|
notifyBatch(count: number): void;
|
|
53
|
+
notifyBatch(queue: string, count: number): void;
|
|
53
54
|
waitForJob(timeoutMs: number): Promise<void>;
|
|
55
|
+
waitForJob(queue: string, timeoutMs: number): Promise<void>;
|
|
54
56
|
getQueue(name: string): IndexedPriorityQueue;
|
|
55
57
|
getState(name: string): QueueState;
|
|
56
58
|
isPaused(name: string): boolean;
|
|
@@ -58,14 +58,22 @@ export class Shard {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
// ============ Waiter Management (delegated) ============
|
|
61
|
-
notify() {
|
|
62
|
-
this.waiterManager.notify();
|
|
61
|
+
notify(queue) {
|
|
62
|
+
this.waiterManager.notify(queue);
|
|
63
63
|
}
|
|
64
|
-
notifyBatch(
|
|
65
|
-
|
|
64
|
+
notifyBatch(queueOrCount, maybeCount) {
|
|
65
|
+
if (typeof queueOrCount === 'number') {
|
|
66
|
+
this.waiterManager.notifyBatch(queueOrCount);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.waiterManager.notifyBatch(queueOrCount, maybeCount ?? 0);
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
|
-
waitForJob(
|
|
68
|
-
|
|
72
|
+
waitForJob(queueOrTimeout, maybeTimeout) {
|
|
73
|
+
if (typeof queueOrTimeout === 'number') {
|
|
74
|
+
return this.waiterManager.waitForJob(queueOrTimeout);
|
|
75
|
+
}
|
|
76
|
+
return this.waiterManager.waitForJob(queueOrTimeout, maybeTimeout ?? 0);
|
|
69
77
|
}
|
|
70
78
|
// ============ Queue Operations ============
|
|
71
79
|
getQueue(name) {
|
|
@@ -87,7 +95,7 @@ export class Shard {
|
|
|
87
95
|
}
|
|
88
96
|
resume(name) {
|
|
89
97
|
this.limiterManager.resume(name);
|
|
90
|
-
this.waiterManager.notify();
|
|
98
|
+
this.waiterManager.notify(name);
|
|
91
99
|
}
|
|
92
100
|
// ============ Unique Key Management (delegated) ============
|
|
93
101
|
isUniqueAvailable(queue, key) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { JobId } from '../types/job';
|
|
2
|
+
export interface TemporalEntry {
|
|
3
|
+
createdAt: number;
|
|
4
|
+
jobId: JobId;
|
|
5
|
+
queue: string;
|
|
6
|
+
}
|
|
7
|
+
/** Queue-local temporal indexes with direct lookup by job ID. */
|
|
8
|
+
export declare class TemporalIndex {
|
|
9
|
+
private readonly indexesByQueue;
|
|
10
|
+
private readonly entriesByJobId;
|
|
11
|
+
private entryCount;
|
|
12
|
+
get size(): number;
|
|
13
|
+
add(createdAt: number, jobId: JobId, queue: string): void;
|
|
14
|
+
getOldJobs(queue: string, threshold: number, limit: number): Array<{
|
|
15
|
+
jobId: JobId;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}>;
|
|
18
|
+
remove(jobId: JobId): void;
|
|
19
|
+
clearQueue(queue: string): void;
|
|
20
|
+
cleanOrphaned(validJobIds: Set<JobId>): number;
|
|
21
|
+
clear(): void;
|
|
22
|
+
private removeFromQueueIndex;
|
|
23
|
+
private detachJobEntry;
|
|
24
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SkipList } from '../../shared/skipList';
|
|
2
|
+
const compareEntries = (a, b) => a.createdAt - b.createdAt || (a.jobId < b.jobId ? -1 : a.jobId > b.jobId ? 1 : 0);
|
|
3
|
+
const createQueueIndex = () => new SkipList(compareEntries);
|
|
4
|
+
/** Queue-local temporal indexes with direct lookup by job ID. */
|
|
5
|
+
export class TemporalIndex {
|
|
6
|
+
indexesByQueue = new Map();
|
|
7
|
+
entriesByJobId = new Map();
|
|
8
|
+
entryCount = 0;
|
|
9
|
+
get size() {
|
|
10
|
+
return this.entryCount;
|
|
11
|
+
}
|
|
12
|
+
add(createdAt, jobId, queue) {
|
|
13
|
+
const jobEntries = this.entriesByJobId.get(jobId);
|
|
14
|
+
if (jobEntries?.some((entry) => entry.createdAt === createdAt))
|
|
15
|
+
return;
|
|
16
|
+
const entry = { createdAt, jobId, queue };
|
|
17
|
+
let queueIndex = this.indexesByQueue.get(queue);
|
|
18
|
+
if (!queueIndex) {
|
|
19
|
+
queueIndex = createQueueIndex();
|
|
20
|
+
this.indexesByQueue.set(queue, queueIndex);
|
|
21
|
+
}
|
|
22
|
+
queueIndex.insert(entry);
|
|
23
|
+
if (jobEntries)
|
|
24
|
+
jobEntries.push(entry);
|
|
25
|
+
else
|
|
26
|
+
this.entriesByJobId.set(jobId, [entry]);
|
|
27
|
+
this.entryCount++;
|
|
28
|
+
}
|
|
29
|
+
getOldJobs(queue, threshold, limit) {
|
|
30
|
+
if (limit <= 0)
|
|
31
|
+
return [];
|
|
32
|
+
const queueIndex = this.indexesByQueue.get(queue);
|
|
33
|
+
if (!queueIndex)
|
|
34
|
+
return [];
|
|
35
|
+
return queueIndex
|
|
36
|
+
.takeWhile((entry) => entry.createdAt <= threshold, limit)
|
|
37
|
+
.map(({ jobId, createdAt }) => ({ jobId, createdAt }));
|
|
38
|
+
}
|
|
39
|
+
remove(jobId) {
|
|
40
|
+
const jobEntries = this.entriesByJobId.get(jobId);
|
|
41
|
+
if (!jobEntries?.length)
|
|
42
|
+
return;
|
|
43
|
+
let targetIndex = 0;
|
|
44
|
+
for (let i = 1; i < jobEntries.length; i++) {
|
|
45
|
+
if (compareEntries(jobEntries[i], jobEntries[targetIndex]) < 0)
|
|
46
|
+
targetIndex = i;
|
|
47
|
+
}
|
|
48
|
+
const [entry] = jobEntries.splice(targetIndex, 1);
|
|
49
|
+
this.removeFromQueueIndex(entry);
|
|
50
|
+
if (jobEntries.length === 0)
|
|
51
|
+
this.entriesByJobId.delete(jobId);
|
|
52
|
+
this.entryCount--;
|
|
53
|
+
}
|
|
54
|
+
clearQueue(queue) {
|
|
55
|
+
const queueIndex = this.indexesByQueue.get(queue);
|
|
56
|
+
if (!queueIndex)
|
|
57
|
+
return;
|
|
58
|
+
for (const entry of queueIndex.values())
|
|
59
|
+
this.detachJobEntry(entry);
|
|
60
|
+
this.entryCount -= queueIndex.size;
|
|
61
|
+
this.indexesByQueue.delete(queue);
|
|
62
|
+
}
|
|
63
|
+
cleanOrphaned(validJobIds) {
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const [jobId, entries] of this.entriesByJobId) {
|
|
66
|
+
if (validJobIds.has(jobId))
|
|
67
|
+
continue;
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
this.removeFromQueueIndex(entry);
|
|
70
|
+
removed++;
|
|
71
|
+
}
|
|
72
|
+
this.entriesByJobId.delete(jobId);
|
|
73
|
+
}
|
|
74
|
+
this.entryCount -= removed;
|
|
75
|
+
return removed;
|
|
76
|
+
}
|
|
77
|
+
clear() {
|
|
78
|
+
this.indexesByQueue.clear();
|
|
79
|
+
this.entriesByJobId.clear();
|
|
80
|
+
this.entryCount = 0;
|
|
81
|
+
}
|
|
82
|
+
removeFromQueueIndex(entry) {
|
|
83
|
+
const queueIndex = this.indexesByQueue.get(entry.queue);
|
|
84
|
+
if (!queueIndex)
|
|
85
|
+
return;
|
|
86
|
+
queueIndex.delete(entry);
|
|
87
|
+
if (queueIndex.isEmpty)
|
|
88
|
+
this.indexesByQueue.delete(entry.queue);
|
|
89
|
+
}
|
|
90
|
+
detachJobEntry(entry) {
|
|
91
|
+
const jobEntries = this.entriesByJobId.get(entry.jobId);
|
|
92
|
+
if (!jobEntries)
|
|
93
|
+
return;
|
|
94
|
+
const index = jobEntries.indexOf(entry);
|
|
95
|
+
if (index !== -1)
|
|
96
|
+
jobEntries.splice(index, 1);
|
|
97
|
+
if (jobEntries.length === 0)
|
|
98
|
+
this.entriesByJobId.delete(entry.jobId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -6,22 +6,12 @@
|
|
|
6
6
|
* - Delayed heap (MinHeap) for O(k) delayed job refresh
|
|
7
7
|
*/
|
|
8
8
|
import type { JobId } from '../types/job';
|
|
9
|
-
|
|
10
|
-
export interface TemporalEntry {
|
|
11
|
-
createdAt: number;
|
|
12
|
-
jobId: JobId;
|
|
13
|
-
queue: string;
|
|
14
|
-
}
|
|
9
|
+
export type { TemporalEntry } from './temporalIndex';
|
|
15
10
|
/**
|
|
16
11
|
* Manages temporal indexing for efficient job cleanup
|
|
17
12
|
* and delayed job tracking for O(k) refresh
|
|
18
13
|
*/
|
|
19
14
|
export declare class TemporalManager {
|
|
20
|
-
/**
|
|
21
|
-
* Temporal index: Skip List for O(log n) insert/delete
|
|
22
|
-
* Ordered by createdAt for efficient cleanQueue range queries
|
|
23
|
-
* Uses equality check on jobId to prevent duplicate entries for the same job
|
|
24
|
-
*/
|
|
25
15
|
private readonly temporalIndex;
|
|
26
16
|
/** Set of delayed job IDs for tracking when they become ready */
|
|
27
17
|
private readonly delayedJobIds;
|
|
@@ -65,6 +55,7 @@ export declare class TemporalManager {
|
|
|
65
55
|
* Returns number of jobs that became ready
|
|
66
56
|
*/
|
|
67
57
|
refreshDelayed(now: number): number;
|
|
58
|
+
private maybeCompactDelayedHeap;
|
|
68
59
|
/** Get delayed job count */
|
|
69
60
|
get delayedCount(): number;
|
|
70
61
|
/** Clear all delayed tracking */
|