Вступление: почему ваш сервер спит, когда должен работать
Представьте: пользователь нажимает кнопку «Отправить отчёт», а страница зависает на 30 секунд. Сервер генерирует PDF, сжимает изображения и шлёт email — всё синхронно. Знакомо? Если да — вы попали в ловушку блокирующего кода.
В этой статье мы разберём, как очереди и фоновые задачи на TypeScript решают эту проблему. Вы узнаете, как перенести тяжёлую работу в фоновые процессы, не теряя контроль над ошибками и масштабируемостью. Материал написан для разработчиков, которые уже знакомы с Node.js и хотят поднять свой проект на новый уровень.
Мы поговорим о библиотеках Bull и BullMQ, затронем RabbitMQ и покажем реальные примеры кода на TypeScript. Поехали!
Что такое очередь задач и зачем она нужна
Очередь задач (task queue) — это механизм, который отделяет запрос пользователя от его выполнения. Вместо того чтобы обрабатывать операцию прямо в HTTP-обработчике, мы отправляем задание в очередь, а фоновый воркер забирает его и выполняет асинхронно.
Типичные сценарии использования
- Отправка писем и push-уведомлений
- Генерация PDF, изображений, видео
- Парсинг данных и веб-скрапинг
- Обработка заказов в интернет-магазинах
- Массовые расчёты и отчёты
Без очередей ваше приложение рискует упасть под нагрузкой. С ними — вы получаете отказоустойчивость (если воркер упал, задача не пропадёт) и масштабирование (можно запустить 10 воркеров вместо одного).
Bull и BullMQ: мощные очереди для Node.js
Bull — самая популярная библиотека для работы с очередями в экосистеме Node.js. Она основана на Redis и поддерживает TypeScript из коробки. BullMQ — её преемник, более современный, с поддержкой сандбоксов (изолированных процессов) и продвинутым управлением.
Установка и базовая настройка
npm install bullmq ioredis
npm install -D @types/bull
Создадим очередь и воркер на TypeScript:
// queue.ts
import { Queue } from 'bullmq';
const connection = {
host: 'localhost',
port: 6379,
};
export const emailQueue = new Queue('email', { connection });
// worker.ts
import { Worker } from 'bullmq';
import { sendEmail } from './emailService';
const worker = new Worker('email', async job => {
const { to, subject, body } = job.data;
await sendEmail(to, subject, body);
console.log(`Email sent to ${to}`);
}, { connection });
worker.on('completed', job => {
console.log(`Job ${job.id} completed`);
});
worker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed: ${err.message}`);
});
Теперь в любом месте приложения мы можем добавить задачу:
import { emailQueue } from './queue';
await emailQueue.add('send-welcome', {
to: 'user@example.com',
subject: 'Добро пожаловать!',
body: 'Спасибо за регистрацию...'
});
Просто, правда? Но это только начало. BullMQ даёт гораздо больше возможностей.
Продвинутые возможности BullMQ
- Повторные попытки — если задача упала, её можно автоматически перезапустить через N секунд.
- Задержка выполнения — отложить задачу на определённое время.
- Приоритеты — срочные задачи обрабатываются раньше.
- События — можно подписаться на прогресс, завершение, ошибки.
- Сэндбоксы — выполнение кода в изолированном процессе (безопасность + чистота).
Пример с повторами и задержкой:
await emailQueue.add('send-invoice', {
invoiceId: 123,
email: 'client@example.com'
}, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
delay: 60 * 1000, // 1 минута
});
RabbitMQ: альтернатива для распределённых систем
Если ваш проект использует микросервисную архитектуру, RabbitMQ может быть лучшим выбором. В отличие от Bull, RabbitMQ — это полноценный брокер сообщений, который поддерживает сложные маршруты, обменники (exchanges) и гарантии доставки.
Подключение RabbitMQ к TypeScript
Установим библиотеку amqplib:
npm install amqplib
npm install -D @types/amqplib
Пример продюсера и консюмера:
// producer.ts
import amqp from 'amqplib';
async function sendTask(data: any) {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'task_queue';
await channel.assertQueue(queue, { durable: true });
channel.sendToQueue(queue, Buffer.from(JSON.stringify(data)), {
persistent: true,
});
console.log('Task sent');
setTimeout(() => connection.close(), 500);
}
// consumer.ts
import amqp from 'amqplib';
async function startWorker() {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'task_queue';
await channel.assertQueue(queue, { durable: true });
channel.prefetch(1); // Не брать новую задачу, пока не завершена текущая
channel.consume(queue, async (msg) => {
if (msg) {
const data = JSON.parse(msg.content.toString());
console.log('Processing:', data);
// Имитация работы
await new Promise(resolve => setTimeout(resolve, 2000));
channel.ack(msg);
console.log('Task done');
}
});
}
RabbitMQ даёт гибкость, но требует больше кода и инфраструктуры. Для простых проектов лучше остаться на BullMQ.
Фоновые задачи без внешних зависимостей
Иногда хочется обойтись без Redis или RabbitMQ. Для простых сценариев можно использовать встроенный setTimeout или worker_threads из Node.js.
Пример с worker_threads:
// main.ts
import { Worker } from 'worker_threads';
function runBackgroundTask(data: any): Promise<void> {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
});
});
}
// worker.js (обычный JS, но можно и