## Purpose Defines stream consumption, safe parsing, schema normalization, micro-batching into ClickHouse, and at-least-once offset management for raw telecom CDRs. ## ADDED Requirements ### Requirement: Safe Parsing and Fault Tolerance The Processing Worker SHALL consume messages from `cdr.raw` and process them without crashing, routing malformed or unprocessable payloads to `cdr.raw.dlq`. #### Scenario: Malformed or unparseable JSON payload - **WHEN** a message consumed from `cdr.raw` cannot be parsed as valid JSON - **THEN** the worker wraps the raw message in a DLQ envelope with `failure_reason: "malformed_json"` and publishes it to `cdr.raw.dlq` without terminating the process #### Scenario: Missing required field - **WHEN** a consumed record is missing any required field (`raw_id`, `device_id`, `caller_num`, `svc`, `rat`, `epoch_start`, `epoch_end`, `up_bytes`, `down_bytes`, `lac`, `ci`) or contains null for a required field - **THEN** the worker routes the record to `cdr.raw.dlq` with `failure_reason: "missing_required_field"` and `failed_field` set to the missing field name #### Scenario: Inverted epoch timestamps - **WHEN** a consumed record has `epoch_start > epoch_end` - **THEN** the worker routes the record to `cdr.raw.dlq` with `failure_reason: "invalid_epoch"` and `failed_field: "epoch_start"` #### Scenario: Negative byte counts - **WHEN** a consumed record has `up_bytes < 0` or `down_bytes < 0` - **THEN** the worker routes the record to `cdr.raw.dlq` with `failure_reason: "normalization_error"` and `failed_field` set to the negative byte field ### Requirement: E.164 Phone Number Normalization The Processing Worker SHALL normalize `caller_num` into a valid E.164 format `msisdn` using the `phonenumbers` library. #### Scenario: Leading international prefix normalization - **WHEN** a valid record contains `caller_num` starting with `00` or `011` - **THEN** the worker replaces the prefix with `+`, parses the number using `phonenumbers`, and sets `msisdn` to the standardized E.164 string #### Scenario: Existing E.164 number preserved - **WHEN** a record contains `caller_num` already formatted with a leading `+` - **THEN** the worker parses and validates the number using `phonenumbers` and outputs the verified E.164 string #### Scenario: Unparseable phone number routed to DLQ - **WHEN** `caller_num` cannot be normalized or validated by `phonenumbers` - **THEN** the worker routes the record to `cdr.raw.dlq` with `failure_reason: "normalization_error"` and `failed_field: "caller_num"` ### Requirement: Radio Access Type Mapping The Processing Worker SHALL map the integer `rat` field to standard string representations. #### Scenario: Supported RAT values mapped - **WHEN** a record has `rat` equal to `1`, `6`, or `9` - **THEN** the worker maps `radio_access_type` to `"3G"`, `"LTE_4G"`, or `"5G_NR"` respectively #### Scenario: Unsupported RAT value routed to DLQ - **WHEN** a record has `rat` not equal to 1, 6, or 9 - **THEN** the worker routes the record to `cdr.raw.dlq` with `failure_reason: "invalid_rat_value"` and `failed_field: "rat"` ### Requirement: Schema Transformation and Deterministic Identity The Processing Worker SHALL transform valid raw CDRs into the normalized schema with deterministic identity derivation. #### Scenario: Deterministic UUIDv5 cdr_id generation - **WHEN** a raw record with `raw_id` is normalized - **THEN** the worker computes `cdr_id` as `uuid.uuid5(uuid.NAMESPACE_DNS, raw_id)` ensuring identical IDs for reprocessed duplicate records #### Scenario: Computed metrics and cell identification - **WHEN** raw fields are mapped to the normalized schema - **THEN** `cell_id` is set to `lac + "_" + ci`, `total_bytes` is computed as `up_bytes + down_bytes`, `duration_seconds` is computed as `epoch_end - epoch_start`, `session_start_time` and `session_end_time` are converted to UTC DateTimes, and `source_ip` defaults to `"0.0.0.0"` if absent ### Requirement: Micro-Batching Buffer Strategy The Processing Worker SHALL buffer normalized records in memory and flush batches to ClickHouse based on size or elapsed time thresholds. #### Scenario: Batch flushed on count threshold - **WHEN** the in-memory normalized record buffer reaches `BATCH_SIZE` (default 1,000 records) - **THEN** the worker immediately flushes the batch to ClickHouse #### Scenario: Batch flushed on time interval threshold - **WHEN** `BATCH_FLUSH_INTERVAL_SECONDS` (default 2 seconds) elapses since the last flush and the buffer contains at least one record - **THEN** the worker flushes the accumulated buffer to ClickHouse ### Requirement: At-Least-Once Delivery and Offset Management The Processing Worker SHALL commit Kafka consumer offsets only after ClickHouse successfully acknowledges the batch write. #### Scenario: Offsets committed after successful write - **WHEN** a micro-batch is written and acknowledged by ClickHouse - **THEN** the worker commits the highest offsets corresponding to the records in that batch #### Scenario: ClickHouse write timeout retry with exponential backoff - **WHEN** a batch write to ClickHouse fails or times out - **THEN** the worker retries the write up to `CH_RETRY_MAX_ATTEMPTS` (default 3) with exponential backoff (`CH_RETRY_INITIAL_DELAY_MS` multiplied by `CH_RETRY_BACKOFF_FACTOR`); if all attempts fail, the worker routes all records in the batch to `cdr.raw.dlq` and commits offsets to avoid pipeline deadlock ### Requirement: Internal Runtime Statistics Endpoint The Processing Worker SHALL expose a lightweight internal HTTP endpoint (e.g., `GET /internal/stats`) to provide its runtime statistics to the Ingestion API. #### Scenario: Internal stats queried - **WHEN** an HTTP request is made to the worker's internal stats endpoint - **THEN** the worker returns HTTP status `200 OK` with JSON containing `total_processed`, `total_dlq`, `in_flight_buffer`, `last_flush_at`, and breakdown counts by service type, RAT, and DLQ reason ### Requirement: Graceful Shutdown Without Data Loss The Processing Worker SHALL handle `SIGTERM` and `SIGINT` signals to flush buffered records before terminating. #### Scenario: SIGTERM received during operation - **WHEN** a `SIGTERM` signal is received by the worker process - **THEN** the worker stops consuming new messages, flushes any remaining in-memory buffered records to ClickHouse, commits the associated Kafka offsets, and exits cleanly with exit code 0