groupmq-plus 1.1.1 → 1.1.3
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/index.cjs +204 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -7
- package/dist/index.d.ts +163 -7
- package/dist/index.js +204 -8
- package/dist/index.js.map +1 -1
- package/dist/lua/change-delay.lua +20 -13
- package/dist/lua/clean-status.lua +13 -4
- package/dist/lua/enqueue-flow.lua +4 -0
- package/dist/lua/remove.lua +13 -3
- package/dist/lua/reserve.lua +2 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -240,6 +240,7 @@ var Job = class Job {
|
|
|
240
240
|
this.timestamp = args.timestamp;
|
|
241
241
|
this.orderMs = args.orderMs;
|
|
242
242
|
this.status = args.status ?? "unknown";
|
|
243
|
+
this.parentId = args.parentId;
|
|
243
244
|
}
|
|
244
245
|
async getState() {
|
|
245
246
|
return this.status ?? "unknown";
|
|
@@ -288,6 +289,39 @@ var Job = class Job {
|
|
|
288
289
|
async waitUntilFinished(timeoutMs = 0) {
|
|
289
290
|
return this.queue.waitUntilFinished(this.id, timeoutMs);
|
|
290
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Get all child jobs of this job (if it's a parent in a flow).
|
|
294
|
+
* @returns Array of child Job instances
|
|
295
|
+
*/
|
|
296
|
+
async getChildren() {
|
|
297
|
+
return this.queue.getFlowChildren(this.id);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get the return values of all child jobs in a flow.
|
|
301
|
+
* @returns Object mapping child job IDs to their return values
|
|
302
|
+
*/
|
|
303
|
+
async getChildrenValues() {
|
|
304
|
+
return this.queue.getFlowResults(this.id);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the number of remaining child jobs that haven't completed yet.
|
|
308
|
+
* @returns Number of remaining dependencies, or null if not a parent job
|
|
309
|
+
*/
|
|
310
|
+
async getDependenciesCount() {
|
|
311
|
+
return this.queue.getFlowDependencies(this.id);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the parent job of this job (if it's a child in a flow).
|
|
315
|
+
* @returns Parent Job instance, or undefined if no parent or parent was deleted
|
|
316
|
+
*/
|
|
317
|
+
async getParent() {
|
|
318
|
+
if (!this.parentId) return void 0;
|
|
319
|
+
try {
|
|
320
|
+
return await this.queue.getJob(this.parentId);
|
|
321
|
+
} catch (_e) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
291
325
|
static fromReserved(queue, reserved, meta) {
|
|
292
326
|
return new Job({
|
|
293
327
|
queue,
|
|
@@ -327,6 +361,7 @@ var Job = class Job {
|
|
|
327
361
|
const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
|
|
328
362
|
const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
|
|
329
363
|
const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
|
|
364
|
+
const parentId = raw.parentId || void 0;
|
|
330
365
|
return new Job({
|
|
331
366
|
queue,
|
|
332
367
|
id,
|
|
@@ -345,7 +380,8 @@ var Job = class Job {
|
|
|
345
380
|
returnvalue,
|
|
346
381
|
timestamp: timestampMs || Date.now(),
|
|
347
382
|
orderMs,
|
|
348
|
-
status: knownStatus ?? coerceStatus(raw.status)
|
|
383
|
+
status: knownStatus ?? coerceStatus(raw.status),
|
|
384
|
+
parentId
|
|
349
385
|
});
|
|
350
386
|
}
|
|
351
387
|
static async fromStore(queue, id) {
|
|
@@ -364,6 +400,7 @@ var Job = class Job {
|
|
|
364
400
|
const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
|
|
365
401
|
const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
|
|
366
402
|
const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
|
|
403
|
+
const parentId = raw.parentId || void 0;
|
|
367
404
|
const [inProcessing, inDelayed] = await Promise.all([queue.redis.zscore(`${queue.namespace}:processing`, id), queue.redis.zscore(`${queue.namespace}:delayed`, id)]);
|
|
368
405
|
let status = raw.status;
|
|
369
406
|
if (inProcessing !== null) status = "active";
|
|
@@ -389,7 +426,8 @@ var Job = class Job {
|
|
|
389
426
|
returnvalue,
|
|
390
427
|
timestamp: timestampMs || Date.now(),
|
|
391
428
|
orderMs,
|
|
392
|
-
status: coerceStatus(status)
|
|
429
|
+
status: coerceStatus(status),
|
|
430
|
+
parentId
|
|
393
431
|
});
|
|
394
432
|
}
|
|
395
433
|
};
|
|
@@ -637,6 +675,38 @@ var Queue = class {
|
|
|
637
675
|
}
|
|
638
676
|
return parsed;
|
|
639
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Gets all child job IDs for a parent job in a flow.
|
|
680
|
+
* @param parentId The ID of the parent job
|
|
681
|
+
* @returns An array of child job IDs
|
|
682
|
+
*/
|
|
683
|
+
async getFlowChildrenIds(parentId) {
|
|
684
|
+
return this.r.smembers(`${this.ns}:flow:children:${parentId}`);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Gets all child jobs for a parent job in a flow.
|
|
688
|
+
* @param parentId The ID of the parent job
|
|
689
|
+
* @returns An array of Job instances for all children
|
|
690
|
+
*/
|
|
691
|
+
async getFlowChildren(parentId) {
|
|
692
|
+
const ids = await this.getFlowChildrenIds(parentId);
|
|
693
|
+
if (ids.length === 0) return [];
|
|
694
|
+
const pipe = this.r.multi();
|
|
695
|
+
for (const id of ids) pipe.hgetall(`${this.ns}:job:${id}`);
|
|
696
|
+
const rows = await pipe.exec();
|
|
697
|
+
const jobs = [];
|
|
698
|
+
for (let i = 0; i < ids.length; i++) {
|
|
699
|
+
const id = ids[i];
|
|
700
|
+
const raw = rows?.[i]?.[1] || {};
|
|
701
|
+
if (!raw || Object.keys(raw).length === 0) {
|
|
702
|
+
this.logger.warn(`Skipping child job ${id} - not found (likely cleaned up)`);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const job = Job.fromRawHash(this, id, raw);
|
|
706
|
+
jobs.push(job);
|
|
707
|
+
}
|
|
708
|
+
return jobs;
|
|
709
|
+
}
|
|
640
710
|
async addSingle(opts) {
|
|
641
711
|
const now = Date.now();
|
|
642
712
|
let delayUntil = 0;
|
|
@@ -2520,7 +2590,10 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2520
2590
|
const oldest = Array.from(this.jobsInProgress)[0];
|
|
2521
2591
|
const now = Date.now();
|
|
2522
2592
|
return {
|
|
2523
|
-
job: oldest.job,
|
|
2593
|
+
job: Job.fromReserved(this.q, oldest.job, {
|
|
2594
|
+
processedOn: oldest.ts,
|
|
2595
|
+
status: "active"
|
|
2596
|
+
}),
|
|
2524
2597
|
processingTimeMs: now - oldest.ts
|
|
2525
2598
|
};
|
|
2526
2599
|
}
|
|
@@ -2530,7 +2603,10 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2530
2603
|
getCurrentJobs() {
|
|
2531
2604
|
const now = Date.now();
|
|
2532
2605
|
return Array.from(this.jobsInProgress).map((item) => ({
|
|
2533
|
-
job: item.job,
|
|
2606
|
+
job: Job.fromReserved(this.q, item.job, {
|
|
2607
|
+
processedOn: item.ts,
|
|
2608
|
+
status: "active"
|
|
2609
|
+
}),
|
|
2534
2610
|
processingTimeMs: now - item.ts
|
|
2535
2611
|
}));
|
|
2536
2612
|
}
|
|
@@ -2560,7 +2636,7 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2560
2636
|
} catch (e) {
|
|
2561
2637
|
const isConnErr = this.q.isConnectionError(e);
|
|
2562
2638
|
if (!isConnErr || !this.stopping) this.logger.error(`Heartbeat error for job ${job.id}:`, e instanceof Error ? e.message : String(e));
|
|
2563
|
-
this.onError?.(e, job);
|
|
2639
|
+
this.onError?.(e, Job.fromReserved(this.q, job, { status: "active" }));
|
|
2564
2640
|
if (!isConnErr || !this.stopping) this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
2565
2641
|
}
|
|
2566
2642
|
}, minInterval);
|
|
@@ -2571,7 +2647,11 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2571
2647
|
heartbeatDelayTimer = setTimeout(() => {
|
|
2572
2648
|
startHeartbeat();
|
|
2573
2649
|
}, heartbeatThreshold);
|
|
2574
|
-
const
|
|
2650
|
+
const jobInstance = Job.fromReserved(this.q, job, {
|
|
2651
|
+
processedOn: jobStartWallTime,
|
|
2652
|
+
status: "active"
|
|
2653
|
+
});
|
|
2654
|
+
const handlerResult = await this.handler(jobInstance);
|
|
2575
2655
|
if (heartbeatDelayTimer) clearTimeout(heartbeatDelayTimer);
|
|
2576
2656
|
if (hbTimer) clearInterval(hbTimer);
|
|
2577
2657
|
const finishedAtWall = Date.now();
|
|
@@ -2595,7 +2675,11 @@ var _Worker = class extends TypedEventEmitter {
|
|
|
2595
2675
|
* Handle job failure: emit events, retry or dead-letter
|
|
2596
2676
|
*/
|
|
2597
2677
|
async handleJobFailure(err, job, jobStartWallTime) {
|
|
2598
|
-
this.
|
|
2678
|
+
const jobInstance = Job.fromReserved(this.q, job, {
|
|
2679
|
+
processedOn: jobStartWallTime,
|
|
2680
|
+
status: "active"
|
|
2681
|
+
});
|
|
2682
|
+
this.onError?.(err, jobInstance);
|
|
2599
2683
|
this.blockingStats.consecutiveEmptyReserves = 0;
|
|
2600
2684
|
this.emptyReserveBackoffMs = 0;
|
|
2601
2685
|
try {
|
|
@@ -2681,9 +2765,122 @@ function sleep(ms) {
|
|
|
2681
2765
|
return new Promise((r) => setTimeout(r, ms));
|
|
2682
2766
|
}
|
|
2683
2767
|
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region src/strategies/priority-strategy.ts
|
|
2770
|
+
var PriorityStrategy = class {
|
|
2771
|
+
constructor(options = {}) {
|
|
2772
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
2773
|
+
this.overrides = /* @__PURE__ */ new Map();
|
|
2774
|
+
this.algorithmConfig = options.algorithm ?? { type: "weighted-random" };
|
|
2775
|
+
this.defaultPriority = options.defaultPriority ?? 1;
|
|
2776
|
+
this.cacheTtlMs = options.cacheTtlMs ?? 5e3;
|
|
2777
|
+
this.onGetPriority = options.onGetPriority;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* 手动覆盖某个组的优先级(优先级高于 Redis 配置和 getPriority)
|
|
2781
|
+
* 主要用于测试或临时调整
|
|
2782
|
+
*/
|
|
2783
|
+
setPriority(groupId, priority) {
|
|
2784
|
+
this.overrides.set(groupId, priority);
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* 清除手动覆盖,恢复使用 Redis 配置或 getPriority
|
|
2788
|
+
*/
|
|
2789
|
+
clearPriority(groupId) {
|
|
2790
|
+
this.overrides.delete(groupId);
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* 获取组的基础优先级
|
|
2794
|
+
* 优先级来源顺序:overrides > cache > onGetPriority(config) 或 config.priority
|
|
2795
|
+
*/
|
|
2796
|
+
async resolvePriority(queue, groupId) {
|
|
2797
|
+
const override = this.overrides.get(groupId);
|
|
2798
|
+
if (override !== void 0) return override;
|
|
2799
|
+
const now = Date.now();
|
|
2800
|
+
const cached = this.cache.get(groupId);
|
|
2801
|
+
if (cached && cached.expiresAt > now) return cached.priority;
|
|
2802
|
+
const config = await queue.getGroupConfig(groupId);
|
|
2803
|
+
let priority;
|
|
2804
|
+
if (this.onGetPriority) priority = await this.onGetPriority(groupId, config);
|
|
2805
|
+
else priority = config.priority !== 1 ? config.priority : this.defaultPriority;
|
|
2806
|
+
if (this.cacheTtlMs > 0) this.cache.set(groupId, {
|
|
2807
|
+
priority,
|
|
2808
|
+
expiresAt: now + this.cacheTtlMs
|
|
2809
|
+
});
|
|
2810
|
+
return priority;
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* 严格优先级算法:总是返回优先级最高的组
|
|
2814
|
+
*/
|
|
2815
|
+
selectByStrict(groups) {
|
|
2816
|
+
groups.sort((a, b) => b.priority - a.priority);
|
|
2817
|
+
return groups[0].groupId;
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* 加权随机算法:根据优先级计算概率选择
|
|
2821
|
+
* 使用 minWeightRatio 确保低优先级组也有机会
|
|
2822
|
+
*/
|
|
2823
|
+
selectByWeightedRandom(groups) {
|
|
2824
|
+
if (groups.length === 1) return groups[0].groupId;
|
|
2825
|
+
const minWeightRatio = this.algorithmConfig.minWeightRatio ?? .1;
|
|
2826
|
+
const minWeight = Math.max(...groups.map((g) => g.priority)) * minWeightRatio;
|
|
2827
|
+
const weights = groups.map((g) => ({
|
|
2828
|
+
groupId: g.groupId,
|
|
2829
|
+
weight: Math.max(g.priority, minWeight)
|
|
2830
|
+
}));
|
|
2831
|
+
const totalWeight = weights.reduce((sum, w) => sum + w.weight, 0);
|
|
2832
|
+
let random = Math.random() * totalWeight;
|
|
2833
|
+
for (const w of weights) {
|
|
2834
|
+
random -= w.weight;
|
|
2835
|
+
if (random <= 0) return w.groupId;
|
|
2836
|
+
}
|
|
2837
|
+
return groups[0].groupId;
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* 时间衰减算法:等待时间越长,优先级加成越高
|
|
2841
|
+
*/
|
|
2842
|
+
selectByAging(groups) {
|
|
2843
|
+
const now = Date.now();
|
|
2844
|
+
const intervalMs = this.algorithmConfig.intervalMs ?? 6e4;
|
|
2845
|
+
const adjustedGroups = groups.map((g) => {
|
|
2846
|
+
let ageBonus = 0;
|
|
2847
|
+
if (g.oldestTimestamp) {
|
|
2848
|
+
const waitTime = now - g.oldestTimestamp;
|
|
2849
|
+
ageBonus = Math.floor(waitTime / intervalMs);
|
|
2850
|
+
}
|
|
2851
|
+
return {
|
|
2852
|
+
groupId: g.groupId,
|
|
2853
|
+
adjustedPriority: g.priority + ageBonus
|
|
2854
|
+
};
|
|
2855
|
+
});
|
|
2856
|
+
adjustedGroups.sort((a, b) => b.adjustedPriority - a.adjustedPriority);
|
|
2857
|
+
return adjustedGroups[0].groupId;
|
|
2858
|
+
}
|
|
2859
|
+
async getNextGroup(queue) {
|
|
2860
|
+
const readyGroups = await queue.getReadyGroups(0, 100);
|
|
2861
|
+
if (readyGroups.length === 0) return null;
|
|
2862
|
+
const groupInfos = await Promise.all(readyGroups.map(async (groupId) => {
|
|
2863
|
+
const priority = await this.resolvePriority(queue, groupId);
|
|
2864
|
+
const info = {
|
|
2865
|
+
groupId,
|
|
2866
|
+
priority
|
|
2867
|
+
};
|
|
2868
|
+
if (this.algorithmConfig.type === "aging") info.oldestTimestamp = await queue.getGroupOldestTimestamp(groupId);
|
|
2869
|
+
return info;
|
|
2870
|
+
}));
|
|
2871
|
+
switch (this.algorithmConfig.type) {
|
|
2872
|
+
case "strict": return this.selectByStrict(groupInfos);
|
|
2873
|
+
case "aging": return this.selectByAging(groupInfos);
|
|
2874
|
+
case "weighted-random":
|
|
2875
|
+
default: return this.selectByWeightedRandom(groupInfos);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
|
|
2684
2880
|
//#endregion
|
|
2685
2881
|
exports.BullBoardGroupMQAdapter = BullBoardGroupMQAdapter;
|
|
2686
2882
|
exports.Job = Job;
|
|
2883
|
+
exports.PriorityStrategy = PriorityStrategy;
|
|
2687
2884
|
exports.Queue = Queue;
|
|
2688
2885
|
exports.UnrecoverableError = UnrecoverableError;
|
|
2689
2886
|
exports.Worker = Worker;
|