|
1 | | -import { Logger } from "@/lib/utils/logging" |
2 | 1 | import amqp from "amqplib" |
3 | 2 |
|
4 | | -const logger = new Logger("RABBITMQ") |
5 | | - |
6 | | -const INITIAL_RETRY_DELAY_MS = parseInt(process.env.RABBITMQ_RETRY_INITIAL_DELAY_MS ?? "1000") |
7 | | -const MAX_RETRY_DELAY_MS = parseInt(process.env.RABBITMQ_RETRY_MAX_DELAY_MS ?? "30000") |
8 | | - |
9 | | -export function jitteredDelay(attempt: number): number { |
10 | | - const exponential = Math.min(INITIAL_RETRY_DELAY_MS * 2 ** attempt, MAX_RETRY_DELAY_MS) |
11 | | - return exponential * (0.5 + 0.5 * Math.random()) |
12 | | -} |
13 | | - |
14 | 3 | export class RabbitConnection { |
15 | | - private readonly user: string |
16 | | - private readonly password: string |
17 | | - private readonly port: number |
18 | | - private readonly heartbeat: number |
19 | | - private conn: amqp.Connection | null = null |
20 | | - private connectPromise: Promise<void> | null = null |
21 | | - private destroyed = false |
22 | | - |
23 | | - constructor(args: { |
24 | | - user: string |
25 | | - password: string |
26 | | - port: string | number |
27 | | - heartbeat?: number |
28 | | - }) { |
| 4 | + private user: string |
| 5 | + private password: string |
| 6 | + private port: number |
| 7 | + private isReady = false |
| 8 | + private isConnecting = false |
| 9 | + private conn: Promise<amqp.Connection | null> = Promise.resolve(null) |
| 10 | + |
| 11 | + constructor(args: { user: string; password: string; port: string | number }) { |
29 | 12 | this.user = args.user |
30 | 13 | this.password = args.password |
31 | 14 | this.port = parseInt(args.port.toString()) |
32 | | - this.heartbeat = args.heartbeat ?? parseInt(process.env.RABBITMQ_HEARTBEAT ?? "60") |
33 | | - } |
34 | | - |
35 | | - private async connectWithRetry(): Promise<void> { |
36 | | - let attempt = 0 |
37 | | - while (!this.destroyed) { |
38 | | - if (attempt > 0) { |
39 | | - const delay = jitteredDelay(attempt - 1) |
40 | | - logger.info("RABBITMQ_RECONNECTING", { attempt, delayMs: Math.round(delay) }) |
41 | | - await new Promise(resolve => setTimeout(resolve, delay)) |
42 | | - } |
43 | | - try { |
44 | | - const conn = await amqp.connect( |
45 | | - `amqp://${this.user}:${this.password}@localhost:${this.port}`, |
46 | | - { heartbeat: this.heartbeat } |
47 | | - ) |
48 | | - this.conn = conn |
49 | | - logger.info("RABBITMQ_CONNECTED", { attempt }) |
50 | | - conn.on("close", () => this.handleConnectionLoss("close")) |
51 | | - conn.on("error", err => |
52 | | - this.handleConnectionLoss( |
53 | | - "error", |
54 | | - err instanceof Error ? err : new Error(String(err)) |
55 | | - ) |
56 | | - ) |
57 | | - return |
58 | | - } catch (err) { |
59 | | - logger.warn( |
60 | | - "RABBITMQ_CONNECT_FAILED", |
61 | | - err instanceof Error ? err : new Error(String(err)), |
62 | | - { attempt } |
63 | | - ) |
64 | | - attempt++ |
65 | | - } |
66 | | - } |
67 | 15 | } |
68 | 16 |
|
69 | | - private handleConnectionLoss(event: string, err?: Error): void { |
70 | | - if (this.conn) { |
71 | | - logger.warn("RABBITMQ_CONNECTION_LOST", err ?? null, { event }) |
72 | | - this.conn = null |
73 | | - } |
74 | | - if (!this.destroyed && !this.connectPromise) { |
75 | | - this.connectPromise = this.connectWithRetry().finally(() => { |
76 | | - this.connectPromise = null |
77 | | - }) |
| 17 | + private async connect() { |
| 18 | + if (!this.isConnecting && !this.isReady) { |
| 19 | + this.isConnecting = true |
| 20 | + this.conn = amqp |
| 21 | + .connect(`amqp://${this.user}:${this.password}@localhost:${this.port}`) |
| 22 | + .finally(() => { |
| 23 | + this.isConnecting = false |
| 24 | + this.isReady = true |
| 25 | + }) |
| 26 | + await this.conn |
78 | 27 | } |
79 | 28 | } |
80 | 29 |
|
81 | | - async createChannel(): Promise<amqp.Channel> { |
82 | | - if (!this.conn) { |
83 | | - if (!this.connectPromise) { |
84 | | - this.connectPromise = this.connectWithRetry().finally(() => { |
85 | | - this.connectPromise = null |
86 | | - }) |
87 | | - } |
88 | | - await this.connectPromise |
89 | | - } |
90 | | - const conn = this.conn |
| 30 | + async createChannel() { |
| 31 | + await this.connect() |
| 32 | + const conn = await this.conn |
91 | 33 | if (!conn) { |
92 | 34 | throw new Error("Failed to connect to RabbitMQ") |
93 | 35 | } |
94 | | - return conn.createChannel() |
| 36 | + return await conn.createChannel() |
95 | 37 | } |
96 | 38 |
|
97 | | - $disconnect(): void { |
98 | | - this.destroyed = true |
99 | | - this.conn?.close() |
| 39 | + $disconnect() { |
| 40 | + this.conn.then(conn => conn?.close()) |
100 | 41 | } |
101 | 42 | } |
0 commit comments