bunqueue 2.8.34 → 2.8.35
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 -2
- package/dist/application/queueManager.d.ts +1 -1
- package/dist/application/queueManager.js +13 -4
- package/dist/client/queue/dlq.d.ts +23 -1
- package/dist/client/queue/dlq.js +75 -0
- package/dist/client/queue/jobProxy.d.ts +1 -1
- package/dist/client/queue/operations/control.d.ts +17 -0
- package/dist/client/queue/operations/control.js +41 -0
- package/dist/client/queue/queue.d.ts +13 -0
- package/dist/client/queue/queue.js +39 -0
- package/dist/client/queue/rateLimit.d.ts +18 -3
- package/dist/client/queue/rateLimit.js +45 -10
- package/dist/client/queue/stall.d.ts +2 -0
- package/dist/client/queue/stall.js +12 -0
- package/dist/domain/queue/limiterManager.d.ts +14 -2
- package/dist/domain/queue/limiterManager.js +32 -5
- package/dist/domain/queue/shard.d.ts +2 -1
- package/dist/domain/queue/shard.js +5 -2
- package/dist/domain/types/command.d.ts +4 -0
- package/dist/domain/types/queue.d.ts +4 -0
- package/dist/domain/types/queue.js +2 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +15 -2
- package/dist/infrastructure/persistence/sqlite.d.ts +9 -1
- package/dist/infrastructure/persistence/sqlite.js +4 -2
- package/dist/infrastructure/persistence/statements.d.ts +2 -0
- package/dist/infrastructure/persistence/statements.js +2 -2
- package/dist/infrastructure/scheduler/cronScheduler.d.ts +5 -0
- package/dist/infrastructure/scheduler/cronScheduler.js +39 -13
- package/dist/infrastructure/server/handlers/advanced.js +11 -2
- package/dist/infrastructure/server/httpRouteQueueConfig.js +2 -0
- package/package.json +1 -1
|
@@ -342,8 +342,15 @@ export function recover(ctx) {
|
|
|
342
342
|
const shard = ctx.shards[shardIndex(qs.name)];
|
|
343
343
|
if (qs.paused)
|
|
344
344
|
shard.pause(qs.name);
|
|
345
|
-
if (qs.rateLimit !== null)
|
|
346
|
-
|
|
345
|
+
if (qs.rateLimit !== null) {
|
|
346
|
+
// TTL'd limits are temporary by definition: an already-expired one must
|
|
347
|
+
// not resurrect as permanent. A still-live one is restored with its
|
|
348
|
+
// remaining TTL so it keeps expiring broker-side.
|
|
349
|
+
const remainingTtl = qs.rateLimitExpiresAt === null ? undefined : qs.rateLimitExpiresAt - Date.now();
|
|
350
|
+
if (remainingTtl === undefined || remainingTtl > 0) {
|
|
351
|
+
shard.setRateLimit(qs.name, qs.rateLimit, qs.rateLimitDuration ?? undefined, remainingTtl);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
347
354
|
if (qs.concurrencyLimit !== null)
|
|
348
355
|
shard.setConcurrency(qs.name, qs.concurrencyLimit);
|
|
349
356
|
ctx.registerQueueName(qs.name);
|
|
@@ -196,7 +196,7 @@ export declare class QueueManager {
|
|
|
196
196
|
retryDlq(queue: string, jobId?: JobId, limit?: number): number;
|
|
197
197
|
purgeDlq(queue: string): number;
|
|
198
198
|
retryCompleted(queue: string, jobId?: JobId): number;
|
|
199
|
-
setRateLimit(queue: string, limit: number): void;
|
|
199
|
+
setRateLimit(queue: string, limit: number, durationMs?: number, ttlMs?: number): void;
|
|
200
200
|
clearRateLimit(queue: string): void;
|
|
201
201
|
setConcurrency(queue: string, limit: number): void;
|
|
202
202
|
clearConcurrency(queue: string): void;
|
|
@@ -860,8 +860,8 @@ export class QueueManager {
|
|
|
860
860
|
return dlqOps.retryCompletedJobs(queue, this.contextFactory.getRetryCompletedContext(), jobId);
|
|
861
861
|
}
|
|
862
862
|
// ============ Rate Limiting ============
|
|
863
|
-
setRateLimit(queue, limit) {
|
|
864
|
-
this.shards[shardIndex(queue)].setRateLimit(queue, limit);
|
|
863
|
+
setRateLimit(queue, limit, durationMs, ttlMs) {
|
|
864
|
+
this.shards[shardIndex(queue)].setRateLimit(queue, limit, durationMs, ttlMs);
|
|
865
865
|
this.persistQueueState(queue);
|
|
866
866
|
}
|
|
867
867
|
clearRateLimit(queue) {
|
|
@@ -893,11 +893,20 @@ export class QueueManager {
|
|
|
893
893
|
this.storage.deleteQueueState(queue);
|
|
894
894
|
return;
|
|
895
895
|
}
|
|
896
|
-
this.storage.saveQueueState(queue,
|
|
896
|
+
this.storage.saveQueueState(queue, {
|
|
897
|
+
paused: state.paused,
|
|
898
|
+
rateLimit: state.rateLimit,
|
|
899
|
+
concurrencyLimit: state.concurrencyLimit,
|
|
900
|
+
rateLimitDuration: state.rateLimitDuration,
|
|
901
|
+
rateLimitExpiresAt: state.rateLimitExpiresAt,
|
|
902
|
+
});
|
|
897
903
|
}
|
|
898
904
|
/** Get rate limit and concurrency limit for a queue */
|
|
899
905
|
getQueueLimits(queue) {
|
|
900
|
-
const
|
|
906
|
+
const shard = this.shards[shardIndex(queue)];
|
|
907
|
+
// Lazy TTL expiry so reads never report a limit that no longer throttles.
|
|
908
|
+
shard.expireRateLimitIfNeeded(queue);
|
|
909
|
+
const state = shard.getState(queue);
|
|
901
910
|
return { rateLimit: state.rateLimit, concurrencyLimit: state.concurrencyLimit };
|
|
902
911
|
}
|
|
903
912
|
/** Get all job results (for cloud telemetry) */
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Wraps dlqOps for Queue class usage
|
|
4
4
|
*/
|
|
5
5
|
import type { TcpConnectionPool } from '../tcpPool';
|
|
6
|
-
import type { DlqConfig, DlqEntry, DlqStats, DlqFilter } from '../types';
|
|
6
|
+
import type { Job, DlqConfig, DlqEntry, DlqStats, DlqFilter } from '../types';
|
|
7
|
+
import { type SimpleJobContext } from './jobProxy';
|
|
7
8
|
interface DlqContext {
|
|
8
9
|
name: string;
|
|
9
10
|
embedded: boolean;
|
|
@@ -11,20 +12,41 @@ interface DlqContext {
|
|
|
11
12
|
}
|
|
12
13
|
/** Set DLQ configuration */
|
|
13
14
|
export declare function setDlqConfig(ctx: DlqContext, config: Partial<DlqConfig>): void;
|
|
15
|
+
/** Set DLQ configuration and resolve once the server has applied it. */
|
|
16
|
+
export declare function setDlqConfigAsync(ctx: DlqContext, config: Partial<DlqConfig>): Promise<void>;
|
|
14
17
|
/** Get DLQ configuration */
|
|
15
18
|
export declare function getDlqConfig(ctx: DlqContext): DlqConfig;
|
|
16
19
|
/** Get DLQ configuration (async, works in TCP mode) */
|
|
17
20
|
export declare function getDlqConfigAsync(ctx: DlqContext): Promise<DlqConfig>;
|
|
18
21
|
/** Get DLQ entries */
|
|
19
22
|
export declare function getDlq<T>(ctx: DlqContext, filter?: DlqFilter): DlqEntry<T>[];
|
|
23
|
+
type DlqQueryContext = DlqContext & Pick<SimpleJobContext, 'getJobState' | 'removeAsync' | 'retryJob' | 'getChildrenValues'>;
|
|
24
|
+
/**
|
|
25
|
+
* Get the dead jobs in the DLQ, working over TCP too (wire command `Dlq`).
|
|
26
|
+
* Returns public Job objects; the wire does not carry DlqEntry metadata
|
|
27
|
+
* (reason, enteredAt, attempts) — that stays embedded-only via getDlq().
|
|
28
|
+
*/
|
|
29
|
+
export declare function getDlqJobsAsync<T>(ctx: DlqQueryContext, count?: number): Promise<Job<T>[]>;
|
|
20
30
|
/** Get DLQ stats */
|
|
21
31
|
export declare function getDlqStats(ctx: DlqContext): DlqStats;
|
|
22
32
|
/** Retry DLQ entries */
|
|
23
33
|
export declare function retryDlq(ctx: DlqContext, id?: string): number;
|
|
34
|
+
/**
|
|
35
|
+
* Retry DLQ entries and resolve with the retried count once the server has
|
|
36
|
+
* processed it. The fire-and-forget retryDlq() always returns 0 over TCP and
|
|
37
|
+
* discards the server's count.
|
|
38
|
+
*/
|
|
39
|
+
export declare function retryDlqAsync(ctx: DlqContext, id?: string): Promise<number>;
|
|
24
40
|
/** Retry DLQ entries by filter */
|
|
25
41
|
export declare function retryDlqByFilter(ctx: DlqContext, filter: DlqFilter): number;
|
|
26
42
|
/** Purge DLQ */
|
|
27
43
|
export declare function purgeDlq(ctx: DlqContext): number;
|
|
44
|
+
/**
|
|
45
|
+
* Purge the DLQ and resolve with the purged count once the server has
|
|
46
|
+
* processed it. The fire-and-forget purgeDlq() always returns 0 over TCP;
|
|
47
|
+
* purge-then-assert-empty patterns need this variant.
|
|
48
|
+
*/
|
|
49
|
+
export declare function purgeDlqAsync(ctx: DlqContext): Promise<number>;
|
|
28
50
|
/** Retry completed job */
|
|
29
51
|
export declare function retryCompleted(ctx: DlqContext, id?: string): number;
|
|
30
52
|
/** Retry completed job (async) */
|
package/dist/client/queue/dlq.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getSharedManager } from '../manager';
|
|
6
6
|
import { jobId } from '../../domain/types/job';
|
|
7
|
+
import { createSimpleJob } from './jobProxy';
|
|
7
8
|
import * as dlqOps from './dlqOps';
|
|
8
9
|
/** Client-side cache for TCP mode */
|
|
9
10
|
const tcpDlqConfigCache = new Map();
|
|
@@ -18,6 +19,18 @@ export function setDlqConfig(ctx, config) {
|
|
|
18
19
|
void ctx.tcp.send({ cmd: 'SetDlqConfig', queue: ctx.name, config });
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
/** Set DLQ configuration and resolve once the server has applied it. */
|
|
23
|
+
export async function setDlqConfigAsync(ctx, config) {
|
|
24
|
+
if (ctx.embedded) {
|
|
25
|
+
dlqOps.setDlqConfig(ctx.name, config);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!ctx.tcp)
|
|
29
|
+
return;
|
|
30
|
+
const current = tcpDlqConfigCache.get(ctx.name) ?? {};
|
|
31
|
+
tcpDlqConfigCache.set(ctx.name, { ...current, ...config });
|
|
32
|
+
await ctx.tcp.send({ cmd: 'SetDlqConfig', queue: ctx.name, config });
|
|
33
|
+
}
|
|
21
34
|
/** Get DLQ configuration */
|
|
22
35
|
export function getDlqConfig(ctx) {
|
|
23
36
|
if (ctx.embedded)
|
|
@@ -45,6 +58,38 @@ export function getDlq(ctx, filter) {
|
|
|
45
58
|
return [];
|
|
46
59
|
return dlqOps.getDlqEntries(ctx.name, filter);
|
|
47
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the dead jobs in the DLQ, working over TCP too (wire command `Dlq`).
|
|
63
|
+
* Returns public Job objects; the wire does not carry DlqEntry metadata
|
|
64
|
+
* (reason, enteredAt, attempts) — that stays embedded-only via getDlq().
|
|
65
|
+
*/
|
|
66
|
+
export async function getDlqJobsAsync(ctx, count) {
|
|
67
|
+
let raw;
|
|
68
|
+
if (ctx.embedded) {
|
|
69
|
+
raw = getSharedManager().getDlq(ctx.name, count);
|
|
70
|
+
}
|
|
71
|
+
else if (ctx.tcp) {
|
|
72
|
+
const response = await ctx.tcp.send({ cmd: 'Dlq', queue: ctx.name, count });
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
return [];
|
|
75
|
+
raw = (response.jobs ?? []);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return raw.map((job) => {
|
|
81
|
+
const name = job.data?.name ?? 'unknown';
|
|
82
|
+
return createSimpleJob(String(job.id), name, job.data, job.createdAt ?? Date.now(), {
|
|
83
|
+
queueName: ctx.name,
|
|
84
|
+
embedded: ctx.embedded,
|
|
85
|
+
tcp: ctx.tcp,
|
|
86
|
+
getJobState: ctx.getJobState,
|
|
87
|
+
removeAsync: ctx.removeAsync,
|
|
88
|
+
retryJob: ctx.retryJob,
|
|
89
|
+
getChildrenValues: ctx.getChildrenValues,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
48
93
|
/** Get DLQ stats */
|
|
49
94
|
export function getDlqStats(ctx) {
|
|
50
95
|
if (!ctx.embedded) {
|
|
@@ -67,6 +112,21 @@ export function retryDlq(ctx, id) {
|
|
|
67
112
|
void ctx.tcp.send({ cmd: 'RetryDlq', queue: ctx.name, jobId: id });
|
|
68
113
|
return 0;
|
|
69
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Retry DLQ entries and resolve with the retried count once the server has
|
|
117
|
+
* processed it. The fire-and-forget retryDlq() always returns 0 over TCP and
|
|
118
|
+
* discards the server's count.
|
|
119
|
+
*/
|
|
120
|
+
export async function retryDlqAsync(ctx, id) {
|
|
121
|
+
if (ctx.embedded)
|
|
122
|
+
return dlqOps.retryDlqEmbedded(ctx.name, id);
|
|
123
|
+
if (!ctx.tcp)
|
|
124
|
+
return 0;
|
|
125
|
+
const response = await ctx.tcp.send({ cmd: 'RetryDlq', queue: ctx.name, jobId: id });
|
|
126
|
+
if (!response.ok)
|
|
127
|
+
return 0;
|
|
128
|
+
return (response.count ?? 0);
|
|
129
|
+
}
|
|
70
130
|
/** Retry DLQ entries by filter */
|
|
71
131
|
export function retryDlqByFilter(ctx, filter) {
|
|
72
132
|
if (!ctx.embedded)
|
|
@@ -81,6 +141,21 @@ export function purgeDlq(ctx) {
|
|
|
81
141
|
void ctx.tcp.send({ cmd: 'PurgeDlq', queue: ctx.name });
|
|
82
142
|
return 0;
|
|
83
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Purge the DLQ and resolve with the purged count once the server has
|
|
146
|
+
* processed it. The fire-and-forget purgeDlq() always returns 0 over TCP;
|
|
147
|
+
* purge-then-assert-empty patterns need this variant.
|
|
148
|
+
*/
|
|
149
|
+
export async function purgeDlqAsync(ctx) {
|
|
150
|
+
if (ctx.embedded)
|
|
151
|
+
return dlqOps.purgeDlqEmbedded(ctx.name);
|
|
152
|
+
if (!ctx.tcp)
|
|
153
|
+
return 0;
|
|
154
|
+
const response = await ctx.tcp.send({ cmd: 'PurgeDlq', queue: ctx.name });
|
|
155
|
+
if (!response.ok)
|
|
156
|
+
return 0;
|
|
157
|
+
return (response.count ?? 0);
|
|
158
|
+
}
|
|
84
159
|
/** Retry completed job */
|
|
85
160
|
export function retryCompleted(ctx, id) {
|
|
86
161
|
if (ctx.embedded) {
|
|
@@ -29,7 +29,7 @@ export interface JobReflectionMeta {
|
|
|
29
29
|
}
|
|
30
30
|
/** Create a full Job proxy with TCP methods */
|
|
31
31
|
export declare function createJobProxy<T>(id: string, name: string, data: T, ctx: JobProxyContext, meta?: JobReflectionMeta): Job<T>;
|
|
32
|
-
interface SimpleJobContext {
|
|
32
|
+
export interface SimpleJobContext {
|
|
33
33
|
queueName: string;
|
|
34
34
|
/** Execution mode — determines whether to use embedded manager or TCP */
|
|
35
35
|
embedded?: boolean;
|
|
@@ -10,12 +10,29 @@ interface ControlContext {
|
|
|
10
10
|
}
|
|
11
11
|
/** Pause the queue */
|
|
12
12
|
export declare function pause(ctx: ControlContext): void;
|
|
13
|
+
/** Pause the queue and resolve once the server has processed it. */
|
|
14
|
+
export declare function pauseAsync(ctx: ControlContext): Promise<void>;
|
|
13
15
|
/** Resume the queue */
|
|
14
16
|
export declare function resume(ctx: ControlContext): void;
|
|
17
|
+
/** Resume the queue and resolve once the server has processed it. */
|
|
18
|
+
export declare function resumeAsync(ctx: ControlContext): Promise<void>;
|
|
15
19
|
/** Drain the queue (remove all waiting jobs) */
|
|
16
20
|
export declare function drain(ctx: ControlContext): void;
|
|
21
|
+
/**
|
|
22
|
+
* Drain the queue and resolve with the number of removed jobs once the
|
|
23
|
+
* server has processed it. Same ordering guarantee as obliterateAsync():
|
|
24
|
+
* jobs added after resolution cannot be caught by a late-arriving drain.
|
|
25
|
+
*/
|
|
26
|
+
export declare function drainAsync(ctx: ControlContext): Promise<number>;
|
|
17
27
|
/** Obliterate the queue (remove all jobs and data) */
|
|
18
28
|
export declare function obliterate(ctx: ControlContext): void;
|
|
29
|
+
/**
|
|
30
|
+
* Obliterate the queue and resolve once the server has processed it.
|
|
31
|
+
* The fire-and-forget obliterate() gives no ordering guarantee over a
|
|
32
|
+
* multi-connection pool: a PUSH sent right after it can be processed first
|
|
33
|
+
* and then wiped. Await this variant before enqueuing follow-up jobs.
|
|
34
|
+
*/
|
|
35
|
+
export declare function obliterateAsync(ctx: ControlContext): Promise<void>;
|
|
19
36
|
/** Check if queue is paused (sync, embedded only) */
|
|
20
37
|
export declare function isPaused(ctx: ControlContext): boolean;
|
|
21
38
|
/** Check if queue is paused (async, works with TCP) */
|
|
@@ -10,6 +10,13 @@ export function pause(ctx) {
|
|
|
10
10
|
else if (ctx.tcp)
|
|
11
11
|
void ctx.tcp.send({ cmd: 'Pause', queue: ctx.name });
|
|
12
12
|
}
|
|
13
|
+
/** Pause the queue and resolve once the server has processed it. */
|
|
14
|
+
export async function pauseAsync(ctx) {
|
|
15
|
+
if (ctx.embedded)
|
|
16
|
+
getSharedManager().pause(ctx.name);
|
|
17
|
+
else if (ctx.tcp)
|
|
18
|
+
await ctx.tcp.send({ cmd: 'Pause', queue: ctx.name });
|
|
19
|
+
}
|
|
13
20
|
/** Resume the queue */
|
|
14
21
|
export function resume(ctx) {
|
|
15
22
|
if (ctx.embedded)
|
|
@@ -17,6 +24,13 @@ export function resume(ctx) {
|
|
|
17
24
|
else if (ctx.tcp)
|
|
18
25
|
void ctx.tcp.send({ cmd: 'Resume', queue: ctx.name });
|
|
19
26
|
}
|
|
27
|
+
/** Resume the queue and resolve once the server has processed it. */
|
|
28
|
+
export async function resumeAsync(ctx) {
|
|
29
|
+
if (ctx.embedded)
|
|
30
|
+
getSharedManager().resume(ctx.name);
|
|
31
|
+
else if (ctx.tcp)
|
|
32
|
+
await ctx.tcp.send({ cmd: 'Resume', queue: ctx.name });
|
|
33
|
+
}
|
|
20
34
|
/** Drain the queue (remove all waiting jobs) */
|
|
21
35
|
export function drain(ctx) {
|
|
22
36
|
if (ctx.embedded)
|
|
@@ -24,6 +38,21 @@ export function drain(ctx) {
|
|
|
24
38
|
else if (ctx.tcp)
|
|
25
39
|
void ctx.tcp.send({ cmd: 'Drain', queue: ctx.name });
|
|
26
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Drain the queue and resolve with the number of removed jobs once the
|
|
43
|
+
* server has processed it. Same ordering guarantee as obliterateAsync():
|
|
44
|
+
* jobs added after resolution cannot be caught by a late-arriving drain.
|
|
45
|
+
*/
|
|
46
|
+
export async function drainAsync(ctx) {
|
|
47
|
+
if (ctx.embedded)
|
|
48
|
+
return getSharedManager().drain(ctx.name);
|
|
49
|
+
if (!ctx.tcp)
|
|
50
|
+
return 0;
|
|
51
|
+
const response = await ctx.tcp.send({ cmd: 'Drain', queue: ctx.name });
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return 0;
|
|
54
|
+
return (response.count ?? 0);
|
|
55
|
+
}
|
|
27
56
|
/** Obliterate the queue (remove all jobs and data) */
|
|
28
57
|
export function obliterate(ctx) {
|
|
29
58
|
if (ctx.embedded)
|
|
@@ -31,6 +60,18 @@ export function obliterate(ctx) {
|
|
|
31
60
|
else if (ctx.tcp)
|
|
32
61
|
void ctx.tcp.send({ cmd: 'Obliterate', queue: ctx.name });
|
|
33
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Obliterate the queue and resolve once the server has processed it.
|
|
65
|
+
* The fire-and-forget obliterate() gives no ordering guarantee over a
|
|
66
|
+
* multi-connection pool: a PUSH sent right after it can be processed first
|
|
67
|
+
* and then wiped. Await this variant before enqueuing follow-up jobs.
|
|
68
|
+
*/
|
|
69
|
+
export async function obliterateAsync(ctx) {
|
|
70
|
+
if (ctx.embedded)
|
|
71
|
+
getSharedManager().obliterate(ctx.name);
|
|
72
|
+
else if (ctx.tcp)
|
|
73
|
+
await ctx.tcp.send({ cmd: 'Obliterate', queue: ctx.name });
|
|
74
|
+
}
|
|
34
75
|
/** Check if queue is paused (sync, embedded only) */
|
|
35
76
|
export function isPaused(ctx) {
|
|
36
77
|
if (!ctx.embedded)
|
|
@@ -74,9 +74,13 @@ export declare class Queue<T = unknown> {
|
|
|
74
74
|
getCountsPerPriority(): Record<number, number>;
|
|
75
75
|
getCountsPerPriorityAsync(): Promise<Record<number, number>>;
|
|
76
76
|
pause(): void;
|
|
77
|
+
pauseAsync(): Promise<void>;
|
|
77
78
|
resume(): void;
|
|
79
|
+
resumeAsync(): Promise<void>;
|
|
78
80
|
drain(): void;
|
|
81
|
+
drainAsync(): Promise<number>;
|
|
79
82
|
obliterate(): void;
|
|
83
|
+
obliterateAsync(): Promise<void>;
|
|
80
84
|
isPaused(): boolean;
|
|
81
85
|
isPausedAsync(): Promise<boolean>;
|
|
82
86
|
waitUntilReady(): Promise<void>;
|
|
@@ -106,23 +110,32 @@ export declare class Queue<T = unknown> {
|
|
|
106
110
|
changeJobPriority(id: string, opts: ChangePriorityOpts): Promise<void>;
|
|
107
111
|
extendJobLock(id: string, token: string, duration: number): Promise<number>;
|
|
108
112
|
setStallConfig(config: Partial<StallConfig>): void;
|
|
113
|
+
setStallConfigAsync(config: Partial<StallConfig>): Promise<void>;
|
|
109
114
|
getStallConfig(): StallConfig;
|
|
110
115
|
getStallConfigAsync(): Promise<StallConfig>;
|
|
111
116
|
setDlqConfig(config: Partial<DlqConfig>): void;
|
|
117
|
+
setDlqConfigAsync(config: Partial<DlqConfig>): Promise<void>;
|
|
112
118
|
getDlqConfig(): DlqConfig;
|
|
113
119
|
getDlqConfigAsync(): Promise<DlqConfig>;
|
|
114
120
|
getDlq(filter?: DlqFilter): DlqEntry<T>[];
|
|
121
|
+
getDlqJobsAsync(count?: number): Promise<Job<T>[]>;
|
|
115
122
|
getDlqStats(): DlqStats;
|
|
116
123
|
retryDlq(id?: string): number;
|
|
124
|
+
retryDlqAsync(id?: string): Promise<number>;
|
|
117
125
|
retryDlqByFilter(filter: DlqFilter): number;
|
|
118
126
|
purgeDlq(): number;
|
|
127
|
+
purgeDlqAsync(): Promise<number>;
|
|
119
128
|
retryCompleted(id?: string): number;
|
|
120
129
|
retryCompletedAsync(id?: string): Promise<number>;
|
|
121
130
|
setGlobalConcurrency(concurrency: number): void;
|
|
131
|
+
setGlobalConcurrencyAsync(concurrency: number): Promise<void>;
|
|
122
132
|
removeGlobalConcurrency(): void;
|
|
133
|
+
removeGlobalConcurrencyAsync(): Promise<void>;
|
|
123
134
|
getGlobalConcurrency(): Promise<number | null>;
|
|
124
135
|
setGlobalRateLimit(max: number, duration?: number): void;
|
|
136
|
+
setGlobalRateLimitAsync(max: number, duration?: number): Promise<void>;
|
|
125
137
|
removeGlobalRateLimit(): void;
|
|
138
|
+
removeGlobalRateLimitAsync(): Promise<void>;
|
|
126
139
|
getGlobalRateLimit(): Promise<{
|
|
127
140
|
max: number;
|
|
128
141
|
duration: number;
|
|
@@ -262,15 +262,27 @@ export class Queue {
|
|
|
262
262
|
pause() {
|
|
263
263
|
controlOps.pause(this.ctx);
|
|
264
264
|
}
|
|
265
|
+
pauseAsync() {
|
|
266
|
+
return controlOps.pauseAsync(this.ctx);
|
|
267
|
+
}
|
|
265
268
|
resume() {
|
|
266
269
|
controlOps.resume(this.ctx);
|
|
267
270
|
}
|
|
271
|
+
resumeAsync() {
|
|
272
|
+
return controlOps.resumeAsync(this.ctx);
|
|
273
|
+
}
|
|
268
274
|
drain() {
|
|
269
275
|
controlOps.drain(this.ctx);
|
|
270
276
|
}
|
|
277
|
+
drainAsync() {
|
|
278
|
+
return controlOps.drainAsync(this.ctx);
|
|
279
|
+
}
|
|
271
280
|
obliterate() {
|
|
272
281
|
controlOps.obliterate(this.ctx);
|
|
273
282
|
}
|
|
283
|
+
obliterateAsync() {
|
|
284
|
+
return controlOps.obliterateAsync(this.ctx);
|
|
285
|
+
}
|
|
274
286
|
isPaused() {
|
|
275
287
|
return controlOps.isPaused(this.ctx);
|
|
276
288
|
}
|
|
@@ -333,6 +345,9 @@ export class Queue {
|
|
|
333
345
|
setStallConfig(config) {
|
|
334
346
|
stallOps.setStallConfig(this.ctx, config);
|
|
335
347
|
}
|
|
348
|
+
setStallConfigAsync(config) {
|
|
349
|
+
return stallOps.setStallConfigAsync(this.ctx, config);
|
|
350
|
+
}
|
|
336
351
|
getStallConfig() {
|
|
337
352
|
return stallOps.getStallConfig(this.ctx);
|
|
338
353
|
}
|
|
@@ -343,6 +358,9 @@ export class Queue {
|
|
|
343
358
|
setDlqConfig(config) {
|
|
344
359
|
dlqOps.setDlqConfig(this.ctx, config);
|
|
345
360
|
}
|
|
361
|
+
setDlqConfigAsync(config) {
|
|
362
|
+
return dlqOps.setDlqConfigAsync(this.ctx, config);
|
|
363
|
+
}
|
|
346
364
|
getDlqConfig() {
|
|
347
365
|
return dlqOps.getDlqConfig(this.ctx);
|
|
348
366
|
}
|
|
@@ -352,18 +370,27 @@ export class Queue {
|
|
|
352
370
|
getDlq(filter) {
|
|
353
371
|
return dlqOps.getDlq(this.ctx, filter);
|
|
354
372
|
}
|
|
373
|
+
getDlqJobsAsync(count) {
|
|
374
|
+
return dlqOps.getDlqJobsAsync(this.queryCtx, count);
|
|
375
|
+
}
|
|
355
376
|
getDlqStats() {
|
|
356
377
|
return dlqOps.getDlqStats(this.ctx);
|
|
357
378
|
}
|
|
358
379
|
retryDlq(id) {
|
|
359
380
|
return dlqOps.retryDlq(this.ctx, id);
|
|
360
381
|
}
|
|
382
|
+
retryDlqAsync(id) {
|
|
383
|
+
return dlqOps.retryDlqAsync(this.ctx, id);
|
|
384
|
+
}
|
|
361
385
|
retryDlqByFilter(filter) {
|
|
362
386
|
return dlqOps.retryDlqByFilter(this.ctx, filter);
|
|
363
387
|
}
|
|
364
388
|
purgeDlq() {
|
|
365
389
|
return dlqOps.purgeDlq(this.ctx);
|
|
366
390
|
}
|
|
391
|
+
purgeDlqAsync() {
|
|
392
|
+
return dlqOps.purgeDlqAsync(this.ctx);
|
|
393
|
+
}
|
|
367
394
|
retryCompleted(id) {
|
|
368
395
|
return dlqOps.retryCompleted(this.ctx, id);
|
|
369
396
|
}
|
|
@@ -374,18 +401,30 @@ export class Queue {
|
|
|
374
401
|
setGlobalConcurrency(concurrency) {
|
|
375
402
|
rateLimitOps.setGlobalConcurrency(this.ctx, concurrency);
|
|
376
403
|
}
|
|
404
|
+
setGlobalConcurrencyAsync(concurrency) {
|
|
405
|
+
return rateLimitOps.setGlobalConcurrencyAsync(this.ctx, concurrency);
|
|
406
|
+
}
|
|
377
407
|
removeGlobalConcurrency() {
|
|
378
408
|
rateLimitOps.removeGlobalConcurrency(this.ctx);
|
|
379
409
|
}
|
|
410
|
+
removeGlobalConcurrencyAsync() {
|
|
411
|
+
return rateLimitOps.removeGlobalConcurrencyAsync(this.ctx);
|
|
412
|
+
}
|
|
380
413
|
getGlobalConcurrency() {
|
|
381
414
|
return rateLimitOps.getGlobalConcurrency(this.ctx);
|
|
382
415
|
}
|
|
383
416
|
setGlobalRateLimit(max, duration) {
|
|
384
417
|
rateLimitOps.setGlobalRateLimit(this.ctx, max, duration);
|
|
385
418
|
}
|
|
419
|
+
setGlobalRateLimitAsync(max, duration) {
|
|
420
|
+
return rateLimitOps.setGlobalRateLimitAsync(this.ctx, max, duration);
|
|
421
|
+
}
|
|
386
422
|
removeGlobalRateLimit() {
|
|
387
423
|
rateLimitOps.removeGlobalRateLimit(this.ctx);
|
|
388
424
|
}
|
|
425
|
+
removeGlobalRateLimitAsync() {
|
|
426
|
+
return rateLimitOps.removeGlobalRateLimitAsync(this.ctx);
|
|
427
|
+
}
|
|
389
428
|
getGlobalRateLimit() {
|
|
390
429
|
return rateLimitOps.getGlobalRateLimit(this.ctx);
|
|
391
430
|
}
|
|
@@ -9,20 +9,35 @@ interface RateLimitContext {
|
|
|
9
9
|
}
|
|
10
10
|
/** Set global concurrency limit */
|
|
11
11
|
export declare function setGlobalConcurrency(ctx: RateLimitContext, concurrency: number): void;
|
|
12
|
+
/** Set global concurrency limit and resolve once the server has applied it. */
|
|
13
|
+
export declare function setGlobalConcurrencyAsync(ctx: RateLimitContext, concurrency: number): Promise<void>;
|
|
12
14
|
/** Remove global concurrency limit */
|
|
13
15
|
export declare function removeGlobalConcurrency(ctx: RateLimitContext): void;
|
|
16
|
+
/** Remove global concurrency limit and resolve once the server has applied it. */
|
|
17
|
+
export declare function removeGlobalConcurrencyAsync(ctx: RateLimitContext): Promise<void>;
|
|
14
18
|
/** Get global concurrency limit */
|
|
15
19
|
export declare function getGlobalConcurrency(_ctx: RateLimitContext): Promise<number | null>;
|
|
16
|
-
/**
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Set global rate limit: `max` jobs per `duration` ms (default 1000ms).
|
|
22
|
+
* The window is honored in both embedded and TCP modes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function setGlobalRateLimit(ctx: RateLimitContext, max: number, duration?: number): void;
|
|
25
|
+
/** Set global rate limit and resolve once the server has applied it. */
|
|
26
|
+
export declare function setGlobalRateLimitAsync(ctx: RateLimitContext, max: number, duration?: number): Promise<void>;
|
|
18
27
|
/** Remove global rate limit */
|
|
19
28
|
export declare function removeGlobalRateLimit(ctx: RateLimitContext): void;
|
|
29
|
+
/** Remove global rate limit and resolve once the server has applied it. */
|
|
30
|
+
export declare function removeGlobalRateLimitAsync(ctx: RateLimitContext): Promise<void>;
|
|
20
31
|
/** Get global rate limit */
|
|
21
32
|
export declare function getGlobalRateLimit(_ctx: RateLimitContext): Promise<{
|
|
22
33
|
max: number;
|
|
23
34
|
duration: number;
|
|
24
35
|
} | null>;
|
|
25
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Set temporary rate limit with expiration. The expiry lives broker-side
|
|
38
|
+
* (RateLimit `ttl`), so it survives client exit and works identically in
|
|
39
|
+
* embedded and TCP modes — no client timer involved.
|
|
40
|
+
*/
|
|
26
41
|
export declare function rateLimit(ctx: RateLimitContext, expireTimeMs: number): Promise<void>;
|
|
27
42
|
/** Get rate limit TTL */
|
|
28
43
|
export declare function getRateLimitTtl(_ctx: RateLimitContext, _maxJobs?: number): Promise<number>;
|
|
@@ -11,6 +11,13 @@ export function setGlobalConcurrency(ctx, concurrency) {
|
|
|
11
11
|
void ctx.tcp.send({ cmd: 'SetConcurrency', queue: ctx.name, limit: concurrency });
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/** Set global concurrency limit and resolve once the server has applied it. */
|
|
15
|
+
export async function setGlobalConcurrencyAsync(ctx, concurrency) {
|
|
16
|
+
if (ctx.embedded)
|
|
17
|
+
getSharedManager().setConcurrency(ctx.name, concurrency);
|
|
18
|
+
else if (ctx.tcp)
|
|
19
|
+
await ctx.tcp.send({ cmd: 'SetConcurrency', queue: ctx.name, limit: concurrency });
|
|
20
|
+
}
|
|
14
21
|
/** Remove global concurrency limit */
|
|
15
22
|
export function removeGlobalConcurrency(ctx) {
|
|
16
23
|
if (ctx.embedded) {
|
|
@@ -20,19 +27,36 @@ export function removeGlobalConcurrency(ctx) {
|
|
|
20
27
|
void ctx.tcp.send({ cmd: 'ClearConcurrency', queue: ctx.name });
|
|
21
28
|
}
|
|
22
29
|
}
|
|
30
|
+
/** Remove global concurrency limit and resolve once the server has applied it. */
|
|
31
|
+
export async function removeGlobalConcurrencyAsync(ctx) {
|
|
32
|
+
if (ctx.embedded)
|
|
33
|
+
getSharedManager().clearConcurrency(ctx.name);
|
|
34
|
+
else if (ctx.tcp)
|
|
35
|
+
await ctx.tcp.send({ cmd: 'ClearConcurrency', queue: ctx.name });
|
|
36
|
+
}
|
|
23
37
|
/** Get global concurrency limit */
|
|
24
38
|
export function getGlobalConcurrency(_ctx) {
|
|
25
39
|
return Promise.resolve(null);
|
|
26
40
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Set global rate limit: `max` jobs per `duration` ms (default 1000ms).
|
|
43
|
+
* The window is honored in both embedded and TCP modes.
|
|
44
|
+
*/
|
|
45
|
+
export function setGlobalRateLimit(ctx, max, duration) {
|
|
29
46
|
if (ctx.embedded) {
|
|
30
|
-
getSharedManager().setRateLimit(ctx.name, max);
|
|
47
|
+
getSharedManager().setRateLimit(ctx.name, max, duration);
|
|
31
48
|
}
|
|
32
49
|
else if (ctx.tcp) {
|
|
33
|
-
void ctx.tcp.send({ cmd: 'RateLimit', queue: ctx.name, limit: max });
|
|
50
|
+
void ctx.tcp.send({ cmd: 'RateLimit', queue: ctx.name, limit: max, duration });
|
|
34
51
|
}
|
|
35
52
|
}
|
|
53
|
+
/** Set global rate limit and resolve once the server has applied it. */
|
|
54
|
+
export async function setGlobalRateLimitAsync(ctx, max, duration) {
|
|
55
|
+
if (ctx.embedded)
|
|
56
|
+
getSharedManager().setRateLimit(ctx.name, max, duration);
|
|
57
|
+
else if (ctx.tcp)
|
|
58
|
+
await ctx.tcp.send({ cmd: 'RateLimit', queue: ctx.name, limit: max, duration });
|
|
59
|
+
}
|
|
36
60
|
/** Remove global rate limit */
|
|
37
61
|
export function removeGlobalRateLimit(ctx) {
|
|
38
62
|
if (ctx.embedded) {
|
|
@@ -42,20 +66,31 @@ export function removeGlobalRateLimit(ctx) {
|
|
|
42
66
|
void ctx.tcp.send({ cmd: 'RateLimitClear', queue: ctx.name });
|
|
43
67
|
}
|
|
44
68
|
}
|
|
69
|
+
/** Remove global rate limit and resolve once the server has applied it. */
|
|
70
|
+
export async function removeGlobalRateLimitAsync(ctx) {
|
|
71
|
+
if (ctx.embedded)
|
|
72
|
+
getSharedManager().clearRateLimit(ctx.name);
|
|
73
|
+
else if (ctx.tcp)
|
|
74
|
+
await ctx.tcp.send({ cmd: 'RateLimitClear', queue: ctx.name });
|
|
75
|
+
}
|
|
45
76
|
/** Get global rate limit */
|
|
46
77
|
export function getGlobalRateLimit(_ctx) {
|
|
47
78
|
return Promise.resolve(null);
|
|
48
79
|
}
|
|
49
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Set temporary rate limit with expiration. The expiry lives broker-side
|
|
82
|
+
* (RateLimit `ttl`), so it survives client exit and works identically in
|
|
83
|
+
* embedded and TCP modes — no client timer involved.
|
|
84
|
+
*/
|
|
50
85
|
export async function rateLimit(ctx, expireTimeMs) {
|
|
86
|
+
if (!Number.isFinite(expireTimeMs) || expireTimeMs <= 0) {
|
|
87
|
+
throw new Error(`rateLimit: expireTimeMs must be a positive finite number, got ${expireTimeMs}`);
|
|
88
|
+
}
|
|
51
89
|
if (ctx.embedded) {
|
|
52
|
-
getSharedManager().setRateLimit(ctx.name, 1);
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
getSharedManager().clearRateLimit(ctx.name);
|
|
55
|
-
}, expireTimeMs);
|
|
90
|
+
getSharedManager().setRateLimit(ctx.name, 1, undefined, expireTimeMs);
|
|
56
91
|
}
|
|
57
92
|
else if (ctx.tcp) {
|
|
58
|
-
await ctx.tcp.send({ cmd: 'RateLimit', queue: ctx.name, limit: 1 });
|
|
93
|
+
await ctx.tcp.send({ cmd: 'RateLimit', queue: ctx.name, limit: 1, ttl: expireTimeMs });
|
|
59
94
|
}
|
|
60
95
|
}
|
|
61
96
|
/** Get rate limit TTL */
|
|
@@ -11,6 +11,8 @@ interface StallContext {
|
|
|
11
11
|
}
|
|
12
12
|
/** Set stall detection configuration */
|
|
13
13
|
export declare function setStallConfig(ctx: StallContext, config: Partial<StallConfig>): void;
|
|
14
|
+
/** Set stall detection configuration and resolve once the server has applied it. */
|
|
15
|
+
export declare function setStallConfigAsync(ctx: StallContext, config: Partial<StallConfig>): Promise<void>;
|
|
14
16
|
/** Get stall detection configuration */
|
|
15
17
|
export declare function getStallConfig(ctx: StallContext): StallConfig;
|
|
16
18
|
/** Get stall detection configuration (async, works in TCP mode) */
|
|
@@ -23,6 +23,18 @@ export function setStallConfig(ctx, config) {
|
|
|
23
23
|
void ctx.tcp.send({ cmd: 'SetStallConfig', queue: ctx.name, config });
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/** Set stall detection configuration and resolve once the server has applied it. */
|
|
27
|
+
export async function setStallConfigAsync(ctx, config) {
|
|
28
|
+
if (ctx.embedded) {
|
|
29
|
+
dlqOps.setStallConfigEmbedded(ctx.name, config);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!ctx.tcp)
|
|
33
|
+
return;
|
|
34
|
+
const current = tcpConfigCache.get(ctx.name) ?? { ...DEFAULT_STALL_CONFIG };
|
|
35
|
+
tcpConfigCache.set(ctx.name, { ...current, ...config });
|
|
36
|
+
await ctx.tcp.send({ cmd: 'SetStallConfig', queue: ctx.name, config });
|
|
37
|
+
}
|
|
26
38
|
/** Get stall detection configuration */
|
|
27
39
|
export function getStallConfig(ctx) {
|
|
28
40
|
if (ctx.embedded) {
|
|
@@ -20,10 +20,22 @@ export declare class LimiterManager {
|
|
|
20
20
|
pause(name: string): void;
|
|
21
21
|
/** Resume queue */
|
|
22
22
|
resume(name: string): void;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Set rate limit for queue.
|
|
25
|
+
* @param durationMs window the limit applies to (default 1000ms): `limit`
|
|
26
|
+
* tokens refill over `durationMs`. Non-finite or non-positive → default.
|
|
27
|
+
* @param ttlMs auto-expiry: the limit clears itself after this many ms
|
|
28
|
+
* (lazy, checked on acquire/read). Non-finite or non-positive → permanent.
|
|
29
|
+
*/
|
|
30
|
+
setRateLimit(queue: string, limit: number, durationMs?: number, ttlMs?: number): void;
|
|
25
31
|
/** Clear rate limit */
|
|
26
32
|
clearRateLimit(queue: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Drop the rate limit if its TTL has elapsed. Lazy expiry: called from the
|
|
35
|
+
* acquire path and from limit reads, so no timer is needed and an expired
|
|
36
|
+
* limit can never throttle a pull.
|
|
37
|
+
*/
|
|
38
|
+
expireRateLimitIfNeeded(queue: string): void;
|
|
27
39
|
/** Try to acquire rate limit token */
|
|
28
40
|
tryAcquireRateLimit(queue: string): boolean;
|
|
29
41
|
/** Set concurrency limit for queue */
|
|
@@ -34,20 +34,47 @@ export class LimiterManager {
|
|
|
34
34
|
this.getState(name).paused = false;
|
|
35
35
|
}
|
|
36
36
|
// ============ Rate Limiting ============
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Set rate limit for queue.
|
|
39
|
+
* @param durationMs window the limit applies to (default 1000ms): `limit`
|
|
40
|
+
* tokens refill over `durationMs`. Non-finite or non-positive → default.
|
|
41
|
+
* @param ttlMs auto-expiry: the limit clears itself after this many ms
|
|
42
|
+
* (lazy, checked on acquire/read). Non-finite or non-positive → permanent.
|
|
43
|
+
*/
|
|
44
|
+
setRateLimit(queue, limit, durationMs, ttlMs) {
|
|
45
|
+
const duration = Number.isFinite(durationMs) && durationMs > 0 ? durationMs : null;
|
|
46
|
+
const ttl = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : null;
|
|
47
|
+
const windowSeconds = (duration ?? 1000) / 1000;
|
|
48
|
+
this.rateLimiters.set(queue, new RateLimiter(limit, limit / windowSeconds));
|
|
49
|
+
const state = this.getState(queue);
|
|
50
|
+
state.rateLimit = limit;
|
|
51
|
+
state.rateLimitDuration = duration;
|
|
52
|
+
state.rateLimitExpiresAt = ttl === null ? null : Date.now() + ttl;
|
|
41
53
|
}
|
|
42
54
|
/** Clear rate limit */
|
|
43
55
|
clearRateLimit(queue) {
|
|
44
56
|
this.rateLimiters.delete(queue);
|
|
45
57
|
const state = this.queueState.get(queue);
|
|
46
|
-
if (state)
|
|
58
|
+
if (state) {
|
|
47
59
|
state.rateLimit = null;
|
|
60
|
+
state.rateLimitDuration = null;
|
|
61
|
+
state.rateLimitExpiresAt = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Drop the rate limit if its TTL has elapsed. Lazy expiry: called from the
|
|
66
|
+
* acquire path and from limit reads, so no timer is needed and an expired
|
|
67
|
+
* limit can never throttle a pull.
|
|
68
|
+
*/
|
|
69
|
+
expireRateLimitIfNeeded(queue) {
|
|
70
|
+
const expiresAt = this.queueState.get(queue)?.rateLimitExpiresAt;
|
|
71
|
+
if (expiresAt !== null && expiresAt !== undefined && Date.now() >= expiresAt) {
|
|
72
|
+
this.clearRateLimit(queue);
|
|
73
|
+
}
|
|
48
74
|
}
|
|
49
75
|
/** Try to acquire rate limit token */
|
|
50
76
|
tryAcquireRateLimit(queue) {
|
|
77
|
+
this.expireRateLimitIfNeeded(queue);
|
|
51
78
|
const limiter = this.rateLimiters.get(queue);
|
|
52
79
|
return !limiter || limiter.tryAcquire();
|
|
53
80
|
}
|
|
@@ -69,8 +69,9 @@ export declare class Shard {
|
|
|
69
69
|
isGroupActive(queue: string, groupId: string): boolean;
|
|
70
70
|
activateGroup(queue: string, groupId: string): void;
|
|
71
71
|
releaseGroup(queue: string, groupId: string): void;
|
|
72
|
-
setRateLimit(queue: string, limit: number): void;
|
|
72
|
+
setRateLimit(queue: string, limit: number, durationMs?: number, ttlMs?: number): void;
|
|
73
73
|
clearRateLimit(queue: string): void;
|
|
74
|
+
expireRateLimitIfNeeded(queue: string): void;
|
|
74
75
|
tryAcquireRateLimit(queue: string): boolean;
|
|
75
76
|
setConcurrency(queue: string, limit: number): void;
|
|
76
77
|
clearConcurrency(queue: string): void;
|
|
@@ -138,12 +138,15 @@ export class Shard {
|
|
|
138
138
|
this.activeGroups.get(queue)?.delete(groupId);
|
|
139
139
|
}
|
|
140
140
|
// ============ Rate & Concurrency Limiting (delegated) ============
|
|
141
|
-
setRateLimit(queue, limit) {
|
|
142
|
-
this.limiterManager.setRateLimit(queue, limit);
|
|
141
|
+
setRateLimit(queue, limit, durationMs, ttlMs) {
|
|
142
|
+
this.limiterManager.setRateLimit(queue, limit, durationMs, ttlMs);
|
|
143
143
|
}
|
|
144
144
|
clearRateLimit(queue) {
|
|
145
145
|
this.limiterManager.clearRateLimit(queue);
|
|
146
146
|
}
|
|
147
|
+
expireRateLimitIfNeeded(queue) {
|
|
148
|
+
this.limiterManager.expireRateLimitIfNeeded(queue);
|
|
149
|
+
}
|
|
147
150
|
tryAcquireRateLimit(queue) {
|
|
148
151
|
return this.limiterManager.tryAcquireRateLimit(queue);
|
|
149
152
|
}
|
|
@@ -253,6 +253,10 @@ export interface RateLimitCommand extends BaseCommand {
|
|
|
253
253
|
readonly cmd: 'RateLimit';
|
|
254
254
|
readonly queue: string;
|
|
255
255
|
readonly limit: number;
|
|
256
|
+
/** Window in ms the limit applies to (default 1000: `limit` per second). */
|
|
257
|
+
readonly duration?: number;
|
|
258
|
+
/** Auto-expiry in ms: the limit clears itself broker-side after this long. */
|
|
259
|
+
readonly ttl?: number;
|
|
256
260
|
}
|
|
257
261
|
export interface SetConcurrencyCommand extends BaseCommand {
|
|
258
262
|
readonly cmd: 'SetConcurrency';
|
|
@@ -7,6 +7,10 @@ export interface QueueState {
|
|
|
7
7
|
readonly name: string;
|
|
8
8
|
paused: boolean;
|
|
9
9
|
rateLimit: number | null;
|
|
10
|
+
/** Rate-limit window in ms (null = default 1000ms bucket). */
|
|
11
|
+
rateLimitDuration: number | null;
|
|
12
|
+
/** Epoch ms after which the rate limit self-expires (null = permanent). */
|
|
13
|
+
rateLimitExpiresAt: number | null;
|
|
10
14
|
concurrencyLimit: number | null;
|
|
11
15
|
activeCount: number;
|
|
12
16
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
/** SQLite PRAGMA settings for optimal performance */
|
|
5
5
|
export declare const PRAGMA_SETTINGS = "\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA cache_size = -64000;\nPRAGMA temp_store = MEMORY;\nPRAGMA mmap_size = 268435456;\nPRAGMA page_size = 4096;\nPRAGMA busy_timeout = 5000;\n";
|
|
6
6
|
/** Main schema creation */
|
|
7
|
-
export declare const SCHEMA = "\n-- Jobs table (using UUIDv7 for job IDs)\n-- Uses BLOB for data fields (MessagePack serialization for ~2-3x faster than JSON)\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n priority INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n run_at INTEGER NOT NULL,\n started_at INTEGER,\n completed_at INTEGER,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n backoff INTEGER NOT NULL DEFAULT 1000,\n ttl INTEGER,\n timeout INTEGER,\n unique_key TEXT,\n custom_id TEXT,\n depends_on BLOB,\n parent_id TEXT,\n children_ids BLOB,\n tags BLOB,\n state TEXT NOT NULL DEFAULT 'waiting',\n lifo INTEGER NOT NULL DEFAULT 0,\n group_id TEXT,\n progress INTEGER DEFAULT 0,\n progress_msg TEXT,\n remove_on_complete INTEGER DEFAULT 0,\n remove_on_fail INTEGER DEFAULT 0,\n stall_timeout INTEGER,\n last_heartbeat INTEGER,\n timeline BLOB,\n stacktrace BLOB\n);\n\n-- Indexes for common queries\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state\n ON jobs(queue, state);\n-- Stable createdAt/id pagination for unfiltered and logical-state queue views\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_created\n ON jobs(queue, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created\n ON jobs(queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_run_at\n ON jobs(run_at) WHERE state IN ('waiting', 'delayed');\nCREATE INDEX IF NOT EXISTS idx_jobs_unique\n ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_custom_id\n ON jobs(custom_id) WHERE custom_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_parent\n ON jobs(parent_id) WHERE parent_id IS NOT NULL;\n\n-- Job results storage (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS job_results (\n job_id TEXT PRIMARY KEY,\n result BLOB,\n completed_at INTEGER NOT NULL\n);\n\n-- Dead letter queue (BLOB for MessagePack - stores full DlqEntry)\nCREATE TABLE IF NOT EXISTS dlq (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n entry BLOB NOT NULL,\n entered_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_dlq_queue ON dlq(queue);\nCREATE INDEX IF NOT EXISTS idx_dlq_job_id ON dlq(job_id);\nCREATE INDEX IF NOT EXISTS idx_dlq_entered_at ON dlq(entered_at);\n\n-- Performance indexes for high-throughput operations\n-- Stall detection: runs every 5s, needs fast lookup of active jobs by started_at\nCREATE INDEX IF NOT EXISTS idx_jobs_state_started\n ON jobs(state, started_at) WHERE state = 'active';\n\n-- Group operations: fast lookup by group_id\nCREATE INDEX IF NOT EXISTS idx_jobs_group_id\n ON jobs(group_id) WHERE group_id IS NOT NULL;\n\n-- Pending jobs: compound index for priority-ordered retrieval\nCREATE INDEX IF NOT EXISTS idx_jobs_pending_priority\n ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');\n\n-- Completed jobs: index for recovery ordering (issue #84)\nCREATE INDEX IF NOT EXISTS idx_jobs_completed_order\n ON jobs(completed_at DESC) WHERE state = 'completed';\n\n-- Cron jobs (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS cron_jobs (\n name TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n schedule TEXT,\n repeat_every INTEGER,\n priority INTEGER NOT NULL DEFAULT 0,\n next_run INTEGER NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n timezone TEXT,\n unique_key TEXT,\n dedup BLOB,\n skip_missed_on_restart INTEGER NOT NULL DEFAULT 0,\n skip_if_no_worker INTEGER NOT NULL DEFAULT 0,\n prevent_overlap INTEGER NOT NULL DEFAULT 1,\n job_options BLOB\n);\n\n-- Queue state persistence (optional)\nCREATE TABLE IF NOT EXISTS queue_state (\n name TEXT PRIMARY KEY,\n paused INTEGER NOT NULL DEFAULT 0,\n rate_limit INTEGER,\n concurrency_limit INTEGER\n);\n";
|
|
7
|
+
export declare const SCHEMA = "\n-- Jobs table (using UUIDv7 for job IDs)\n-- Uses BLOB for data fields (MessagePack serialization for ~2-3x faster than JSON)\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n priority INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n run_at INTEGER NOT NULL,\n started_at INTEGER,\n completed_at INTEGER,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n backoff INTEGER NOT NULL DEFAULT 1000,\n ttl INTEGER,\n timeout INTEGER,\n unique_key TEXT,\n custom_id TEXT,\n depends_on BLOB,\n parent_id TEXT,\n children_ids BLOB,\n tags BLOB,\n state TEXT NOT NULL DEFAULT 'waiting',\n lifo INTEGER NOT NULL DEFAULT 0,\n group_id TEXT,\n progress INTEGER DEFAULT 0,\n progress_msg TEXT,\n remove_on_complete INTEGER DEFAULT 0,\n remove_on_fail INTEGER DEFAULT 0,\n stall_timeout INTEGER,\n last_heartbeat INTEGER,\n timeline BLOB,\n stacktrace BLOB\n);\n\n-- Indexes for common queries\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state\n ON jobs(queue, state);\n-- Stable createdAt/id pagination for unfiltered and logical-state queue views\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_created\n ON jobs(queue, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created\n ON jobs(queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_run_at\n ON jobs(run_at) WHERE state IN ('waiting', 'delayed');\nCREATE INDEX IF NOT EXISTS idx_jobs_unique\n ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_custom_id\n ON jobs(custom_id) WHERE custom_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_parent\n ON jobs(parent_id) WHERE parent_id IS NOT NULL;\n\n-- Job results storage (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS job_results (\n job_id TEXT PRIMARY KEY,\n result BLOB,\n completed_at INTEGER NOT NULL\n);\n\n-- Dead letter queue (BLOB for MessagePack - stores full DlqEntry)\nCREATE TABLE IF NOT EXISTS dlq (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n entry BLOB NOT NULL,\n entered_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_dlq_queue ON dlq(queue);\nCREATE INDEX IF NOT EXISTS idx_dlq_job_id ON dlq(job_id);\nCREATE INDEX IF NOT EXISTS idx_dlq_entered_at ON dlq(entered_at);\n\n-- Performance indexes for high-throughput operations\n-- Stall detection: runs every 5s, needs fast lookup of active jobs by started_at\nCREATE INDEX IF NOT EXISTS idx_jobs_state_started\n ON jobs(state, started_at) WHERE state = 'active';\n\n-- Group operations: fast lookup by group_id\nCREATE INDEX IF NOT EXISTS idx_jobs_group_id\n ON jobs(group_id) WHERE group_id IS NOT NULL;\n\n-- Pending jobs: compound index for priority-ordered retrieval\nCREATE INDEX IF NOT EXISTS idx_jobs_pending_priority\n ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');\n\n-- Completed jobs: index for recovery ordering (issue #84)\nCREATE INDEX IF NOT EXISTS idx_jobs_completed_order\n ON jobs(completed_at DESC) WHERE state = 'completed';\n\n-- Cron jobs (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS cron_jobs (\n name TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n schedule TEXT,\n repeat_every INTEGER,\n priority INTEGER NOT NULL DEFAULT 0,\n next_run INTEGER NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n timezone TEXT,\n unique_key TEXT,\n dedup BLOB,\n skip_missed_on_restart INTEGER NOT NULL DEFAULT 0,\n skip_if_no_worker INTEGER NOT NULL DEFAULT 0,\n prevent_overlap INTEGER NOT NULL DEFAULT 1,\n job_options BLOB\n);\n\n-- Queue state persistence (optional)\nCREATE TABLE IF NOT EXISTS queue_state (\n name TEXT PRIMARY KEY,\n paused INTEGER NOT NULL DEFAULT 0,\n rate_limit INTEGER,\n concurrency_limit INTEGER,\n rate_limit_duration INTEGER,\n rate_limit_expires_at INTEGER\n);\n";
|
|
8
8
|
/** Migration version table */
|
|
9
9
|
export declare const MIGRATION_TABLE = "\nCREATE TABLE IF NOT EXISTS migrations (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n";
|
|
10
10
|
/** Current schema version */
|
|
11
|
-
export declare const SCHEMA_VERSION =
|
|
11
|
+
export declare const SCHEMA_VERSION = 16;
|
|
12
12
|
/** All migrations in order */
|
|
13
13
|
export declare const MIGRATIONS: Record<number, string>;
|
|
@@ -127,7 +127,9 @@ CREATE TABLE IF NOT EXISTS queue_state (
|
|
|
127
127
|
name TEXT PRIMARY KEY,
|
|
128
128
|
paused INTEGER NOT NULL DEFAULT 0,
|
|
129
129
|
rate_limit INTEGER,
|
|
130
|
-
concurrency_limit INTEGER
|
|
130
|
+
concurrency_limit INTEGER,
|
|
131
|
+
rate_limit_duration INTEGER,
|
|
132
|
+
rate_limit_expires_at INTEGER
|
|
131
133
|
);
|
|
132
134
|
`;
|
|
133
135
|
/** Migration version table */
|
|
@@ -138,7 +140,7 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|
|
138
140
|
);
|
|
139
141
|
`;
|
|
140
142
|
/** Current schema version */
|
|
141
|
-
export const SCHEMA_VERSION =
|
|
143
|
+
export const SCHEMA_VERSION = 16;
|
|
142
144
|
/** All migrations in order */
|
|
143
145
|
export const MIGRATIONS = {
|
|
144
146
|
1: SCHEMA,
|
|
@@ -199,5 +201,16 @@ CREATE INDEX IF NOT EXISTS idx_jobs_queue_created
|
|
|
199
201
|
ON jobs(queue, created_at, id);
|
|
200
202
|
CREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created
|
|
201
203
|
ON jobs(queue, state, created_at, id);
|
|
204
|
+
`,
|
|
205
|
+
// Migrations 15-16: Rate-limit window (duration) + TTL auto-expiry on
|
|
206
|
+
// queue_state. One ALTER per migration ON PURPOSE: each runs in its own
|
|
207
|
+
// db.run with its own error-swallow, so a crash between the two leaves a
|
|
208
|
+
// retryable state (a two-ALTER string would abort at the first "duplicate
|
|
209
|
+
// column" on re-run and never execute the second statement).
|
|
210
|
+
15: `
|
|
211
|
+
ALTER TABLE queue_state ADD COLUMN rate_limit_duration INTEGER;
|
|
212
|
+
`,
|
|
213
|
+
16: `
|
|
214
|
+
ALTER TABLE queue_state ADD COLUMN rate_limit_expires_at INTEGER;
|
|
202
215
|
`,
|
|
203
216
|
};
|
|
@@ -204,13 +204,21 @@ export declare class SqliteStorage {
|
|
|
204
204
|
* Persist a queue's control-state (paused / rate-limit / concurrency) so it
|
|
205
205
|
* survives a server restart. Write-through on every pause/resume/limit change.
|
|
206
206
|
*/
|
|
207
|
-
saveQueueState(name: string,
|
|
207
|
+
saveQueueState(name: string, state: {
|
|
208
|
+
paused: boolean;
|
|
209
|
+
rateLimit: number | null;
|
|
210
|
+
concurrencyLimit: number | null;
|
|
211
|
+
rateLimitDuration?: number | null;
|
|
212
|
+
rateLimitExpiresAt?: number | null;
|
|
213
|
+
}): void;
|
|
208
214
|
/** Load all persisted queue control-state rows (used by recover() on boot). */
|
|
209
215
|
loadQueueState(): Array<{
|
|
210
216
|
name: string;
|
|
211
217
|
paused: boolean;
|
|
212
218
|
rateLimit: number | null;
|
|
213
219
|
concurrencyLimit: number | null;
|
|
220
|
+
rateLimitDuration: number | null;
|
|
221
|
+
rateLimitExpiresAt: number | null;
|
|
214
222
|
}>;
|
|
215
223
|
/** Drop a queue's persisted control-state (e.g. on obliterate). */
|
|
216
224
|
deleteQueueState(name: string): void;
|
|
@@ -650,11 +650,11 @@ export class SqliteStorage {
|
|
|
650
650
|
* Persist a queue's control-state (paused / rate-limit / concurrency) so it
|
|
651
651
|
* survives a server restart. Write-through on every pause/resume/limit change.
|
|
652
652
|
*/
|
|
653
|
-
saveQueueState(name,
|
|
653
|
+
saveQueueState(name, state) {
|
|
654
654
|
this.safeWrite(() => {
|
|
655
655
|
this.statements
|
|
656
656
|
.get('upsertQueueState')
|
|
657
|
-
.run(name, paused ? 1 : 0, rateLimit, concurrencyLimit);
|
|
657
|
+
.run(name, state.paused ? 1 : 0, state.rateLimit, state.concurrencyLimit, state.rateLimitDuration ?? null, state.rateLimitExpiresAt ?? null);
|
|
658
658
|
});
|
|
659
659
|
}
|
|
660
660
|
/** Load all persisted queue control-state rows (used by recover() on boot). */
|
|
@@ -665,6 +665,8 @@ export class SqliteStorage {
|
|
|
665
665
|
paused: row.paused === 1,
|
|
666
666
|
rateLimit: row.rate_limit,
|
|
667
667
|
concurrencyLimit: row.concurrency_limit,
|
|
668
|
+
rateLimitDuration: row.rate_limit_duration ?? null,
|
|
669
|
+
rateLimitExpiresAt: row.rate_limit_expires_at ?? null,
|
|
668
670
|
}));
|
|
669
671
|
}
|
|
670
672
|
/** Drop a queue's persisted control-state (e.g. on obliterate). */
|
|
@@ -57,8 +57,8 @@ export const SQL_STATEMENTS = {
|
|
|
57
57
|
`,
|
|
58
58
|
updateCron: 'UPDATE cron_jobs SET executions = ?, next_run = ? WHERE name = ?',
|
|
59
59
|
// Queue control-state persistence (#100): paused / rate-limit / concurrency.
|
|
60
|
-
upsertQueueState: 'INSERT OR REPLACE INTO queue_state (name, paused, rate_limit, concurrency_limit) VALUES (?, ?, ?, ?)',
|
|
61
|
-
loadQueueState: 'SELECT name, paused, rate_limit, concurrency_limit FROM queue_state',
|
|
60
|
+
upsertQueueState: 'INSERT OR REPLACE INTO queue_state (name, paused, rate_limit, concurrency_limit, rate_limit_duration, rate_limit_expires_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
61
|
+
loadQueueState: 'SELECT name, paused, rate_limit, concurrency_limit, rate_limit_duration, rate_limit_expires_at FROM queue_state',
|
|
62
62
|
deleteQueueState: 'DELETE FROM queue_state WHERE name = ?',
|
|
63
63
|
};
|
|
64
64
|
/** Prepare all statements */
|
|
@@ -100,6 +100,11 @@ export declare class CronScheduler {
|
|
|
100
100
|
*/
|
|
101
101
|
private tick;
|
|
102
102
|
/** Fire a cron job with overlap and worker detection */
|
|
103
|
+
/**
|
|
104
|
+
* Reasons a due cron must not push right now. Evaluated BEFORE the
|
|
105
|
+
* executions increment in tick(), so skips never consume maxLimit budget.
|
|
106
|
+
*/
|
|
107
|
+
private getSkipReason;
|
|
103
108
|
private fireCronJob;
|
|
104
109
|
/**
|
|
105
110
|
* Get scheduler stats
|
|
@@ -283,6 +283,35 @@ export class CronScheduler {
|
|
|
283
283
|
else {
|
|
284
284
|
newNextRun = executionTime;
|
|
285
285
|
}
|
|
286
|
+
// Skip decision comes BEFORE the executions increment: a skipped fire
|
|
287
|
+
// pushes no job, so it must not consume the maxLimit budget (a cron
|
|
288
|
+
// with maxLimit=N must deliver N jobs, not N-minus-skips — worst case
|
|
289
|
+
// skipIfNoWorker used to burn the whole budget with zero deliveries).
|
|
290
|
+
// The schedule still advances so the heap doesn't hot-loop on it.
|
|
291
|
+
const skipReason = this.getSkipReason(cron, now);
|
|
292
|
+
if (skipReason) {
|
|
293
|
+
if (this.persistCron) {
|
|
294
|
+
try {
|
|
295
|
+
this.persistCron(cron.name, cron.executions, newNextRun);
|
|
296
|
+
}
|
|
297
|
+
catch (persistErr) {
|
|
298
|
+
// Benign here: nothing was pushed, in-memory advance is enough;
|
|
299
|
+
// the next successful persist catches the row up.
|
|
300
|
+
cronLog.error('Failed to persist cron nextRun on skip', {
|
|
301
|
+
name: cron.name,
|
|
302
|
+
error: String(persistErr),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
cron.nextRun = newNextRun;
|
|
307
|
+
this.dashboardEmit?.('cron:skipped', {
|
|
308
|
+
name: cron.name,
|
|
309
|
+
queue: cron.queue,
|
|
310
|
+
reason: skipReason,
|
|
311
|
+
});
|
|
312
|
+
toReinsert.push(entry);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
286
315
|
// PERSIST STATE FIRST (before pushing job to prevent duplicates)
|
|
287
316
|
if (this.persistCron) {
|
|
288
317
|
try {
|
|
@@ -339,27 +368,24 @@ export class CronScheduler {
|
|
|
339
368
|
this.scheduleNext();
|
|
340
369
|
}
|
|
341
370
|
/** Fire a cron job with overlap and worker detection */
|
|
342
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Reasons a due cron must not push right now. Evaluated BEFORE the
|
|
373
|
+
* executions increment in tick(), so skips never consume maxLimit budget.
|
|
374
|
+
*/
|
|
375
|
+
getSkipReason(cron, now) {
|
|
343
376
|
// skipIfNoWorker: skip if no workers registered for this queue
|
|
344
377
|
if (cron.skipIfNoWorker && this.hasWorkers && !this.hasWorkers(cron.queue)) {
|
|
345
|
-
|
|
346
|
-
name: cron.name,
|
|
347
|
-
queue: cron.queue,
|
|
348
|
-
reason: 'no-worker',
|
|
349
|
-
});
|
|
350
|
-
return;
|
|
378
|
+
return 'no-worker';
|
|
351
379
|
}
|
|
352
380
|
// Overlap detection: skip if last fire was within repeatEvery window
|
|
353
381
|
const lastFire = this.lastFiredAt.get(cron.name);
|
|
354
382
|
const interval = cron.repeatEvery ?? 60000;
|
|
355
383
|
if (lastFire && now - lastFire < interval * 0.8) {
|
|
356
|
-
|
|
357
|
-
name: cron.name,
|
|
358
|
-
queue: cron.queue,
|
|
359
|
-
reason: 'overlap',
|
|
360
|
-
});
|
|
361
|
-
return;
|
|
384
|
+
return 'overlap';
|
|
362
385
|
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
async fireCronJob(cron, now) {
|
|
363
389
|
// preventOverlap: auto-set uniqueKey to block pushes while previous job is active
|
|
364
390
|
const effectiveUniqueKey = cron.uniqueKey ?? (cron.preventOverlap ? `cron:${cron.name}` : undefined);
|
|
365
391
|
const opts = cron.jobOptions;
|
|
@@ -172,8 +172,17 @@ export function handleRateLimit(cmd, ctx, reqId) {
|
|
|
172
172
|
const limit = toFiniteNumber(cmd.limit);
|
|
173
173
|
if (limit === undefined)
|
|
174
174
|
return resp.error('limit must be a finite number', reqId);
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// Optional window/TTL: invalid values degrade to the defaults (1s window,
|
|
176
|
+
// permanent limit) inside the limiter rather than failing the command.
|
|
177
|
+
const duration = toFiniteNumber(cmd.duration);
|
|
178
|
+
const ttl = toFiniteNumber(cmd.ttl);
|
|
179
|
+
ctx.queueManager.setRateLimit(cmd.queue, limit, duration, ttl);
|
|
180
|
+
ctx.queueManager.emitDashboardEvent('ratelimit:set', {
|
|
181
|
+
queue: cmd.queue,
|
|
182
|
+
max: limit,
|
|
183
|
+
...(duration !== undefined && { duration }),
|
|
184
|
+
...(ttl !== undefined && { ttl }),
|
|
185
|
+
});
|
|
177
186
|
return resp.ok(undefined, reqId);
|
|
178
187
|
}
|
|
179
188
|
/** Handle RateLimitClear command */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunqueue",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.35",
|
|
4
4
|
"description": "High-performance job queue for Bun & AI agents. SQLite persistence, cron scheduling, priorities, retries, DLQ, webhooks, native MCP server. Zero external infrastructure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|