BullMQ Background Jobs System
Panoramica
Sistema di gestione job in background basato su BullMQ e Redis per operazioni asincrone lunghe.
Architettura
Code Disponibili
- ai-analysis - Analisi AI (ricette, ottimizzazione costi, sprechi)
- invoice-import - Importazione fatture XML/PDF
- cassaincloud-sync - Sincronizzazione dati da Cassa in Cloud
- foodcost-recalculation - Ricalcolo food cost prodotti
Componenti
src/
├── lib/
│ └── queue.ts # Configurazione code e helper
├── workers/
│ ├── index.ts # Manager per avviare tutti i worker
│ ├── aiAnalysisWorker.ts
│ ├── invoiceImportWorker.ts
│ ├── cassaInCloudSyncWorker.ts
│ └── foodcostRecalculationWorker.ts
├── app/api/jobs/
│ └── route.ts # API per creare e monitorare job
└── components/jobs/
├── JobMonitor.tsx # UI per monitorare job
└── JobLauncher.tsx # UI per avviare jobSetup
1. Installazione Redis
# macOS con Homebrew
brew install redis
brew services start redis
# Docker
docker run -d -p 6379:6379 redis:alpine
# Linux
sudo apt-get install redis-server
sudo systemctl start redis2. Configurazione Ambiente
Aggiungi a .env.local:
REDIS_HOST=localhost
REDIS_PORT=63793. Avviare i Worker
# Development
npm run workers
# Production
npm run workers:prodOppure con PM2 (configurato in ecosystem.config.js):
pm2 start ecosystem.config.jsUtilizzo
Creare un Job (API)
POST /api/jobs
const response = await fetch("/api/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "ai-analysis",
data: {
analysisType: "recipe",
productId: "507f1f77bcf86cd799439011",
},
}),
});
const { jobId, queueName } = await response.json();Creare un Job (Programmaticamente)
import { addAIAnalysisJob } from "@/lib/queue";
const job = await addAIAnalysisJob({
tenant_id: "tenant123",
userId: "user456",
analysisType: "recipe",
productId: "product789",
});
console.log("Job creato:", job.id);Monitorare un Job
GET /api/jobs?jobId=xxx&queue=aiAnalysis
const response = await fetch(`/api/jobs?jobId=${jobId}&queue=aiAnalysis`);
const jobStatus = await response.json();
console.log("Stato:", jobStatus.state); // active, completed, failed
console.log("Progresso:", jobStatus.progress); // 0-100
console.log("Risultato:", jobStatus.returnvalue);Ottenere tutti i Job di una Coda
GET /api/jobs?queue=aiAnalysis&status=active
const response = await fetch(`/api/jobs?queue=aiAnalysis&status=completed`);
const { jobs, total } = await response.json();Tipi di Job
1. AI Analysis
Tipi di analisi:
recipe- Analisi ricetta con suggerimenti alternativicost-optimization- Ottimizzazione costi fornitoriwaste-analysis- Analisi sprechi e inefficienze
Esempio:
await addAIAnalysisJob({
tenant_id: "...",
userId: "...",
analysisType: "recipe",
productId: "507f1f77bcf86cd799439011",
parameters: {
considerSeasonality: true,
maxAlternatives: 5,
},
});Output:
{
"success": true,
"analysisType": "recipe",
"productId": "...",
"suggestions": [
{
"ingredient": "Pomodoro",
"alternative": "Pomodoro pelato",
"savings": 0.5
}
],
"estimatedSavings": 5.5
}2. Invoice Import
Importa fatture da file XML (FatturaPA) o PDF.
Esempio:
await addInvoiceImportJob({
tenant_id: "...",
userId: "...",
fileUrl: "https://storage.example.com/invoices/IT123.xml",
supplierId: "supplier123",
});Output:
{
"success": true,
"invoiceId": "invoice789",
"invoiceNumber": "FT-001",
"supplier": "Fornitore Test",
"totalAmount": 1000
}Trigger automatico: Al termine, viene creato automaticamente un job di ricalcolo food cost.
3. Cassa in Cloud Sync
Sincronizza dati da Cassa in Cloud.
Tipi di sincronizzazione:
products- Solo prodotticategories- Solo categorieoutlets- Solo punti venditasales- Solo venditefull- Sincronizzazione completa
Esempio:
await addCassaInCloudSyncJob({
tenant_id: "...",
userId: "...",
syncType: "full",
dateFrom: "2024-01-01",
dateTo: "2024-12-31",
idsSalesPoint: [123, 456],
});Output:
{
"success": true,
"syncType": "full",
"results": {
"outlets": { "fetched": 5, "created": 1, "updated": 4 },
"categories": { "fetched": 25, "created": 2, "updated": 23 },
"products": { "fetched": 150, "created": 10, "updated": 140 },
"sales": { "fetched": 5420, "created": 5420 }
},
"syncedAt": "2024-11-22T10:30:00Z"
}4. Foodcost Recalculation
Ricalcola i costi dei prodotti finiti.
Esempio:
await addFoodcostRecalculationJob({
tenant_id: "...",
userId: "...",
recalculateAll: true,
triggeredBy: "invoice-import", // o "price-update" o "manual"
});Output:
{
"success": true,
"triggeredBy": "invoice-import",
"total": 50,
"recalculated": 48,
"errors": 2,
"changes": [
{
"productId": "...",
"oldCost": 10.5,
"newCost": 11.2,
"variance": 6.67
}
],
"significantChanges": [
/* variazioni > 5% */
]
}Componenti UI
JobMonitor
Componente per monitorare i job in tempo reale.
import { JobMonitor } from "@/components/jobs/JobMonitor";
<JobMonitor />;Features:
- Selezione coda e stato
- Auto-refresh ogni 5 secondi per job attivi
- Progress bar per job in corso
- Visualizzazione errori e risultati
JobLauncher
Componente per avviare job facilmente.
import { JobLauncher } from "@/components/jobs/JobLauncher";
<JobLauncher />;Features:
- Form per ogni tipo di job
- Validazione input
- Feedback immediato
Configurazione Worker
Concorrenza
// In worker file
const worker = new Worker("queue-name", handler, {
connection,
concurrency: 2, // Numero di job in parallelo
});Retry e Backoff
await queue.add("job-name", data, {
attempts: 3, // Numero tentativi
backoff: {
type: "exponential",
delay: 5000, // Delay iniziale in ms
},
});Limiter
const worker = new Worker("queue-name", handler, {
connection,
limiter: {
max: 10, // Massimo 10 job
duration: 60000, // per minuto
},
});Monitoraggio e Debugging
Eventi Worker
worker.on("completed", job => {
console.log(`Job ${job.id} completato`);
});
worker.on("failed", (job, err) => {
console.error(`Job ${job?.id} fallito:`, err);
});
worker.on("progress", (job, progress) => {
console.log(`Job ${job.id} progresso: ${progress}%`);
});
worker.on("error", err => {
console.error("Worker error:", err);
});Logs dentro il Job
async (job: Job) => {
await job.log("Inizio elaborazione...");
await job.updateProgress(50);
await job.log("Metà del lavoro completata");
// ...
};BullMQ Board (Dashboard Web)
Installa BullMQ Board per UI di monitoraggio:
npm install @bull-board/express @bull-board/apiScripts NPM
Aggiungi a package.json:
{
"scripts": {
"workers": "tsx watch src/workers/index.ts",
"workers:prod": "tsx src/workers/index.ts"
}
}Gestione Errori
Retry Automatico
Job falliti vengono ritentati automaticamente secondo la configurazione:
{
attempts: 3,
backoff: {
type: "exponential", // o "fixed"
delay: 5000
}
}Dead Letter Queue
Dopo tutti i tentativi falliti, il job va in “failed” state e può essere reinserito manualmente:
const failedJobs = await queue.getFailed();
for (const job of failedJobs) {
await job.retry();
}Best Practices
- Progress Updates: Aggiorna il progress regolarmente per UX migliore
- Idempotenza: I job devono essere idempotenti (eseguibili più volte senza effetti collaterali)
- Timeout: Imposta timeout realistici per evitare job bloccati
- Logging: Logga informazioni utili per debugging
- Tenant Isolation: Ogni job deve includere
tenant_idper isolamento multi-tenant - Cleanup: Pulisci job completati/falliti periodicamente
Troubleshooting
Redis non raggiungibile
Error: connect ECONNREFUSED 127.0.0.1:6379Soluzione: Verifica che Redis sia avviato:
redis-cli ping # Deve rispondere "PONG"Worker non processano job
Verifica:
- Worker è in esecuzione?
ps aux | grep workers - Redis è accessibile?
- Controlla i log del worker
Job bloccati in “active”
Soluzione:
// Pulisci job attivi stalled
await queue.clean(0, 1000, "active");Produzione
PM2 Configuration
// ecosystem.config.js
module.exports = {
apps: [
{
name: "foodcost-workers",
script: "src/workers/index.ts",
interpreter: "tsx",
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "1G",
env: {
NODE_ENV: "production",
},
},
],
};Docker
# Worker container
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["npm", "run", "workers:prod"]Scalabilità
Per gestire più carico, avvia più istanze worker:
# PM2
pm2 start ecosystem.config.js -i 4
# Docker Compose
docker-compose up --scale workers=4Ogni worker processerà job in parallelo dalla stessa coda Redis.