6.3 KiB
Reliability and Fault Handling
This document summarises how the service handles failures, provides resilience, and avoids data loss or duplication.
Dead-Letter Queue (DLQ) and Poison Messages
Configuration is defined under dlq:
[dlq]
enabled = true
subject = "caatsm.dlq"
- When
dlq.enabledistrueanddlq.subjectis non-empty, permanent failures are routed to the DLQ subject. - A permanent failure is indicated by wrapping an error with
app.Permanentand is treated as a poison message.
Note: DLQ routing only applies when the NATS mode is
jetstream. Incoremode, the consumer does not attempt to publish to the DLQ even if it is configured.
Behaviour (JetStream mode):
- The NATS consumer calls
processor.Handle. - If an error is returned and
app.IsPermanent(err)is true:- The original NATS message is copied into a DLQ payload with metadata:
transport_msg_id(NATS message ID)subject,stream,consumernats_sequence,deliverieserror(stringified cause)received_at(DLQ event time)body(raw message body)
- The payload is published to
dlq.subjectusing JetStream. - The original message is ACKed, so it will not be redelivered.
- The original NATS message is copied into a DLQ payload with metadata:
The DLQ subject should be consumed by an offline repair/analysis tool or operational dashboard that can:
- Inspect poison messages.
- Decide whether to fix and re-publish, or discard with justification.
- Track DLQ volume over time for alerting (e.g. via
caatsm_dlq_messages_totalandcaatsm_dlq_publish_failures_totalmetrics).
Transient Errors and Backoff
Transient errors (not marked permanent) result in:
- Negative acknowledgements with delay (
NakWithDelay) according tonats.consumer_rules.backoff. - A retry streak counter inside the consumer:
- Each transient error increases
consecutiveProcessErrors. - After 10 or more consecutive errors, the consumer applies an additional sleep:
backoff = min(consecutive_errors * 100ms, 5s).- A warning log with the sleep duration and error count is emitted.
- Each transient error increases
This combination provides backpressure when downstream systems (especially the DB) are in trouble, slowing down consumption instead of aggressively retrying.
Persistence and Idempotency
The primary persistence path is Repository.InsertOne into aviation.telegrams. To avoid applying the same business event multiple times, a minimal idempotency check is implemented:
- If both
message_idanddate_timeare non-empty:InsertOnefirst callsmessageExists(message_id, date_time).- If a row already exists, the insert is skipped and an informational log is written.
- Otherwise, the insert proceeds.
This makes repeated delivery of the same telegram (same message_id/date_time) safe from a business perspective, even if JetStream redelivers messages or upstream replays.
For higher guarantees in production environments, you may:
- Add a unique index on
(message_id, date_time)at the DB level, and treat any conflict as a duplicate. - Extend the idempotency key with additional fields (e.g. originator, category) if required by the business model.
DB Degradation and Backpressure
Database write failures in Repository.InsertOne and related methods are treated as transient by default:
- Errors propagate back to the NATS consumer.
- The consumer issues a NAK (with delay) and increases the transient error counter.
- When errors persist, the added sleep in the consumer reduces message throughput and gives the DB time to recover.
DB health also feeds into readiness:
- The monitoring server hits
pgxpool.Pool.Pingon/readyzand/healthz. - If the DB is not reachable, the endpoints return
503, signalling to orchestrators that this instance should be drained from traffic.
Together, this yields:
- Backpressure via reduced consumption rate and NATS-level backoff.
- Degradation signalling via health probes for external systems to act upon.
Retry and Max Deliver
JetStream consumer configuration (via nats.consumer_rules) controls:
max_deliver– maximum number of redeliveries before JetStream gives up.ack_wait– how long JetStream waits for an ACK before considering the message pending.backoff– per-attempt delays forNakWithDelay.
Recommended pattern:
- Keep
max_delivermodest (e.g. 5). - Use a backoff array such as
[5s, 30s, 2m]. - Treat messages that still fail after
max_deliveras candidates for DLQ, via the permanent error/poison message path where applicable.
JetStream Availability and Auto-Recovery (Dev vs Prod)
- When the JetStream API is temporarily unavailable (for example, NATS has just
restarted and returns
ErrNoResponders), the consumer uses an exponential backoff when retryingFetchcalls (roughly1s, 2s, 4s, ...up to around30s) to avoid log spam while allowing the system to recover. - In dev/test environments, if the stream or consumer is detected as missing at
runtime (for example after
docker compose down -v), the consumer calls the sharedEnsureStreamandensureConsumerlogic to recreate them and re-establish subscriptions. - In production environments, missing streams/consumers are treated as
configuration or operational errors:
- They are not auto-recreated.
- Errors are logged prominently so operators can diagnose and fix the issue.
- On the publishing side, JetStream
ErrNoRespondersand similar errors are treated as temporary by the processor:- Such errors cause the consumer to NAK messages and rely on the configured backoff for retries.
- Permanent configuration/permission errors remain mapped to permanent failures and follow the DLQ + ACK flow.
Alerts and Dashboards
Prometheus alert suggestions:
-
High failure rate:
rate(caatsm_messages_total{result!="ok"}[5m])above a small threshold.rate(caatsm_retries_total[5m])above a threshold.
-
DLQ growth:
- Alerts on DLQ stream message count, using NATS/JetStream exporter metrics.
-
Readiness / health:
- Alert when
/readyzfails or when DB/NATS checks start failing consistently.
- Alert when
Dashboards should combine:
- Message rates, error rates, and DLQ rates.
- NATS consumer statistics (pending, redelivered, ack_pending).
- DB health indicators (latency, error counts, connection usage).