Message Queues and Event-Driven Architecture
Building asynchronous systems with message queues
Challenge
Section titled “Challenge”Decouple services, handle high-volume async operations, and ensure reliable message delivery.
Explanation
Section titled “Explanation”Message queues enable asynchronous communication between services, improving scalability and reliability.
RabbitMQ Basics
Section titled “RabbitMQ Basics”const amqp = require('amqplib');
// Connect to RabbitMQconst connection = await amqp.connect('amqp://localhost');const channel = await connection.createChannel();
// Declare queueawait channel.assertQueue('task_queue', { durable: true });
// Publish messagechannel.sendToQueue('task_queue', Buffer.from('Task data'), { persistent: true});
// Consume messageschannel.consume('task_queue', async (msg) => { console.log('Received:', msg.content.toString());
// Process message // ...
// Acknowledge after processing channel.ack(msg);});
await connection.close();Work Queues (Task Distribution)
Section titled “Work Queues (Task Distribution)”// Publisher (send tasks)async function publishTask(task) { const connection = await amqp.connect('amqp://localhost'); const channel = await connection.createChannel();
await channel.assertQueue('tasks', { durable: true });
channel.sendToQueue('tasks', Buffer.from(JSON.stringify(task)), { persistent: true });
console.log('Task published:', task); await connection.close();}
// Worker (process tasks)async function startWorker() { const connection = await amqp.connect('amqp://localhost'); const channel = await connection.createChannel();
await channel.assertQueue('tasks', { durable: true });
// Process one task at a time channel.prefetch(1);
channel.consume('tasks', async (msg) => { const task = JSON.parse(msg.content.toString()); console.log('Processing:', task);
// Simulate work await processTask(task);
channel.ack(msg); });
console.log('Worker started, waiting for tasks...');}
// UsagepublishTask({ type: 'email', to: 'user@example.com' });await startWorker();Redis as Message Queue
Section titled “Redis as Message Queue”const redis = require('redis');
const publisher = redis.createClient();const subscriber = redis.createClient();
// Publish-Subscribepublisher.on('connect', () => { publisher.publish('notifications', JSON.stringify({ type: 'user_signup', userId: 123 }));});
subscriber.on('connect', async () => { await subscriber.subscribe('notifications');});
subscriber.on('message', (channel, message) => { const event = JSON.parse(message); console.log(`Received on ${channel}:`, event);});
// Message Queues with Listsconst client = redis.createClient();
// Push to queueawait client.lpush('queue:tasks', JSON.stringify({ task: 'process' }));
// Pop from queueconst task = await client.rpop('queue:tasks');console.log(task);
// Blocking pop (wait for message)const [key, task] = await client.blpop('queue:tasks', 0);Bull: Node.js Queue Library
Section titled “Bull: Node.js Queue Library”const Queue = require('bull');
// Create queueconst emailQueue = new Queue('emails', 'redis://localhost:6379');
// Add jobconst job = await emailQueue.add( { email: 'user@example.com', subject: 'Welcome' }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
// Process jobsemailQueue.process(async (job) => { console.log('Sending email:', job.data);
// Send email await sendEmail(job.data);
// Update progress job.progress(50);
return { success: true };});
// Listen to eventsemailQueue.on('completed', (job) => { console.log('Job completed:', job.id);});
emailQueue.on('failed', (job, err) => { console.log('Job failed:', job.id, err.message);});
// Get job statusconst status = await job.getState();const progress = await job.progress();Pub/Sub Pattern
Section titled “Pub/Sub Pattern”const EventEmitter = require('events');
class EventBus extends EventEmitter { publishAsync(event, data) { setImmediate(() => { this.emit(event, data); }); }}
const eventBus = new EventBus();
// Multiple subscriberseventBus.on('order:created', async (order) => { console.log('Sending confirmation email...'); await sendEmail(order);});
eventBus.on('order:created', async (order) => { console.log('Updating inventory...'); await updateInventory(order);});
eventBus.on('order:created', async (order) => { console.log('Recording analytics...'); await recordAnalytics(order);});
// Publish eventeventBus.publishAsync('order:created', { id: 1, userId: 123, items: [...]});Error Handling and Retry
Section titled “Error Handling and Retry”const queue = new Queue('tasks', 'redis://localhost');
queue.process(async (job) => { try { await processTask(job.data); } catch (error) { // Exponential backoff retry if (job.attemptsMade < 3) { throw error; // Retry } else { // Move to dead letter queue await queue.add(job.data, { attempts: 0 }); throw error; } }});
queue.on('failed', (job, error) => { console.error(`Job ${job.id} failed:`, error.message); // Alert monitoring system alertMonitoring({ jobId: job.id, error: error.message, attempts: job.attemptsMade });});Best Practices
Section titled “Best Practices”// 1. Always acknowledge after processingchannel.consume(queue, (msg) => { process(msg); channel.ack(msg); // Don't forget!});
// 2. Set timeoutsqueue.process(async (job) => { return Promise.race([ processTask(job.data), timeout(30000) // 30 second timeout ]);});
// 3. Use dead letter queuesconst dlq = new Queue('dlq');queue.on('failed', (job) => { dlq.add(job.data, { retries: 0 });});
// 4. Monitor queue depthsetInterval(async () => { const count = await queue.count(); metrics.recordQueueDepth(count);}, 60000);