bunqueue 2.9.0 → 2.9.2
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 +7 -3
- package/dist/application/background/recovery/active.d.ts +1 -0
- package/dist/application/background/recovery/active.js +12 -0
- package/dist/application/background/recovery/index.js +3 -1
- package/dist/application/background/recovery/pending.js +1 -0
- package/dist/application/cleanupTasks.js +4 -16
- package/dist/application/contextFactory.js +3 -0
- package/dist/application/dlqManager.js +1 -0
- package/dist/application/dlqRetry.js +1 -0
- package/dist/application/eventsManager.d.ts +8 -1
- package/dist/application/eventsManager.js +76 -29
- package/dist/application/operations/ackHelpers.d.ts +1 -4
- package/dist/application/operations/ackHelpers.js +39 -14
- package/dist/application/operations/flowPush.js +1 -0
- package/dist/application/operations/flowValidation.js +4 -4
- package/dist/application/operations/pull.d.ts +7 -2
- package/dist/application/operations/pull.js +15 -57
- package/dist/application/operations/pullFinalization.d.ts +6 -0
- package/dist/application/operations/pullFinalization.js +69 -0
- package/dist/application/operations/pullStateTransition.d.ts +8 -7
- package/dist/application/operations/pullStateTransition.js +25 -18
- package/dist/application/operations/push.js +3 -0
- package/dist/application/operations/pushBatch.js +14 -7
- package/dist/application/operations/pushContext.d.ts +2 -1
- package/dist/application/postgres-queue-manager/control.d.ts +4 -0
- package/dist/application/postgres-queue-manager/control.js +24 -0
- package/dist/application/postgres-queue-manager/delivery.d.ts +5 -4
- package/dist/application/postgres-queue-manager/delivery.js +13 -10
- package/dist/application/postgres-queue-manager/groupQueries.d.ts +10 -0
- package/dist/application/postgres-queue-manager/groupQueries.js +40 -0
- package/dist/application/postgres-queue-manager/queries.d.ts +2 -2
- package/dist/application/postgres-queue-manager/queries.js +2 -2
- package/dist/application/queue-manager/control.js +1 -0
- package/dist/application/queue-manager/delivery.d.ts +5 -4
- package/dist/application/queue-manager/delivery.js +15 -8
- package/dist/application/queue-manager/limits.d.ts +11 -0
- package/dist/application/queue-manager/limits.js +61 -0
- package/dist/application/queue-manager/state.js +1 -1
- package/dist/application/queueTelemetryJournal.d.ts +2 -0
- package/dist/application/queueTelemetryJournal.js +23 -0
- package/dist/application/types/ack.d.ts +3 -1
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flowPush.js +6 -0
- package/dist/client/groupId.d.ts +1 -0
- package/dist/client/groupId.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/jobHelpers.js +1 -0
- package/dist/client/queue/operations/add/bulk.js +4 -4
- package/dist/client/queue/operations/add/payload.d.ts +1 -0
- package/dist/client/queue/operations/add/payload.js +7 -1
- package/dist/client/queue/operations/add/single.js +3 -3
- package/dist/client/queue/operations/groups.d.ts +21 -0
- package/dist/client/queue/operations/groups.js +88 -0
- package/dist/client/queue/runtime/queries.d.ts +13 -0
- package/dist/client/queue/runtime/queries.js +31 -0
- package/dist/client/queue/runtime/state.d.ts +4 -0
- package/dist/client/queue/runtime/state.js +1 -0
- package/dist/client/types/index.d.ts +2 -2
- package/dist/client/types/options.d.ts +4 -0
- package/dist/client/types/worker.d.ts +8 -0
- package/dist/client/worker/runtime/execution.js +1 -0
- package/dist/client/worker/runtime/options.js +3 -0
- package/dist/client/worker/types/options.d.ts +2 -1
- package/dist/client/worker/workerPull.d.ts +2 -0
- package/dist/client/worker/workerPull.js +6 -4
- package/dist/domain/job/groupFifoOrder.d.ts +9 -0
- package/dist/domain/job/groupFifoOrder.js +26 -0
- package/dist/domain/queue/groupLimiterManager.d.ts +23 -0
- package/dist/domain/queue/groupLimiterManager.js +107 -0
- package/dist/domain/queue/groupScheduler.d.ts +31 -0
- package/dist/domain/queue/groupScheduler.js +256 -0
- package/dist/domain/queue/groupSchedulerOrder.d.ts +6 -0
- package/dist/domain/queue/groupSchedulerOrder.js +30 -0
- package/dist/domain/queue/priorityQueue.d.ts +13 -23
- package/dist/domain/queue/priorityQueue.js +53 -42
- package/dist/domain/queue/shard/keys.d.ts +1 -0
- package/dist/domain/queue/shard/keys.js +25 -2
- package/dist/domain/queue/shard/lifecycle.js +4 -0
- package/dist/domain/queue/shard/limits.d.ts +20 -1
- package/dist/domain/queue/shard/limits.js +56 -0
- package/dist/domain/queue/shard/state.d.ts +5 -0
- package/dist/domain/queue/shard/state.js +9 -1
- package/dist/domain/types/commands/core.d.ts +3 -0
- package/dist/domain/types/commands/limits.d.ts +41 -0
- package/dist/domain/types/commands/union.d.ts +1 -1
- package/dist/domain/types/group.d.ts +21 -0
- package/dist/domain/types/group.js +78 -0
- package/dist/domain/types/priorityQueue.d.ts +1 -0
- package/dist/infrastructure/persistence/jobOptionsBlob.d.ts +1 -0
- package/dist/infrastructure/persistence/jobOptionsBlob.js +10 -2
- package/dist/infrastructure/persistence/migrations.js +11 -0
- package/dist/infrastructure/persistence/postgres/admission.js +5 -2
- package/dist/infrastructure/persistence/postgres/batchAdmission.js +22 -4
- package/dist/infrastructure/persistence/postgres/claimBatch.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/claimBatch.js +16 -3
- package/dist/infrastructure/persistence/postgres/claimSelection.d.ts +8 -1
- package/dist/infrastructure/persistence/postgres/claimSelection.js +73 -131
- package/dist/infrastructure/persistence/postgres/claims.d.ts +8 -1
- package/dist/infrastructure/persistence/postgres/claims.js +19 -3
- package/dist/infrastructure/persistence/postgres/eventCatchupCursors.d.ts +32 -0
- package/dist/infrastructure/persistence/postgres/eventCatchupCursors.js +66 -0
- package/dist/infrastructure/persistence/postgres/eventJournalSchema.d.ts +2 -2
- package/dist/infrastructure/persistence/postgres/eventJournalSchema.js +16 -2
- package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.js +21 -3
- package/dist/infrastructure/persistence/postgres/events.d.ts +3 -3
- package/dist/infrastructure/persistence/postgres/events.js +44 -17
- package/dist/infrastructure/persistence/postgres/groupClaims.d.ts +15 -0
- package/dist/infrastructure/persistence/postgres/groupClaims.js +95 -0
- package/dist/infrastructure/persistence/postgres/groupSchema.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/groupSchema.js +155 -0
- package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.js +127 -0
- package/dist/infrastructure/persistence/postgres/groupStateRetention.d.ts +3 -0
- package/dist/infrastructure/persistence/postgres/groupStateRetention.js +93 -0
- package/dist/infrastructure/persistence/postgres/groups.d.ts +11 -0
- package/dist/infrastructure/persistence/postgres/groups.js +117 -0
- package/dist/infrastructure/persistence/postgres/queueDestruction.js +4 -0
- package/dist/infrastructure/persistence/postgres/runtime.d.ts +0 -1
- package/dist/infrastructure/persistence/postgres/runtime.js +3 -1
- package/dist/infrastructure/persistence/postgres/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/postgres/schema.js +8 -4
- package/dist/infrastructure/persistence/postgres/schemaInitialization.js +6 -1
- package/dist/infrastructure/persistence/postgres/store.d.ts +11 -1
- package/dist/infrastructure/persistence/postgres/store.js +47 -3
- package/dist/infrastructure/persistence/postgres/telemetry.d.ts +2 -0
- package/dist/infrastructure/persistence/postgres/telemetry.js +33 -21
- package/dist/infrastructure/persistence/postgres/types.d.ts +1 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +12 -1
- package/dist/infrastructure/persistence/sqlite/control.d.ts +14 -0
- package/dist/infrastructure/persistence/sqlite/control.js +69 -0
- package/dist/infrastructure/persistence/sqlite/jobs.d.ts +12 -0
- package/dist/infrastructure/persistence/sqlite/jobs.js +36 -0
- package/dist/infrastructure/persistence/sqlite/telemetry.d.ts +2 -1
- package/dist/infrastructure/persistence/sqlite/telemetry.js +5 -64
- package/dist/infrastructure/persistence/sqlite/telemetryWrites.d.ts +4 -0
- package/dist/infrastructure/persistence/sqlite/telemetryWrites.js +123 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +2 -0
- package/dist/infrastructure/server/handler-routes/control.js +13 -0
- package/dist/infrastructure/server/handlers/core.d.ts +0 -5
- package/dist/infrastructure/server/handlers/core.js +15 -18
- package/dist/infrastructure/server/handlers/groups.d.ts +11 -0
- package/dist/infrastructure/server/handlers/groups.js +55 -0
- package/dist/infrastructure/server/handlers/pushBatchValidation.js +4 -1
- package/dist/infrastructure/server/protocol/validation.d.ts +1 -0
- package/dist/infrastructure/server/protocol/validation.js +1 -0
- package/dist/infrastructure/server/protocol.d.ts +1 -1
- package/dist/infrastructure/server/protocol.js +1 -1
- package/package.json +2 -1
|
@@ -10,18 +10,18 @@ export async function pullEmbedded(config, count) {
|
|
|
10
10
|
// configured lock TTL is honored in embedded mode too (undefined → server default).
|
|
11
11
|
if (config.useLocks) {
|
|
12
12
|
if (count === 1) {
|
|
13
|
-
const { job, token } = await manager.pullWithLock(config.name, config.workerId, 0, config.lockDuration);
|
|
13
|
+
const { job, token } = await manager.pullWithLock(config.name, config.workerId, 0, config.lockDuration, undefined, config.group);
|
|
14
14
|
return job ? [{ job, token }] : [];
|
|
15
15
|
}
|
|
16
|
-
const { jobs, tokens } = await manager.pullBatchWithLock(config.name, count, config.workerId, 0, config.lockDuration);
|
|
16
|
+
const { jobs, tokens } = await manager.pullBatchWithLock(config.name, count, config.workerId, 0, config.lockDuration, undefined, config.group);
|
|
17
17
|
return jobs.map((job, i) => ({ job, token: tokens[i] || null }));
|
|
18
18
|
}
|
|
19
19
|
// No locks - use regular pull
|
|
20
20
|
if (count === 1) {
|
|
21
|
-
const job = await manager.pull(config.name, 0);
|
|
21
|
+
const job = await manager.pull(config.name, 0, undefined, config.group);
|
|
22
22
|
return job ? [{ job, token: null }] : [];
|
|
23
23
|
}
|
|
24
|
-
const jobs = await manager.pullBatch(config.name, count, 0);
|
|
24
|
+
const jobs = await manager.pullBatch(config.name, count, 0, undefined, config.group);
|
|
25
25
|
return jobs.map((job) => ({ job, token: null }));
|
|
26
26
|
}
|
|
27
27
|
export async function pullTcp(config, tcp, count, closing) {
|
|
@@ -36,6 +36,8 @@ export async function pullTcp(config, tcp, count, closing) {
|
|
|
36
36
|
};
|
|
37
37
|
if (count > 1)
|
|
38
38
|
cmd.count = count;
|
|
39
|
+
if (config.group)
|
|
40
|
+
cmd.group = config.group;
|
|
39
41
|
// Only request lock ownership when useLocks is enabled
|
|
40
42
|
if (config.useLocks) {
|
|
41
43
|
cmd.owner = config.workerId;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Job } from '../types/jobs/model';
|
|
2
|
+
/** Read the broker-internal admission order used by grouped FIFO lanes. */
|
|
3
|
+
export declare function getGroupFifoOrder(job: Job): bigint | null;
|
|
4
|
+
/** Stamp a stable order without exposing it through the public job protocol. */
|
|
5
|
+
export declare function setGroupFifoOrder(job: Job, order: bigint): void;
|
|
6
|
+
/** Preserve the hidden order when an immutable job replacement is created. */
|
|
7
|
+
export declare function copyGroupFifoOrder(source: Job, target: Job): void;
|
|
8
|
+
/** Restore a validated decimal order from SQLite extended options. */
|
|
9
|
+
export declare function restoreGroupFifoOrder(job: Job, stored: string | null): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const GROUP_FIFO_ORDER = Symbol('bunqueue.groupFifoOrder');
|
|
2
|
+
/** Read the broker-internal admission order used by grouped FIFO lanes. */
|
|
3
|
+
export function getGroupFifoOrder(job) {
|
|
4
|
+
return job[GROUP_FIFO_ORDER] ?? null;
|
|
5
|
+
}
|
|
6
|
+
/** Stamp a stable order without exposing it through the public job protocol. */
|
|
7
|
+
export function setGroupFifoOrder(job, order) {
|
|
8
|
+
Object.defineProperty(job, GROUP_FIFO_ORDER, {
|
|
9
|
+
value: order,
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: false,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Preserve the hidden order when an immutable job replacement is created. */
|
|
16
|
+
export function copyGroupFifoOrder(source, target) {
|
|
17
|
+
const order = getGroupFifoOrder(source);
|
|
18
|
+
if (order !== null)
|
|
19
|
+
setGroupFifoOrder(target, order);
|
|
20
|
+
}
|
|
21
|
+
/** Restore a validated decimal order from SQLite extended options. */
|
|
22
|
+
export function restoreGroupFifoOrder(job, stored) {
|
|
23
|
+
if (stored === null || !/^[1-9]\d*$/.test(stored))
|
|
24
|
+
return;
|
|
25
|
+
setGroupFifoOrder(job, BigInt(stored));
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type GroupPullOptions, type GroupRateLimitOverride } from '../types/group';
|
|
2
|
+
export interface GroupLimitStatus {
|
|
3
|
+
eligible: boolean;
|
|
4
|
+
retryAt: number | null;
|
|
5
|
+
}
|
|
6
|
+
/** Stored group overrides and broker-authoritative runtime rate windows. */
|
|
7
|
+
export declare class GroupLimiterManager {
|
|
8
|
+
private readonly rateOverrides;
|
|
9
|
+
private readonly concurrencyOverrides;
|
|
10
|
+
private readonly windows;
|
|
11
|
+
status(queue: string, groupId: string, defaults: GroupPullOptions | undefined, active: number, now?: number): GroupLimitStatus;
|
|
12
|
+
acquire(queue: string, groupId: string, defaults: GroupPullOptions | undefined, active: number, now?: number): boolean;
|
|
13
|
+
setRateLimit(queue: string, groupId: string, max: number, duration: number): void;
|
|
14
|
+
getRateLimit(queue: string, groupId: string): GroupRateLimitOverride | null;
|
|
15
|
+
removeRateLimit(queue: string, groupId: string): number;
|
|
16
|
+
getRateLimitTtl(queue: string, groupId: string, maxJobs?: number, now?: number): number;
|
|
17
|
+
setConcurrency(queue: string, groupId: string, concurrency: number): void;
|
|
18
|
+
getConcurrency(queue: string, groupId: string): number | null;
|
|
19
|
+
removeConcurrency(queue: string, groupId: string): number;
|
|
20
|
+
clearQueue(queue: string): void;
|
|
21
|
+
pruneExpiredWindows(now?: number): void;
|
|
22
|
+
private currentWindow;
|
|
23
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { assertPositiveSafeInteger, } from '../types/group';
|
|
2
|
+
function key(queue, groupId) {
|
|
3
|
+
return `${queue}\0${groupId}`;
|
|
4
|
+
}
|
|
5
|
+
/** Stored group overrides and broker-authoritative runtime rate windows. */
|
|
6
|
+
export class GroupLimiterManager {
|
|
7
|
+
rateOverrides = new Map();
|
|
8
|
+
concurrencyOverrides = new Map();
|
|
9
|
+
windows = new Map();
|
|
10
|
+
status(queue, groupId, defaults, active, now = Date.now()) {
|
|
11
|
+
const groupKey = key(queue, groupId);
|
|
12
|
+
if (defaults?.concurrency !== undefined) {
|
|
13
|
+
const concurrency = this.concurrencyOverrides.get(groupKey) ?? defaults.concurrency;
|
|
14
|
+
if (active >= concurrency)
|
|
15
|
+
return { eligible: false, retryAt: null };
|
|
16
|
+
}
|
|
17
|
+
if (!defaults?.limit)
|
|
18
|
+
return { eligible: true, retryAt: null };
|
|
19
|
+
const limit = this.rateOverrides.get(groupKey) ?? defaults.limit;
|
|
20
|
+
const window = this.currentWindow(groupKey, limit.max, limit.duration, now);
|
|
21
|
+
if (window && window.count >= limit.max) {
|
|
22
|
+
return { eligible: false, retryAt: window.startedAt + window.duration };
|
|
23
|
+
}
|
|
24
|
+
return { eligible: true, retryAt: null };
|
|
25
|
+
}
|
|
26
|
+
acquire(queue, groupId, defaults, active, now = Date.now()) {
|
|
27
|
+
const status = this.status(queue, groupId, defaults, active, now);
|
|
28
|
+
if (!status.eligible)
|
|
29
|
+
return false;
|
|
30
|
+
if (!defaults?.limit)
|
|
31
|
+
return true;
|
|
32
|
+
const groupKey = key(queue, groupId);
|
|
33
|
+
const limit = this.rateOverrides.get(groupKey) ?? defaults.limit;
|
|
34
|
+
let window = this.currentWindow(groupKey, limit.max, limit.duration, now);
|
|
35
|
+
if (!window) {
|
|
36
|
+
window = { max: limit.max, duration: limit.duration, startedAt: now, count: 0 };
|
|
37
|
+
this.windows.set(groupKey, window);
|
|
38
|
+
}
|
|
39
|
+
window.count++;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
setRateLimit(queue, groupId, max, duration) {
|
|
43
|
+
assertPositiveSafeInteger(max, 'max');
|
|
44
|
+
assertPositiveSafeInteger(duration, 'duration');
|
|
45
|
+
const groupKey = key(queue, groupId);
|
|
46
|
+
this.rateOverrides.set(groupKey, { max, duration });
|
|
47
|
+
this.windows.delete(groupKey);
|
|
48
|
+
}
|
|
49
|
+
getRateLimit(queue, groupId) {
|
|
50
|
+
return this.rateOverrides.get(key(queue, groupId)) ?? null;
|
|
51
|
+
}
|
|
52
|
+
removeRateLimit(queue, groupId) {
|
|
53
|
+
const groupKey = key(queue, groupId);
|
|
54
|
+
this.windows.delete(groupKey);
|
|
55
|
+
return this.rateOverrides.delete(groupKey) ? 1 : 0;
|
|
56
|
+
}
|
|
57
|
+
getRateLimitTtl(queue, groupId, maxJobs, now = Date.now()) {
|
|
58
|
+
const groupKey = key(queue, groupId);
|
|
59
|
+
const window = this.windows.get(groupKey);
|
|
60
|
+
if (!window)
|
|
61
|
+
return -2;
|
|
62
|
+
if (now >= window.startedAt + window.duration) {
|
|
63
|
+
this.windows.delete(groupKey);
|
|
64
|
+
return -2;
|
|
65
|
+
}
|
|
66
|
+
if (maxJobs !== undefined && window.count < maxJobs)
|
|
67
|
+
return 0;
|
|
68
|
+
return window.startedAt + window.duration - now;
|
|
69
|
+
}
|
|
70
|
+
setConcurrency(queue, groupId, concurrency) {
|
|
71
|
+
assertPositiveSafeInteger(concurrency, 'concurrency');
|
|
72
|
+
this.concurrencyOverrides.set(key(queue, groupId), concurrency);
|
|
73
|
+
}
|
|
74
|
+
getConcurrency(queue, groupId) {
|
|
75
|
+
return this.concurrencyOverrides.get(key(queue, groupId)) ?? null;
|
|
76
|
+
}
|
|
77
|
+
removeConcurrency(queue, groupId) {
|
|
78
|
+
return this.concurrencyOverrides.delete(key(queue, groupId)) ? 1 : 0;
|
|
79
|
+
}
|
|
80
|
+
clearQueue(queue) {
|
|
81
|
+
const prefix = `${queue}\0`;
|
|
82
|
+
for (const map of [this.rateOverrides, this.concurrencyOverrides, this.windows]) {
|
|
83
|
+
for (const groupKey of map.keys()) {
|
|
84
|
+
if (groupKey.startsWith(prefix))
|
|
85
|
+
map.delete(groupKey);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pruneExpiredWindows(now = Date.now()) {
|
|
90
|
+
for (const [groupKey, window] of this.windows) {
|
|
91
|
+
if (now >= window.startedAt + window.duration)
|
|
92
|
+
this.windows.delete(groupKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
currentWindow(groupKey, max, duration, now) {
|
|
96
|
+
const window = this.windows.get(groupKey);
|
|
97
|
+
if (!window)
|
|
98
|
+
return null;
|
|
99
|
+
if (now >= window.startedAt + window.duration ||
|
|
100
|
+
window.max !== max ||
|
|
101
|
+
window.duration !== duration) {
|
|
102
|
+
this.windows.delete(groupKey);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return window;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Job } from '../types/job';
|
|
2
|
+
export interface GroupEligibility {
|
|
3
|
+
eligible: boolean;
|
|
4
|
+
retryAt: number | null;
|
|
5
|
+
}
|
|
6
|
+
export interface GroupCandidate {
|
|
7
|
+
job: Job | null;
|
|
8
|
+
nextRunAt: number | null;
|
|
9
|
+
}
|
|
10
|
+
/** Lazy secondary ready/group indexes; the primary queue remains authoritative. */
|
|
11
|
+
export declare class GroupScheduler {
|
|
12
|
+
private readonly queues;
|
|
13
|
+
private nextFifoOrder;
|
|
14
|
+
assignFifoOrder(job: Job): void;
|
|
15
|
+
observeFifoOrder(job: Job): void;
|
|
16
|
+
insert(queue: string, job: Job, primaryJobs: () => readonly Job[], now?: number): boolean;
|
|
17
|
+
remove(queue: string, job: Job): boolean;
|
|
18
|
+
isActive(queue: string): boolean;
|
|
19
|
+
peek(queue: string, now: number, eligibility: (groupId: string) => GroupEligibility): GroupCandidate;
|
|
20
|
+
advance(queue: string, groupId: string): void;
|
|
21
|
+
getGroupJobsCount(queue: string, groupId: string): number;
|
|
22
|
+
getGroupsJobsCount(queue: string): number;
|
|
23
|
+
clearQueue(queue: string): void;
|
|
24
|
+
private createState;
|
|
25
|
+
private insertTracked;
|
|
26
|
+
private promoteDue;
|
|
27
|
+
private insertReady;
|
|
28
|
+
private demoteReady;
|
|
29
|
+
private attachLane;
|
|
30
|
+
private detachLane;
|
|
31
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { getGroupFifoOrder, setGroupFifoOrder } from '../job/groupFifoOrder';
|
|
2
|
+
import { isExpired, isReady } from '../types/job';
|
|
3
|
+
import { IndexedPriorityQueue } from './priorityQueue';
|
|
4
|
+
import { compareGroupFifoEntries, compareGroupWakeEntries, nextGroupCheckAt, orderedGroupClone, } from './groupSchedulerOrder';
|
|
5
|
+
/** Lazy secondary ready/group indexes; the primary queue remains authoritative. */
|
|
6
|
+
export class GroupScheduler {
|
|
7
|
+
queues = new Map();
|
|
8
|
+
nextFifoOrder = 1n;
|
|
9
|
+
assignFifoOrder(job) {
|
|
10
|
+
if (!job.groupId)
|
|
11
|
+
return;
|
|
12
|
+
const existing = getGroupFifoOrder(job);
|
|
13
|
+
if (existing !== null) {
|
|
14
|
+
this.observeFifoOrder(job);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
setGroupFifoOrder(job, this.nextFifoOrder++);
|
|
18
|
+
}
|
|
19
|
+
observeFifoOrder(job) {
|
|
20
|
+
const order = getGroupFifoOrder(job);
|
|
21
|
+
if (order !== null && order >= this.nextFifoOrder)
|
|
22
|
+
this.nextFifoOrder = order + 1n;
|
|
23
|
+
}
|
|
24
|
+
insert(queue, job, primaryJobs, now = Date.now()) {
|
|
25
|
+
this.assignFifoOrder(job);
|
|
26
|
+
let state = this.queues.get(queue);
|
|
27
|
+
const previous = state?.jobs.get(job.id);
|
|
28
|
+
if (previous) {
|
|
29
|
+
this.remove(queue, previous);
|
|
30
|
+
state = this.queues.get(queue);
|
|
31
|
+
}
|
|
32
|
+
if (!state) {
|
|
33
|
+
if (!job.groupId)
|
|
34
|
+
return false;
|
|
35
|
+
state = this.createState();
|
|
36
|
+
this.queues.set(queue, state);
|
|
37
|
+
for (const current of primaryJobs()) {
|
|
38
|
+
this.assignFifoOrder(current);
|
|
39
|
+
this.insertTracked(state, current, now);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
this.insertTracked(state, job, now);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
remove(queue, job) {
|
|
47
|
+
const state = this.queues.get(queue);
|
|
48
|
+
if (!state)
|
|
49
|
+
return false;
|
|
50
|
+
const tracked = state.jobs.get(job.id) ?? job;
|
|
51
|
+
const placement = state.placements.get(job.id);
|
|
52
|
+
if (!placement)
|
|
53
|
+
return true;
|
|
54
|
+
if (placement.kind === 'plain') {
|
|
55
|
+
state.plain.remove(job.id);
|
|
56
|
+
}
|
|
57
|
+
else if (placement.kind === 'delayed') {
|
|
58
|
+
state.delayed.remove(job.id);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const lane = state.lanes.get(placement.groupId);
|
|
62
|
+
lane?.jobs.remove(job.id);
|
|
63
|
+
if (lane?.jobs.isEmpty)
|
|
64
|
+
this.detachLane(state, placement.groupId, lane);
|
|
65
|
+
}
|
|
66
|
+
state.placements.delete(job.id);
|
|
67
|
+
state.jobs.delete(job.id);
|
|
68
|
+
if (tracked.groupId) {
|
|
69
|
+
const count = (state.groupCounts.get(tracked.groupId) ?? 1) - 1;
|
|
70
|
+
if (count > 0)
|
|
71
|
+
state.groupCounts.set(tracked.groupId, count);
|
|
72
|
+
else {
|
|
73
|
+
state.groupCounts.delete(tracked.groupId);
|
|
74
|
+
const lane = state.lanes.get(tracked.groupId);
|
|
75
|
+
if (lane?.jobs.isEmpty && !lane.inRotation)
|
|
76
|
+
state.lanes.delete(tracked.groupId);
|
|
77
|
+
}
|
|
78
|
+
state.groupedCount = Math.max(0, state.groupedCount - 1);
|
|
79
|
+
if (state.groupedCount === 0)
|
|
80
|
+
this.queues.delete(queue);
|
|
81
|
+
}
|
|
82
|
+
return this.queues.has(queue);
|
|
83
|
+
}
|
|
84
|
+
isActive(queue) {
|
|
85
|
+
return this.queues.has(queue);
|
|
86
|
+
}
|
|
87
|
+
peek(queue, now, eligibility) {
|
|
88
|
+
const state = this.queues.get(queue);
|
|
89
|
+
if (!state)
|
|
90
|
+
return { job: null, nextRunAt: null };
|
|
91
|
+
while (true) {
|
|
92
|
+
this.promoteDue(state, now);
|
|
93
|
+
let plain = state.plain.peek();
|
|
94
|
+
while (plain) {
|
|
95
|
+
const job = state.jobs.get(plain.id) ?? plain;
|
|
96
|
+
if (isReady(job, now) || isExpired(job, now))
|
|
97
|
+
return { job, nextRunAt: null };
|
|
98
|
+
this.demoteReady(state, job);
|
|
99
|
+
plain = state.plain.peek();
|
|
100
|
+
}
|
|
101
|
+
let nextRunAt = state.delayed.peek()?.runAt ?? null;
|
|
102
|
+
let groupId = state.cursor;
|
|
103
|
+
let restart = false;
|
|
104
|
+
for (let visited = 0; groupId && visited < state.rotationSize; visited++) {
|
|
105
|
+
const lane = state.lanes.get(groupId);
|
|
106
|
+
if (!lane?.inRotation)
|
|
107
|
+
break;
|
|
108
|
+
const candidate = lane.jobs.peek();
|
|
109
|
+
if (candidate) {
|
|
110
|
+
const job = state.jobs.get(candidate.id) ?? candidate;
|
|
111
|
+
if (!isReady(job, now) && !isExpired(job, now)) {
|
|
112
|
+
this.demoteReady(state, job);
|
|
113
|
+
restart = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const status = eligibility(groupId);
|
|
117
|
+
if (status.eligible)
|
|
118
|
+
return { job, nextRunAt };
|
|
119
|
+
if (status.retryAt !== null) {
|
|
120
|
+
nextRunAt = nextRunAt === null ? status.retryAt : Math.min(nextRunAt, status.retryAt);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
groupId = lane.next;
|
|
124
|
+
}
|
|
125
|
+
if (!restart)
|
|
126
|
+
return { job: null, nextRunAt };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
advance(queue, groupId) {
|
|
130
|
+
const state = this.queues.get(queue);
|
|
131
|
+
const lane = state?.lanes.get(groupId);
|
|
132
|
+
if (state && lane?.inRotation)
|
|
133
|
+
state.cursor = lane.next;
|
|
134
|
+
}
|
|
135
|
+
getGroupJobsCount(queue, groupId) {
|
|
136
|
+
return this.queues.get(queue)?.groupCounts.get(groupId) ?? 0;
|
|
137
|
+
}
|
|
138
|
+
getGroupsJobsCount(queue) {
|
|
139
|
+
return this.queues.get(queue)?.groupedCount ?? 0;
|
|
140
|
+
}
|
|
141
|
+
clearQueue(queue) {
|
|
142
|
+
this.queues.delete(queue);
|
|
143
|
+
}
|
|
144
|
+
createState() {
|
|
145
|
+
return {
|
|
146
|
+
jobs: new Map(),
|
|
147
|
+
placements: new Map(),
|
|
148
|
+
plain: new IndexedPriorityQueue(),
|
|
149
|
+
delayed: new IndexedPriorityQueue(undefined, compareGroupWakeEntries, getGroupFifoOrder),
|
|
150
|
+
lanes: new Map(),
|
|
151
|
+
groupCounts: new Map(),
|
|
152
|
+
groupedCount: 0,
|
|
153
|
+
cursor: null,
|
|
154
|
+
rotationSize: 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
insertTracked(state, job, now) {
|
|
158
|
+
state.jobs.set(job.id, job);
|
|
159
|
+
if (job.groupId) {
|
|
160
|
+
state.groupCounts.set(job.groupId, (state.groupCounts.get(job.groupId) ?? 0) + 1);
|
|
161
|
+
state.groupedCount++;
|
|
162
|
+
}
|
|
163
|
+
if (nextGroupCheckAt(job) > now) {
|
|
164
|
+
state.delayed.push(orderedGroupClone(job, nextGroupCheckAt(job)));
|
|
165
|
+
state.placements.set(job.id, { kind: 'delayed' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.insertReady(state, job);
|
|
169
|
+
}
|
|
170
|
+
promoteDue(state, now) {
|
|
171
|
+
while (true) {
|
|
172
|
+
const scheduled = state.delayed.peek();
|
|
173
|
+
if (!scheduled || scheduled.runAt > now)
|
|
174
|
+
return;
|
|
175
|
+
state.delayed.pop();
|
|
176
|
+
const job = state.jobs.get(scheduled.id);
|
|
177
|
+
if (!job || state.placements.get(job.id)?.kind !== 'delayed')
|
|
178
|
+
continue;
|
|
179
|
+
this.insertReady(state, job);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
insertReady(state, job) {
|
|
183
|
+
if (!job.groupId) {
|
|
184
|
+
state.plain.push(job);
|
|
185
|
+
state.placements.set(job.id, { kind: 'plain' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let lane = state.lanes.get(job.groupId);
|
|
189
|
+
if (!lane) {
|
|
190
|
+
lane = {
|
|
191
|
+
jobs: new IndexedPriorityQueue(undefined, compareGroupFifoEntries, getGroupFifoOrder),
|
|
192
|
+
previous: job.groupId,
|
|
193
|
+
next: job.groupId,
|
|
194
|
+
inRotation: false,
|
|
195
|
+
};
|
|
196
|
+
state.lanes.set(job.groupId, lane);
|
|
197
|
+
}
|
|
198
|
+
lane.jobs.push(orderedGroupClone(job));
|
|
199
|
+
state.placements.set(job.id, { kind: 'group', groupId: job.groupId });
|
|
200
|
+
if (!lane.inRotation)
|
|
201
|
+
this.attachLane(state, job.groupId, lane);
|
|
202
|
+
}
|
|
203
|
+
demoteReady(state, job) {
|
|
204
|
+
const placement = state.placements.get(job.id);
|
|
205
|
+
if (placement?.kind === 'plain') {
|
|
206
|
+
state.plain.remove(job.id);
|
|
207
|
+
}
|
|
208
|
+
else if (placement?.kind === 'group') {
|
|
209
|
+
const lane = state.lanes.get(placement.groupId);
|
|
210
|
+
lane?.jobs.remove(job.id);
|
|
211
|
+
if (lane?.jobs.isEmpty)
|
|
212
|
+
this.detachLane(state, placement.groupId, lane);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
state.delayed.push(orderedGroupClone(job, nextGroupCheckAt(job)));
|
|
218
|
+
state.placements.set(job.id, { kind: 'delayed' });
|
|
219
|
+
}
|
|
220
|
+
attachLane(state, groupId, lane) {
|
|
221
|
+
if (state.cursor === null) {
|
|
222
|
+
lane.previous = groupId;
|
|
223
|
+
lane.next = groupId;
|
|
224
|
+
state.cursor = groupId;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const cursor = state.lanes.get(state.cursor);
|
|
228
|
+
const tail = state.lanes.get(cursor.previous);
|
|
229
|
+
lane.previous = cursor.previous;
|
|
230
|
+
lane.next = state.cursor;
|
|
231
|
+
tail.next = groupId;
|
|
232
|
+
cursor.previous = groupId;
|
|
233
|
+
}
|
|
234
|
+
lane.inRotation = true;
|
|
235
|
+
state.rotationSize++;
|
|
236
|
+
}
|
|
237
|
+
detachLane(state, groupId, lane) {
|
|
238
|
+
if (!lane.inRotation)
|
|
239
|
+
return;
|
|
240
|
+
if (state.rotationSize === 1) {
|
|
241
|
+
state.cursor = null;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const previous = state.lanes.get(lane.previous);
|
|
245
|
+
const next = state.lanes.get(lane.next);
|
|
246
|
+
previous.next = lane.next;
|
|
247
|
+
next.previous = lane.previous;
|
|
248
|
+
if (state.cursor === groupId)
|
|
249
|
+
state.cursor = lane.next;
|
|
250
|
+
}
|
|
251
|
+
lane.inRotation = false;
|
|
252
|
+
state.rotationSize--;
|
|
253
|
+
if (lane.jobs.isEmpty && !state.groupCounts.has(groupId))
|
|
254
|
+
state.lanes.delete(groupId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Job } from '../types/job';
|
|
2
|
+
import type { HeapEntry } from '../types/priorityQueue';
|
|
3
|
+
export declare function orderedGroupClone(job: Job, runAt?: number): Job;
|
|
4
|
+
export declare function nextGroupCheckAt(job: Job): number;
|
|
5
|
+
export declare function compareGroupFifoEntries(left: HeapEntry, right: HeapEntry): number;
|
|
6
|
+
export declare function compareGroupWakeEntries(left: HeapEntry, right: HeapEntry): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { copyGroupFifoOrder } from '../job/groupFifoOrder';
|
|
2
|
+
export function orderedGroupClone(job, runAt = job.runAt) {
|
|
3
|
+
const clone = { ...job, priority: 0, lifo: false, runAt };
|
|
4
|
+
copyGroupFifoOrder(job, clone);
|
|
5
|
+
return clone;
|
|
6
|
+
}
|
|
7
|
+
export function nextGroupCheckAt(job) {
|
|
8
|
+
if (job.ttl === null)
|
|
9
|
+
return job.runAt;
|
|
10
|
+
return Math.min(job.runAt, job.createdAt + job.ttl + 1);
|
|
11
|
+
}
|
|
12
|
+
function compareOrder(left, right) {
|
|
13
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
export function compareGroupFifoEntries(left, right) {
|
|
16
|
+
const order = compareOrder(left.groupFifoOrder ?? left.generation, right.groupFifoOrder ?? right.generation);
|
|
17
|
+
if (order !== 0)
|
|
18
|
+
return order;
|
|
19
|
+
return left.jobId < right.jobId ? -1 : left.jobId > right.jobId ? 1 : 0;
|
|
20
|
+
}
|
|
21
|
+
export function compareGroupWakeEntries(left, right) {
|
|
22
|
+
if (left.runAt !== right.runAt)
|
|
23
|
+
return left.runAt - right.runAt;
|
|
24
|
+
if (left.groupFifoOrder !== undefined && right.groupFifoOrder !== undefined) {
|
|
25
|
+
const order = compareOrder(left.groupFifoOrder, right.groupFifoOrder);
|
|
26
|
+
if (order !== 0)
|
|
27
|
+
return order;
|
|
28
|
+
}
|
|
29
|
+
return left.jobId < right.jobId ? -1 : left.jobId > right.jobId ? 1 : 0;
|
|
30
|
+
}
|
|
@@ -1,44 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Indexed Priority Queue
|
|
3
|
-
* Combines binary heap for O(log n) priority operations
|
|
4
|
-
* with Map for O(1) lookups by job ID
|
|
5
|
-
*/
|
|
6
1
|
import type { Job, JobId } from '../types/job';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
import type { HeapEntry } from '../types/priorityQueue';
|
|
3
|
+
export interface PriorityQueueHooks {
|
|
4
|
+
onInsert(job: Job): boolean;
|
|
5
|
+
onRemove(job: Job): boolean;
|
|
6
|
+
}
|
|
7
|
+
export type PriorityQueueComparator = (left: HeapEntry, right: HeapEntry) => number;
|
|
8
|
+
export type PriorityQueueOrder = (job: Job) => bigint | null;
|
|
9
|
+
/** Indexed map-backed 4-ary heap with O(log4 n) mutations and O(1) lookup. */
|
|
13
10
|
export declare class IndexedPriorityQueue {
|
|
14
|
-
|
|
11
|
+
private readonly hooks?;
|
|
12
|
+
private readonly compare;
|
|
13
|
+
private readonly order?;
|
|
15
14
|
private static readonly D;
|
|
16
15
|
private heap;
|
|
17
16
|
private readonly index;
|
|
18
17
|
private generation;
|
|
19
|
-
|
|
18
|
+
private hooksActive;
|
|
19
|
+
constructor(hooks?: PriorityQueueHooks | undefined, compare?: PriorityQueueComparator, order?: PriorityQueueOrder | undefined);
|
|
20
20
|
get size(): number;
|
|
21
|
-
/** Check if empty */
|
|
22
21
|
get isEmpty(): boolean;
|
|
23
|
-
/** Push a job into the queue */
|
|
24
22
|
push(job: Job): void;
|
|
25
|
-
/** Pop the highest priority job */
|
|
26
23
|
pop(): Job | null;
|
|
27
|
-
/** Peek at the highest priority job without removing */
|
|
28
24
|
peek(): Job | null;
|
|
29
|
-
/** Find a job by ID - O(1) */
|
|
30
25
|
find(jobId: JobId): Job | null;
|
|
31
|
-
/** Check if job exists - O(1) */
|
|
32
26
|
has(jobId: JobId): boolean;
|
|
33
|
-
/** Remove a job by ID - O(1) for index, heap cleans lazily */
|
|
34
27
|
remove(jobId: JobId): Job | null;
|
|
35
28
|
/** Update job priority - O(log n). Optionally update the LIFO tie-break flag. */
|
|
36
29
|
updatePriority(jobId: JobId, newPriority: number, newLifo?: boolean): boolean;
|
|
37
30
|
/** Update job runAt (for delay changes) - O(log n) */
|
|
38
31
|
updateRunAt(jobId: JobId, newRunAt: number): boolean;
|
|
39
|
-
/** Get all jobs (for iteration) */
|
|
40
32
|
values(): Job[];
|
|
41
|
-
/** Clear the queue */
|
|
42
33
|
clear(): void;
|
|
43
34
|
/**
|
|
44
35
|
* Get the ratio of stale entries in the heap
|
|
@@ -51,7 +42,6 @@ export declare class IndexedPriorityQueue {
|
|
|
51
42
|
* O(n) operation but reclaims memory and improves performance
|
|
52
43
|
*/
|
|
53
44
|
compact(): void;
|
|
54
|
-
/** Rebuild heap property from arbitrary array - O(n) */
|
|
55
45
|
private heapify;
|
|
56
46
|
/** Check if compaction is needed (stale ratio > threshold) */
|
|
57
47
|
needsCompaction(threshold?: number): boolean;
|