/** * BullMQ Queue Service * * Zentrale Queue-Verwaltung für Background Jobs. * Verwendet Redis als Backend (gleiche Instanz wie Caching). */ import { Queue, QueueEvents, JobsOptions } from 'bullmq' import IORedis from 'ioredis' // Queue-Namen export const QUEUE_NAMES = { EMAIL: 'email', PDF: 'pdf', CLEANUP: 'cleanup', YOUTUBE_UPLOAD: 'youtube-upload', } as const export type QueueName = (typeof QUEUE_NAMES)[keyof typeof QUEUE_NAMES] // Redis-Konfiguration für Queue (separate DB) const QUEUE_REDIS_DB = parseInt(process.env.QUEUE_REDIS_DB || '1', 10) // Gemeinsame Redis-Connection für alle Queues let redisConnection: IORedis | null = null /** * Erstellt oder gibt die existierende Redis-Connection zurück */ export function getQueueRedisConnection(): IORedis { if (!redisConnection) { const host = process.env.REDIS_HOST || 'localhost' const port = parseInt(process.env.REDIS_PORT || '6379', 10) redisConnection = new IORedis({ host, port, db: QUEUE_REDIS_DB, password: process.env.REDIS_PASSWORD || undefined, maxRetriesPerRequest: null, // BullMQ requirement enableReadyCheck: false, }) redisConnection.on('error', (err) => { console.error('[Queue] Redis connection error:', err.message) }) redisConnection.on('connect', () => { console.log(`[Queue] Redis connected (db: ${QUEUE_REDIS_DB})`) }) } return redisConnection } // Queue-Instanzen Cache const queues = new Map() const queueEvents = new Map() /** * Standard Job-Optionen */ export const defaultJobOptions: JobsOptions = { attempts: parseInt(process.env.QUEUE_DEFAULT_RETRY || '3', 10), backoff: { type: 'exponential', delay: 1000, // 1s, 2s, 4s, 8s, ... }, removeOnComplete: { count: 100, // Behalte letzte 100 erfolgreiche Jobs age: 24 * 60 * 60, // Oder 24 Stunden }, removeOnFail: { count: 500, // Behalte letzte 500 fehlgeschlagene Jobs age: 7 * 24 * 60 * 60, // Oder 7 Tage }, } /** * Erstellt oder gibt eine existierende Queue zurück */ export function getQueue(name: QueueName): Queue { if (!queues.has(name)) { const queue = new Queue(name, { connection: getQueueRedisConnection(), defaultJobOptions, }) queues.set(name, queue) console.log(`[Queue] Queue "${name}" initialized`) } return queues.get(name)! } /** * Erstellt oder gibt QueueEvents für eine Queue zurück */ export function getQueueEvents(name: QueueName): QueueEvents { if (!queueEvents.has(name)) { const events = new QueueEvents(name, { connection: getQueueRedisConnection(), }) queueEvents.set(name, events) } return queueEvents.get(name)! } /** * Gibt alle aktiven Queues zurück (für bull-board) */ export function getAllQueues(): Queue[] { // Initialisiere alle Queues falls noch nicht geschehen Object.values(QUEUE_NAMES).forEach((name) => getQueue(name)) return Array.from(queues.values()) } /** * Schließt alle Queue-Verbindungen (für graceful shutdown) */ export async function closeAllQueues(): Promise { console.log('[Queue] Closing all queues...') // Schließe QueueEvents const eventEntries = Array.from(queueEvents.values()) for (const events of eventEntries) { await events.close() } queueEvents.clear() // Schließe Queues const queueEntries = Array.from(queues.values()) for (const queue of queueEntries) { await queue.close() } queues.clear() // Schließe Redis Connection if (redisConnection) { await redisConnection.quit() redisConnection = null } console.log('[Queue] All queues closed') } /** * Prüft ob Queue-System verfügbar ist */ export async function isQueueAvailable(): Promise { try { const connection = getQueueRedisConnection() const pong = await connection.ping() return pong === 'PONG' } catch { return false } } /** * Queue-Status für Monitoring */ export interface QueueStatus { name: string waiting: number active: number completed: number failed: number delayed: number paused: boolean } /** * Gibt den Status aller Queues zurück */ export async function getQueuesStatus(): Promise { const statuses: QueueStatus[] = [] const entries = Array.from(queues.entries()) for (const [name, queue] of entries) { const [waiting, active, completed, failed, delayed, isPaused] = await Promise.all([ queue.getWaitingCount(), queue.getActiveCount(), queue.getCompletedCount(), queue.getFailedCount(), queue.getDelayedCount(), queue.isPaused(), ]) statuses.push({ name, waiting, active, completed, failed, delayed, paused: isPaused, }) } return statuses }