livrare lot 2
This commit is contained in:
commit
8ecc78e729
763 changed files with 164593 additions and 0 deletions
|
|
@ -0,0 +1,234 @@
|
|||
/**
|
||||
* RabbitMQ Connection Manager
|
||||
*
|
||||
* Lazy initialization with graceful degradation.
|
||||
* When RabbitMQ is unavailable, async operations fall back to sync.
|
||||
*/
|
||||
|
||||
import amqp from 'amqplib';
|
||||
import { getRabbitMQUrl, getRabbitMQConfig, EXCHANGE_NAME } from '../shared/queue/constants';
|
||||
import { log } from '../shared/logger';
|
||||
|
||||
// Use any to avoid amqplib type issues
|
||||
let connection: any = null;
|
||||
let channel: any = null;
|
||||
let confirmChannel: any = null;
|
||||
let isConnecting = false;
|
||||
let lastConnectionAttempt = 0;
|
||||
// 2s between retries (was 5s). Tuned for HA clusters where HAProxy may need
|
||||
// a few seconds to reroute after failover; shorter backoff = faster recovery.
|
||||
const CONNECTION_RETRY_DELAY = 2000;
|
||||
|
||||
/**
|
||||
* Get or create RabbitMQ connection
|
||||
*/
|
||||
export async function getConnection(): Promise<any> {
|
||||
if (connection) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Prevent concurrent connection attempts
|
||||
if (isConnecting) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Rate limit connection attempts
|
||||
const now = Date.now();
|
||||
if (now - lastConnectionAttempt < CONNECTION_RETRY_DELAY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
lastConnectionAttempt = now;
|
||||
|
||||
try {
|
||||
const url = getRabbitMQUrl();
|
||||
const cfg = getRabbitMQConfig();
|
||||
const safeTarget = `${cfg.host}:${cfg.port}${cfg.vhost} (user: ${cfg.user})`;
|
||||
log.info(`[RabbitMQ] Connecting to ${safeTarget}...`);
|
||||
|
||||
connection = await amqp.connect(url);
|
||||
|
||||
connection.on('error', (err: Error) => {
|
||||
log.error(`[RabbitMQ] Connection error to ${cfg.host}${cfg.vhost}:`, err.message);
|
||||
connection = null;
|
||||
channel = null;
|
||||
confirmChannel = null;
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
log.warn(`[RabbitMQ] Connection closed to ${cfg.host}${cfg.vhost}`);
|
||||
connection = null;
|
||||
channel = null;
|
||||
confirmChannel = null;
|
||||
});
|
||||
|
||||
connection.on('blocked', (reason: string) => {
|
||||
log.warn(`[RabbitMQ] Connection blocked by broker: ${reason}`);
|
||||
});
|
||||
connection.on('unblocked', () => {
|
||||
log.info('[RabbitMQ] Connection unblocked');
|
||||
});
|
||||
|
||||
log.info(`[RabbitMQ] ✓ Connected to ${safeTarget}`);
|
||||
return connection;
|
||||
} catch (err) {
|
||||
log.warn('[RabbitMQ] Connection failed:', (err as Error).message);
|
||||
return null;
|
||||
} finally {
|
||||
isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create channel for consuming messages
|
||||
*/
|
||||
export async function getChannel(): Promise<any> {
|
||||
if (channel) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
if (!conn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
channel = await conn.createChannel();
|
||||
|
||||
// Ensure exchange exists
|
||||
await channel.assertExchange(EXCHANGE_NAME, 'topic', { durable: true });
|
||||
|
||||
channel.on('error', (err: Error) => {
|
||||
log.error('[RabbitMQ] Channel error:', err.message);
|
||||
channel = null;
|
||||
});
|
||||
|
||||
channel.on('close', () => {
|
||||
log.warn('[RabbitMQ] Channel closed');
|
||||
channel = null;
|
||||
});
|
||||
|
||||
return channel;
|
||||
} catch (err) {
|
||||
log.error('[RabbitMQ] Failed to create channel:', (err as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create confirm channel for publishing with confirms
|
||||
*/
|
||||
export async function getConfirmChannel(): Promise<any> {
|
||||
if (confirmChannel) {
|
||||
return confirmChannel;
|
||||
}
|
||||
|
||||
const conn = await getConnection();
|
||||
if (!conn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
confirmChannel = await conn.createConfirmChannel();
|
||||
|
||||
// Ensure exchange exists
|
||||
await confirmChannel.assertExchange(EXCHANGE_NAME, 'topic', { durable: true });
|
||||
|
||||
confirmChannel.on('error', (err: Error) => {
|
||||
log.error('[RabbitMQ] Confirm channel error:', err.message);
|
||||
confirmChannel = null;
|
||||
});
|
||||
|
||||
confirmChannel.on('close', () => {
|
||||
log.warn('[RabbitMQ] Confirm channel closed');
|
||||
confirmChannel = null;
|
||||
});
|
||||
|
||||
return confirmChannel;
|
||||
} catch (err) {
|
||||
log.error('[RabbitMQ] Failed to create confirm channel:', (err as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish on a confirm channel and WAIT for the broker ack.
|
||||
*
|
||||
* Without this, publishes on a confirm channel are fire-and-forget: the broker
|
||||
* can nack (queue overflow, internal error) or the connection can drop before
|
||||
* delivery, and the message is silently lost while the caller reports success.
|
||||
* Throws on nack/error so callers can fall back (sync path, error result).
|
||||
*/
|
||||
export function publishConfirmed(
|
||||
ch: any,
|
||||
exchange: string,
|
||||
routingKey: string,
|
||||
content: Buffer,
|
||||
options: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ch.publish(exchange, routingKey, content, options, (err: unknown) => {
|
||||
if (err) reject(err instanceof Error ? err : new Error(String(err)));
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if RabbitMQ is available
|
||||
*/
|
||||
export async function isRabbitMQAvailable(): Promise<boolean> {
|
||||
const conn = await getConnection();
|
||||
return conn !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all connections gracefully
|
||||
*/
|
||||
export async function closeConnection(): Promise<void> {
|
||||
try {
|
||||
if (channel) {
|
||||
await channel.close();
|
||||
channel = null;
|
||||
}
|
||||
if (confirmChannel) {
|
||||
await confirmChannel.close();
|
||||
confirmChannel = null;
|
||||
}
|
||||
if (connection) {
|
||||
await connection.close();
|
||||
connection = null;
|
||||
}
|
||||
log.info('[RabbitMQ] Connections closed');
|
||||
} catch (err) {
|
||||
log.error('[RabbitMQ] Error closing connections:', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
export async function healthCheck(): Promise<{
|
||||
connected: boolean;
|
||||
host: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const url = getRabbitMQUrl();
|
||||
const host = url.replace(/amqp:\/\/[^@]+@/, '').replace(/\/.*$/, '');
|
||||
|
||||
try {
|
||||
const conn = await getConnection();
|
||||
return {
|
||||
connected: conn !== null,
|
||||
host,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
connected: false,
|
||||
host,
|
||||
error: (err as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue