ADR-0007: Pipeline CDR — NATS JetStream come bus durevole
Status
Accepted — supersede l'indicazione del docx v5.2 cap. 4.2 ("NATS queue + worker") chiarendone l'edizione (JetStream, non core NATS) e dello STARTUP_PROGETTO.md §13 ("arq su Redis") chiarendone lo scope (job UI/admin, non CDR).
Superseded by: ADR-0016 per l'implementazione concreta della pipeline CDR e ADR-0019 per il placement NATS sul tier stateful.
Context
Il flusso CDR di Akira ha caratteristiche peculiari:
- Volumetria: target di sostenibilità 10k CDR/s (peak), con burst durante eventi commerciali.
- Origine: Kamailio modulo
acc_jsonemette un record JSON ad ogni evento di accounting (200 OK, BYE, CANCEL, failure). - Destinazione: TimescaleDB hypertable
cdr_*(chunk 1 giorno). - Durabilità richiesta: se il DB rallenta o va giù, non possiamo perdere CDR (impatto fatturazione diretto). Buffer minimo 24h, target 48h.
- Ordering: best-effort, non strict (i CDR hanno timestamp self-contained).
- Backpressure: se il consumer è lento, il broker deve assorbire — Kamailio non può essere bloccato.
Le opzioni preesistenti nel progetto:
- arq + Redis (raccomandato in STARTUP §13 per job): Redis è in-memory, persistenza AOF non garantisce durabilità a 24-48h volumi 10k/s senza dimensionamenti enormi. Non è il tool giusto per CDR.
- NATS core: at-most-once, no durabilità → inadatto.
- Kafka: durabilità eccellente ma operationally pesante (Zookeeper/KRaft, retention complessa, dimensione team operations).
- INSERT diretto da Kamailio: nessun buffer → se DB lento, perdita CDR o blocco Kamailio.
Decision
Adottare NATS JetStream come bus durevole dedicato per la pipeline CDR. arq+Redis resta invariato per job UI/admin.
Topologia pipeline
Kamailio (acc_json)
│
▼ publish subject "cdr.raw"
NATS JetStream stream "AKIRA_CDR"
│ retention=24h staging / 48h prod, MaxMsgs=10M
▼ pull consumer "cdr-worker"
cdr-worker (Python, asyncpg COPY)
│
▼ INSERT batch (5000 row chunk)
TimescaleDB hypertable cdr_*
Subject naming
cdr.raw— pubblicato da Kamailio, schema JSON nativoacc_json.cdr.>— wildcard inclusa nello streamAKIRA_CDRper future estensioni della pipeline senza ricreare lo stream.cdr.rated— pubblicato post-rating engine (Fase 2), per consumer downstream (alerting, balance update).cdr.failed— DLQ per record che hanno fallito parsing/insert N volte.
Stream config
| Param | Staging | Prod |
|---|---|---|
| MaxAge | 24h | 48h |
| MaxMsgs | 10M | 50M |
| Replicas | 1 | 3 |
| Storage | file | file |
| Retention | limits | limits |
| Discard | old | old |
Backpressure & alerting
- Se backlog (
num_pending) del consumercdr-worker> 30s di throughput medio → alert Prometheus. - Se backlog > 5 min → alert critico, valutare scale-out worker.
- Se stream
MaxMsgsraggiunto al 80% → alert capacità.
Deployment
- Staging: NATS server single-node su
akira-cache-01-staging, tier stateful definito da ADR-0019, JetStream file storage persistente. - Produzione (Fase 2 Opzione A): cluster NATS 3 nodi su VM dedicate (
nats-01,nats-02,nats-03), 200GB SSD ciascuna, replicas=3.
arq + Redis resta per
- Generazione PDF report.
- Future Tariff applier (cron applica tariffe schedulate).
- IMAP poller (cap. 16.6, 49.2.4, 58.9).
Pattern Analyzer scheduled.Superseded by ADR-0028: Pattern Analyzer scheduled runs use Postgres-backedpattern_runsclaim/lease with a systemd-managed worker; NATS is only an optional wakeup, not the durable job state.- Telegram outbound queue (rate limit + retry).
- Email outbound (Open Ticket auto-email).
Consequences
Positive
- Durabilità reale: 24-48h buffer permette di tollerare DB down totale senza perdita CDR.
- Disaccoppiamento: Kamailio pubblica fire-and-forget, nessun back-pressure sul signaling path.
- Scalabilità consumer: pull-based, possiamo aggiungere worker se backlog cresce.
- Replay: in caso di bug nel cdr-worker, possiamo ri-consumare da timestamp X (durable consumer cursor).
Negative
- +1 sistema da operare: NATS server (staging) o cluster (prod).
- Operational complexity: monitoring backlog, replication health, disk usage.
- Curva apprendimento: il team deve familiarizzare con JetStream (subject vs stream vs consumer).
Neutral
- 2 broker in produzione (Redis per arq, NATS per CDR) — è una decisione consapevole: ognuno fa il suo mestiere.
Implementation hints
Client Python
- Libreria:
nats-py(async-native, supporta JetStream). - Connessione condivisa per app, singleton.
- Pull consumer con
fetch(batch=500, timeout=2s)loop.
Batch insert TimescaleDB
- Usare
asyncpg.connection.copy_records_to_table(COPY) — ordini di grandezza più veloce di INSERT. - Chunk size 5000 record o flush a 2s timeout (whichever first).
- Ack JetStream dopo commit DB (at-least-once → idempotenza su
cdr_uuidPK / UPSERT ON CONFLICT DO NOTHING).
Kamailio side
- Modulo
acc_jsonconfigurato per publish via TCP a NATS bridge (script Lua o Python sidecar che fa il publish). - Alternative: scrivere su file rotativo + filebeat-style sidecar che pubblica (più robusto se NATS down brevemente).
Subject naming convention
cdr.raw— input grezzo.cdr.>— subject set dello streamAKIRA_CDR, cosi'cdr.processed,cdr.failedecdr.auditpossono essere introdotti senza migrazione stream.cdr.rated— dopo rating.cdr.failed— DLQ (max 3 retry su raw, poi qui).
Stream config Ansible
- Role:
roles/nats/. - Stream config in
roles/nats/files/streams/cdr.json. - Applicato via
nats stream add --configtask.
Alternatives considered
- arq + Redis per CDR: scartato — Redis AOF a 10k/s + 24h retention richiede RAM enorme e fsync killer per throughput.
- Kafka: scartato — overkill per il team size, operational burden alto, valutabile se cresciamo a > 100k/s.
- Direct INSERT da Kamailio: scartato — no backpressure, no durabilità, accoppia Kamailio al DB.
- NATS core (no JetStream): scartato — at-most-once, perdita CDR garantita su failure.
- RabbitMQ: scartato — più pesante di NATS JetStream, throughput inferiore per use case durable, team meno familiare.
- PostgreSQL LISTEN/NOTIFY: scartato — non è broker, non ha durabilità sui messaggi.