Skip to content

Commit 04720c9

Browse files
authored
Merge pull request #350 from Vvictor-commits/backend/priority-queue
feat: implement priority-based queue
2 parents 62362b9 + d31f372 commit 04720c9

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

listener/src/services/event-processing-queue.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import * as StellarSDK from '@stellar/stellar-sdk';
22
import { ContractConfig } from '../types';
33
import logger from '../utils/logger';
44

5+
export enum Priority {
6+
Low = 0,
7+
Medium = 1,
8+
High = 2,
9+
}
10+
511
export interface EventProcessingQueueOptions {
612
maxConcurrency?: number;
713
pollIntervalMs?: number;
814
maxRetries?: number;
915
baseDelayMs?: number;
16+
priorityWeights?: { high: number; medium: number; low: number };
1017
}
1118

1219
export type EventProcessor = (
@@ -22,13 +29,16 @@ interface QueuedEvent {
2229
retryCount: number;
2330
nextRetryAt: number;
2431
fingerprint: string;
32+
priority: Priority;
33+
enqueuedAt: number;
2534
}
2635

2736
const DEFAULTS = {
2837
maxConcurrency: 1,
2938
pollIntervalMs: 1_000,
3039
maxRetries: 3,
3140
baseDelayMs: 2_000,
41+
priorityWeights: { high: 5, medium: 2, low: 1 },
3242
};
3343

3444
export class EventProcessingQueue {
@@ -39,21 +49,25 @@ export class EventProcessingQueue {
3949
private readonly pollIntervalMs: number;
4050
private readonly maxRetries: number;
4151
private readonly baseDelayMs: number;
52+
private readonly priorityWeights: { high: number; medium: number; low: number };
4253
private readonly processor: EventProcessor;
4354
private timer: ReturnType<typeof setInterval> | null = null;
55+
private priorityCounters: { high: number; medium: number; low: number } = { high: 0, medium: 0, low: 0 };
4456

4557
constructor(processor: EventProcessor, options?: EventProcessingQueueOptions) {
4658
this.processor = processor;
4759
this.maxConcurrency = Math.max(1, options?.maxConcurrency ?? DEFAULTS.maxConcurrency);
4860
this.pollIntervalMs = options?.pollIntervalMs ?? DEFAULTS.pollIntervalMs;
4961
this.maxRetries = options?.maxRetries ?? DEFAULTS.maxRetries;
5062
this.baseDelayMs = options?.baseDelayMs ?? DEFAULTS.baseDelayMs;
63+
this.priorityWeights = options?.priorityWeights ?? DEFAULTS.priorityWeights;
5164
}
5265

5366
enqueue(
5467
event: StellarSDK.rpc.Api.EventResponse,
5568
contractConfig: ContractConfig,
56-
requestId?: string
69+
requestId?: string,
70+
priority: Priority = Priority.Medium
5771
): boolean {
5872
const fingerprint = buildEventFingerprint(event, contractConfig.address);
5973

@@ -77,6 +91,7 @@ export class EventProcessingQueue {
7791
delayMs,
7892
nextRetryAt: new Date(nextRetryAt).toISOString(),
7993
maxRetries: this.maxRetries,
94+
priority: Priority[priority],
8095
});
8196

8297
this.queuedFingerprints.add(fingerprint);
@@ -87,6 +102,8 @@ export class EventProcessingQueue {
87102
retryCount: 0,
88103
nextRetryAt,
89104
fingerprint,
105+
priority,
106+
enqueuedAt: Date.now(),
90107
});
91108

92109
return true;
@@ -124,10 +141,22 @@ export class EventProcessingQueue {
124141

125142
const due = this.queue
126143
.filter((item) => item.nextRetryAt <= now && !this.activeFingerprints.has(item.fingerprint))
144+
.sort((a, b) => {
145+
const priorityA = this.getWeightedPriority(a);
146+
const priorityB = this.getWeightedPriority(b);
147+
if (priorityB !== priorityA) return priorityB - priorityA;
148+
return a.enqueuedAt - b.enqueuedAt;
149+
})
127150
.slice(0, available);
128151

129152
if (due.length === 0) return;
130153

154+
for (const item of due) {
155+
if (item.priority === Priority.High) this.priorityCounters.high++;
156+
else if (item.priority === Priority.Medium) this.priorityCounters.medium++;
157+
else this.priorityCounters.low++;
158+
}
159+
131160
const selectedFingerprints = new Set(due.map((item) => item.fingerprint));
132161

133162
this.queue = this.queue.filter(
@@ -148,6 +177,19 @@ export class EventProcessingQueue {
148177
}
149178
}
150179

180+
private getWeightedPriority(item: QueuedEvent): number {
181+
const basePriority = item.priority;
182+
const age = Date.now() - item.enqueuedAt;
183+
const ageBonus = Math.floor(age / 60000);
184+
185+
let weight = 0;
186+
if (item.priority === Priority.High) weight = this.priorityWeights.high;
187+
else if (item.priority === Priority.Medium) weight = this.priorityWeights.medium;
188+
else weight = this.priorityWeights.low;
189+
190+
return basePriority + ageBonus + weight;
191+
}
192+
151193
private async processItem(item: QueuedEvent): Promise<void> {
152194
this.activeFingerprints.add(item.fingerprint);
153195

listener/src/services/notification-retry-queue.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ import { getEventName } from '../utils/event-utils';
55
import { getNotificationAnalyticsAggregator, NotificationAnalyticsAggregator } from './notification-analytics-aggregator';
66
import { NotificationType } from '../types/scheduled-notification';
77

8+
export enum Priority {
9+
Low = 0,
10+
Medium = 1,
11+
High = 2,
12+
}
13+
814
export interface RetryQueueOptions {
915
baseDelayMs?: number;
1016
multiplier?: number;
1117
jitter?: boolean;
1218
maxRetries?: number;
1319
processIntervalMs?: number;
20+
priorityWeights?: { high: number; medium: number; low: number };
1421
}
1522

1623
interface RetryItem {
@@ -19,6 +26,8 @@ interface RetryItem {
1926
retryCount: number;
2027
nextRetryAt: number;
2128
requestId?: string;
29+
priority: Priority;
30+
enqueuedAt: number;
2231
}
2332

2433
const DEFAULTS = {
@@ -27,6 +36,7 @@ const DEFAULTS = {
2736
jitter: true,
2837
maxRetries: 5,
2938
processIntervalMs: 5_000,
39+
priorityWeights: { high: 5, medium: 2, low: 1 },
3040
};
3141

3242
export type NotificationFn = (
@@ -43,9 +53,11 @@ export class NotificationRetryQueue {
4353
private readonly jitter: boolean;
4454
private readonly maxRetries: number;
4555
private readonly processIntervalMs: number;
56+
private readonly priorityWeights: { high: number; medium: number; low: number };
4657
private timer: ReturnType<typeof setInterval> | null = null;
4758
private readonly notificationFn: NotificationFn;
4859
private readonly analytics: NotificationAnalyticsAggregator | null;
60+
private priorityCounters: { high: number; medium: number; low: number } = { high: 0, medium: 0, low: 0 };
4961

5062
constructor(notificationFn: NotificationFn, options?: RetryQueueOptions) {
5163
this.notificationFn = notificationFn;
@@ -54,13 +66,15 @@ export class NotificationRetryQueue {
5466
this.jitter = options?.jitter ?? DEFAULTS.jitter;
5567
this.maxRetries = options?.maxRetries ?? DEFAULTS.maxRetries;
5668
this.processIntervalMs = options?.processIntervalMs ?? DEFAULTS.processIntervalMs;
69+
this.priorityWeights = options?.priorityWeights ?? DEFAULTS.priorityWeights;
5770
this.analytics = getNotificationAnalyticsAggregator();
5871
}
5972

6073
enqueue(
6174
event: StellarSDK.rpc.Api.EventResponse,
6275
contractConfig: ContractConfig,
63-
requestId?: string
76+
requestId?: string,
77+
priority: Priority = Priority.Medium
6478
): void {
6579
const fingerprint = buildRetryFingerprint(event, contractConfig.address);
6680

@@ -84,10 +98,11 @@ export class NotificationRetryQueue {
8498
delayMs,
8599
nextRetryAt: new Date(nextRetryAt).toISOString(),
86100
maxRetries: this.maxRetries,
101+
priority: Priority[priority],
87102
});
88103

89104
this.queuedFingerprints.add(fingerprint);
90-
this.queue.push({ event, contractConfig, retryCount: 0, nextRetryAt, requestId });
105+
this.queue.push({ event, contractConfig, retryCount: 0, nextRetryAt, requestId, priority, enqueuedAt: Date.now() });
91106
}
92107

93108
start(): void {
@@ -112,14 +127,41 @@ export class NotificationRetryQueue {
112127

113128
private async processQueue(): Promise<void> {
114129
const now = Date.now();
115-
const due = this.queue.filter((item) => item.nextRetryAt <= now);
130+
const due = this.queue
131+
.filter((item) => item.nextRetryAt <= now)
132+
.sort((a, b) => {
133+
const priorityA = this.getWeightedPriority(a);
134+
const priorityB = this.getWeightedPriority(b);
135+
if (priorityB !== priorityA) return priorityB - priorityA;
136+
return a.enqueuedAt - b.enqueuedAt;
137+
});
138+
116139
this.queue = this.queue.filter((item) => item.nextRetryAt > now);
117140

141+
for (const item of due) {
142+
if (item.priority === Priority.High) this.priorityCounters.high++;
143+
else if (item.priority === Priority.Medium) this.priorityCounters.medium++;
144+
else this.priorityCounters.low++;
145+
}
146+
118147
for (const item of due) {
119148
await this.retryItem(item);
120149
}
121150
}
122151

152+
private getWeightedPriority(item: RetryItem): number {
153+
const basePriority = item.priority;
154+
const age = Date.now() - item.enqueuedAt;
155+
const ageBonus = Math.floor(age / 60000);
156+
157+
let weight = 0;
158+
if (item.priority === Priority.High) weight = this.priorityWeights.high;
159+
else if (item.priority === Priority.Medium) weight = this.priorityWeights.medium;
160+
else weight = this.priorityWeights.low;
161+
162+
return basePriority + ageBonus + weight;
163+
}
164+
123165
private async retryItem(item: RetryItem): Promise<void> {
124166
const attempt = item.retryCount + 1;
125167
const fingerprint = buildRetryFingerprint(item.event, item.contractConfig.address);

0 commit comments

Comments
 (0)