bunqueue 2.8.32 → 2.8.34
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 +4 -3
- 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.d.ts +0 -2
- package/dist/application/operations/jobManagement.js +2 -16
- package/dist/application/operations/jobPromotion.d.ts +7 -0
- package/dist/application/operations/jobPromotion.js +58 -0
- 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 +7 -11
- package/dist/application/queueManager.js +21 -4
- 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/queue/operations/management.js +1 -11
- package/dist/domain/queue/shard.d.ts +5 -1
- package/dist/domain/queue/shard.js +20 -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/advanced.js +1 -8
- 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 +4 -3
|
@@ -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.
|
|
@@ -93,17 +93,7 @@ export async function cleanAsync(ctx, grace, limit, type) {
|
|
|
93
93
|
/** Promote delayed jobs to waiting */
|
|
94
94
|
export async function promoteJobs(ctx, opts) {
|
|
95
95
|
if (ctx.embedded) {
|
|
96
|
-
|
|
97
|
-
const manager = getSharedManager();
|
|
98
|
-
const jobs = manager.getJobs(ctx.name, { state: 'delayed' });
|
|
99
|
-
const count = opts?.count ?? jobs.length;
|
|
100
|
-
let promoted = 0;
|
|
101
|
-
for (let i = 0; i < Math.min(count, jobs.length); i++) {
|
|
102
|
-
const success = await manager.promote(jobs[i].id);
|
|
103
|
-
if (success)
|
|
104
|
-
promoted++;
|
|
105
|
-
}
|
|
106
|
-
return promoted;
|
|
96
|
+
return getSharedManager().promoteJobs(ctx.name, opts?.count);
|
|
107
97
|
}
|
|
108
98
|
const response = await ctx.tcp.send({
|
|
109
99
|
cmd: 'PromoteJobs',
|
|
@@ -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;
|
|
@@ -114,6 +116,8 @@ export declare class Shard {
|
|
|
114
116
|
};
|
|
115
117
|
incrementQueued(jobId: JobId, isDelayed: boolean, createdAt?: number, queue?: string, runAt?: number): void;
|
|
116
118
|
decrementQueued(jobId: JobId): void;
|
|
119
|
+
/** Remove a promoted job from delayed tracking without changing queue depth. */
|
|
120
|
+
promoteDelayed(jobId: JobId): void;
|
|
117
121
|
incrementDlq(): void;
|
|
118
122
|
decrementDlq(count?: number): void;
|
|
119
123
|
refreshDelayedCount(now: number): void;
|
|
@@ -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) {
|
|
@@ -294,6 +302,11 @@ export class Shard {
|
|
|
294
302
|
decrementQueued(jobId) {
|
|
295
303
|
this.counters.decrementQueued(jobId);
|
|
296
304
|
}
|
|
305
|
+
/** Remove a promoted job from delayed tracking without changing queue depth. */
|
|
306
|
+
promoteDelayed(jobId) {
|
|
307
|
+
this.temporalManager.removeDelayed(jobId);
|
|
308
|
+
this.counters.syncDelayedCount();
|
|
309
|
+
}
|
|
297
310
|
incrementDlq() {
|
|
298
311
|
this.counters.incrementDlq();
|
|
299
312
|
}
|
|
@@ -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 */
|
|
@@ -5,29 +5,15 @@
|
|
|
5
5
|
* - Temporal index (SkipList) for efficient cleanQueue range queries
|
|
6
6
|
* - Delayed heap (MinHeap) for O(k) delayed job refresh
|
|
7
7
|
*/
|
|
8
|
-
import { SkipList } from '../../shared/skipList';
|
|
9
8
|
import { MinHeap } from '../../shared/minHeap';
|
|
9
|
+
import { TemporalIndex } from './temporalIndex';
|
|
10
|
+
const DELAYED_COMPACTION_MIN_STALE = 256;
|
|
10
11
|
/**
|
|
11
12
|
* Manages temporal indexing for efficient job cleanup
|
|
12
13
|
* and delayed job tracking for O(k) refresh
|
|
13
14
|
*/
|
|
14
15
|
export class TemporalManager {
|
|
15
|
-
|
|
16
|
-
* Temporal index: Skip List for O(log n) insert/delete
|
|
17
|
-
* Ordered by createdAt for efficient cleanQueue range queries
|
|
18
|
-
* Uses equality check on jobId to prevent duplicate entries for the same job
|
|
19
|
-
*/
|
|
20
|
-
// Total-order comparator: createdAt first, then jobId as a tie-break. Without
|
|
21
|
-
// the tie-break, a batch of jobs sharing one createdAt (the addBulk case —
|
|
22
|
-
// `now` is captured once) makes every node compare-equal, which (a) turns
|
|
23
|
-
// SkipList.insert's duplicate-check scan into O(n) per insert => O(n²) for the
|
|
24
|
-
// batch, and (b) makes SkipList.delete remove the WRONG same-createdAt node
|
|
25
|
-
// (it stops at the first compare-equal node). A total order fixes both: the
|
|
26
|
-
// dedup scan and delete both resolve to the exact (createdAt, jobId) node in
|
|
27
|
-
// O(log n). jobId is a string (UUIDv7 by default, or any custom id), so its
|
|
28
|
-
// lexicographic comparison is a valid total order in every case. Equality is
|
|
29
|
-
// still by jobId (now reached only for a true duplicate).
|
|
30
|
-
temporalIndex = new SkipList((a, b) => a.createdAt - b.createdAt || (a.jobId < b.jobId ? -1 : a.jobId > b.jobId ? 1 : 0), 16, 0.5, (a, b) => a.jobId === b.jobId);
|
|
16
|
+
temporalIndex = new TemporalIndex();
|
|
31
17
|
/** Set of delayed job IDs for tracking when they become ready */
|
|
32
18
|
delayedJobIds = new Set();
|
|
33
19
|
/**
|
|
@@ -40,7 +26,7 @@ export class TemporalManager {
|
|
|
40
26
|
// ============ Temporal Index Operations ============
|
|
41
27
|
/** Add job to temporal index - O(log n) */
|
|
42
28
|
addToIndex(createdAt, jobId, queue) {
|
|
43
|
-
this.temporalIndex.
|
|
29
|
+
this.temporalIndex.add(createdAt, jobId, queue);
|
|
44
30
|
}
|
|
45
31
|
/**
|
|
46
32
|
* Get old jobs from temporal index - O(log n + k)
|
|
@@ -49,36 +35,22 @@ export class TemporalManager {
|
|
|
49
35
|
getOldJobs(queue, thresholdMs, limit) {
|
|
50
36
|
const now = Date.now();
|
|
51
37
|
const threshold = now - thresholdMs;
|
|
52
|
-
|
|
53
|
-
for (const entry of this.temporalIndex.values()) {
|
|
54
|
-
if (entry.createdAt > threshold)
|
|
55
|
-
break;
|
|
56
|
-
if (entry.queue === queue) {
|
|
57
|
-
result.push({ jobId: entry.jobId, createdAt: entry.createdAt });
|
|
58
|
-
if (result.length >= limit)
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
38
|
+
return this.temporalIndex.getOldJobs(queue, threshold, limit);
|
|
63
39
|
}
|
|
64
40
|
/** Remove job from temporal index */
|
|
65
41
|
removeFromIndex(jobId) {
|
|
66
|
-
this.temporalIndex.
|
|
42
|
+
this.temporalIndex.remove(jobId);
|
|
67
43
|
}
|
|
68
44
|
/** Clear temporal index for a queue */
|
|
69
45
|
clearIndexForQueue(queue) {
|
|
70
|
-
this.temporalIndex.
|
|
46
|
+
this.temporalIndex.clearQueue(queue);
|
|
71
47
|
}
|
|
72
48
|
/**
|
|
73
49
|
* Clean orphaned temporal index entries.
|
|
74
50
|
* Removes entries for jobs that no longer exist.
|
|
75
51
|
*/
|
|
76
52
|
cleanOrphaned(validJobIds) {
|
|
77
|
-
|
|
78
|
-
return 0;
|
|
79
|
-
const beforeSize = this.temporalIndex.size;
|
|
80
|
-
this.temporalIndex.removeAll((e) => !validJobIds.has(e.jobId));
|
|
81
|
-
return beforeSize - this.temporalIndex.size;
|
|
53
|
+
return this.temporalIndex.cleanOrphaned(validJobIds);
|
|
82
54
|
}
|
|
83
55
|
/** Get temporal index size */
|
|
84
56
|
get indexSize() {
|
|
@@ -94,15 +66,15 @@ export class TemporalManager {
|
|
|
94
66
|
this.delayedJobIds.add(jobId);
|
|
95
67
|
this.delayedHeap.push({ jobId, runAt });
|
|
96
68
|
this.delayedRunAt.set(jobId, runAt);
|
|
69
|
+
this.maybeCompactDelayedHeap();
|
|
97
70
|
}
|
|
98
71
|
/** Remove a delayed job (lazy removal from heap) */
|
|
99
72
|
removeDelayed(jobId) {
|
|
100
|
-
if (this.delayedJobIds.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return false;
|
|
73
|
+
if (!this.delayedJobIds.delete(jobId))
|
|
74
|
+
return false;
|
|
75
|
+
this.delayedRunAt.delete(jobId);
|
|
76
|
+
this.maybeCompactDelayedHeap();
|
|
77
|
+
return true;
|
|
106
78
|
}
|
|
107
79
|
/**
|
|
108
80
|
* Refresh delayed jobs that have become ready
|
|
@@ -127,8 +99,20 @@ export class TemporalManager {
|
|
|
127
99
|
this.delayedRunAt.delete(top.jobId);
|
|
128
100
|
count++;
|
|
129
101
|
}
|
|
102
|
+
this.maybeCompactDelayedHeap();
|
|
130
103
|
return count;
|
|
131
104
|
}
|
|
105
|
+
maybeCompactDelayedHeap() {
|
|
106
|
+
const liveCount = this.delayedRunAt.size;
|
|
107
|
+
if (liveCount === 0) {
|
|
108
|
+
this.delayedHeap.clear();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const staleCount = this.delayedHeap.size - liveCount;
|
|
112
|
+
if (staleCount < DELAYED_COMPACTION_MIN_STALE || staleCount < liveCount)
|
|
113
|
+
return;
|
|
114
|
+
this.delayedHeap.buildFrom(Array.from(this.delayedRunAt, ([jobId, runAt]) => ({ jobId, runAt })));
|
|
115
|
+
}
|
|
132
116
|
/** Get delayed job count */
|
|
133
117
|
get delayedCount() {
|
|
134
118
|
return this.delayedJobIds.size;
|
|
@@ -143,8 +127,7 @@ export class TemporalManager {
|
|
|
143
127
|
/** Clear all data */
|
|
144
128
|
clear() {
|
|
145
129
|
this.clearDelayed();
|
|
146
|
-
|
|
147
|
-
this.temporalIndex.removeAll(() => true);
|
|
130
|
+
this.temporalIndex.clear();
|
|
148
131
|
}
|
|
149
132
|
// ============ Debug Info ============
|
|
150
133
|
/** Get internal structure sizes for memory debugging */
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WaiterManager - Manages job availability notifications
|
|
3
|
-
* Handles worker polling with timeout-based waiting
|
|
2
|
+
* WaiterManager - Manages queue-scoped job availability notifications.
|
|
3
|
+
* Handles worker polling with timeout-based waiting.
|
|
4
4
|
*/
|
|
5
5
|
export declare class WaiterManager {
|
|
6
|
-
|
|
7
|
-
private
|
|
8
|
-
|
|
9
|
-
private pendingNotifications;
|
|
10
|
-
/** Notify that multiple jobs are available - wakes up to `count` waiters */
|
|
6
|
+
private readonly queues;
|
|
7
|
+
private activeWaiters;
|
|
8
|
+
notify(queue?: string): void;
|
|
11
9
|
notifyBatch(count: number): void;
|
|
12
|
-
|
|
13
|
-
notify(): void;
|
|
14
|
-
/** Wait for a job to become available (with timeout) */
|
|
10
|
+
notifyBatch(queue: string, count: number): void;
|
|
15
11
|
waitForJob(timeoutMs: number): Promise<void>;
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
12
|
+
waitForJob(queue: string, timeoutMs: number): Promise<void>;
|
|
13
|
+
private notifyQueue;
|
|
14
|
+
private createQueue;
|
|
15
|
+
private compact;
|
|
16
|
+
private deleteIdleQueue;
|
|
19
17
|
get length(): number;
|
|
20
18
|
}
|