docs(specs): initialize OpenSpec and define telecom CDR pipeline specifications

This commit is contained in:
2026-09-19 13:09:00 +03:30
commit 5f75a1a2be
28 changed files with 2866 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
## Context
See `proposal.md` for background and problem motivation.
The architecture is constrained to pure Python without web frameworks (FastAPI, Flask, Starlette, or Django). The pipeline must ingest high-throughput telecom CDRs, decouple ingestion from processing via Kafka, normalize heterogeneous fields, micro-batch into ClickHouse with at-least-once delivery guarantees, and provide complete operational observability via Prometheus metrics and structured JSON logging.
## Goals / Non-Goals
**Goals:**
- Implement a non-blocking HTTP ingestion server using Python standard library `asyncio` primitives.
- Enforce backpressure using bounded async queues (`429 Too Many Requests`).
- Publish to Kafka topic `cdr.raw` using `raw_id` partition keys with `confluent-kafka`.
- Build a resilient worker that normalizes phone numbers (E.164 via `phonenumbers`), maps RATs, generates deterministic UUIDv5 `cdr_id`s, and safely routes bad data to `cdr.raw.dlq`.
- Micro-batch inserts into ClickHouse (`ReplacingMergeTree`) with configurable count (1,000) and time (2s) flush thresholds.
- Guarantee at-least-once delivery by committing Kafka offsets only after ClickHouse write ACK.
- Expose dual-service Prometheus `/metrics` and aggregate public `/stats` via an internal worker HTTP endpoint.
- Emit structured JSON logs across all services.
- Containerize all services in `docker-compose.yml` with health checks and `condition: service_healthy`.
**Non-Goals:**
- Web-based visualization UI (analytical queries and verification scripts provide reporting).
- Schema Registry broker integration (deferred from optional bonus challenges).
- Distributed multi-worker consumer coordination (single worker instance fulfills baseline requirements).
## Decisions
### 1. Framework-less `asyncio` HTTP Server
- **Choice**: Implement the HTTP listener using `asyncio.start_server` with a lightweight, zero-dependency HTTP/1.1 request parser.
- **Rationale**: Strictly satisfies the constraint of "pure Python without frameworks" while delivering high concurrent connection handling and native integration with `asyncio.Queue` for backpressure.
- **Alternatives Considered**:
- `http.server.ThreadingHTTPServer`: Standard library, but thread-per-request model has higher memory overhead and makes bounded async backpressure coordination harder.
- `FastAPI`/`Starlette`: Disallowed by project constraints.
### 2. Service Separation and Stats Aggregation
- **Choice**: Run `Ingestion API` and `Processing Worker` as separate Docker containers. The public `/stats` endpoint is hosted by the Ingestion API, which queries a lightweight internal HTTP endpoint on the Worker (`GET http://worker:8001/internal/stats`) and aggregates with local ingestion metrics.
- **Rationale**: Isolates HTTP ingestion from processing CPU/network bottlenecks. Enables independent scaling and lifecycle management.
- **Alternatives Considered**:
- Single combined monolithic process: Violates explicit requirement for separate services.
- Querying ClickHouse directly for `/stats`: Incurs database query overhead and does not report real-time in-flight buffer size or consumer lag.
### 3. Kafka Client and Partitioning Strategy
- **Choice**: Use `confluent-kafka` with `raw_id` as the message key when publishing to `cdr.raw`.
- **Rationale**: `confluent-kafka` is built on `librdkafka` and offers superior throughput over pure Python clients. Using `raw_id` ensures deterministic partition affinity for duplicate CDR submissions while spreading traffic evenly across the 3 partitions via Murmur2 hashing.
- **Alternatives Considered**:
- `caller_num` as key: Risk of partition hotspots if certain subscribers generate disproportionate traffic.
- `null` (round-robin): Loses record-level partition affinity.
### 4. ClickHouse Client and Idempotent Deduplication
- **Choice**: Use `clickhouse-connect` HTTP client targeting table `telecom.cdr_records` with engine `ReplacingMergeTree(processed_at)`.
- **Rationale**: ClickHouse recommends HTTP batch inserts. `ReplacingMergeTree` combined with deterministic UUIDv5 `cdr_id` ensures that if a batch is replayed after a worker crash prior to offset commit, duplicate records with identical order keys are safely merged without data duplication.
- **Alternatives Considered**:
- `clickhouse-driver` (native TCP protocol): Slightly higher client-side setup complexity; `clickhouse-connect` is the official modern ClickHouse client.
- Standard `MergeTree`: Does not deduplicate replayed batches, violating idempotency.
### 5. Micro-Batching and Fault Tolerance Strategy
- **Choice**: Worker accumulates records into an in-memory batch. Flushes when `len(batch) >= BATCH_SIZE` (default 1,000) or elapsed time >= `BATCH_FLUSH_INTERVAL_SECONDS` (default 2s).
- **Failure Handling**:
- If ClickHouse write times out, worker retries with exponential backoff up to `CH_RETRY_MAX_ATTEMPTS` (default 3, initial delay 500ms, factor 2.0).
- If all retries fail, the entire batch is wrapped and routed to `cdr.raw.dlq`, and offsets are committed to prevent pipeline stall.
- On `SIGTERM`, consumer stops, remaining buffer is flushed to ClickHouse, offsets are committed, and worker exits cleanly.
### 6. Structured JSON Logging
- **Choice**: Standard library Python logging with a custom JSON formatter producing fields: `timestamp`, `level`, `service`, `raw_id`, and `event`.
- **Rationale**: Clean, zero-dependency implementation producing machine-readable logs compatible with modern observability stacks.
## Risks / Trade-offs
- **[Risk] High ingestion bursts saturate in-memory queue** → **[Mitigation]** The bounded `asyncio.Queue` rejects requests with `429 Too Many Requests` as soon as capacity is reached, protecting process memory.
- **[Risk] ClickHouse outage stalls worker** → **[Mitigation]** Retries are bounded to 3 attempts with exponential backoff. If ClickHouse remains unreachable, batch is offloaded to DLQ and offsets committed to prevent infinite consumer lag loops.
- **[Risk] Worker crashes between ClickHouse ACK and Kafka commit** → **[Mitigation]** Re-consumed batch is re-inserted; `ReplacingMergeTree(processed_at)` deduplicates rows sharing the order key `(service_type, radio_access_type, cell_id, session_start_time, msisdn)`.
- **[Risk] Phone number parsing performance overhead** → **[Mitigation]** Use `phonenumbers` library with fast-path prefix checks (`00`, `011`, `+`) before full parsing.