@@ -697,11 +697,41 @@ def queue_size(self) -> int:
697697 return self ._queue .qsize ()
698698
699699 def _queue_message (self , data_type : str , obj : Any , receipt_timestamp : Optional [float ] = None ) -> bool :
700+ """Queue a message for processing with backpressure protection.
701+
702+ Args:
703+ data_type: Normalized data type name (e.g., 'trade', 'orderbook')
704+ obj: Message object to queue
705+ receipt_timestamp: Optional receipt timestamp
706+
707+ Returns:
708+ True if message was queued, False if queue was full
709+
710+ Backpressure Strategy (Critical Issue #2):
711+ - If queue is full, log error with structured metadata
712+ - Drop message to prevent blocking upstream data ingestion
713+ - Emit metrics for monitoring and alerting
714+ """
700715 message = _QueuedMessage (data_type = data_type , obj = obj , receipt_timestamp = receipt_timestamp )
716+
717+ # Extract metadata for error logging
718+ exchange = getattr (obj , "exchange" , "unknown" )
719+ symbol = getattr (obj , "symbol" , "unknown" )
720+
701721 try :
702722 self ._queue .put_nowait (message )
703723 except asyncio .QueueFull :
704- LOG .error ("KafkaCallback queue is full; dropping message for %s" , data_type )
724+ LOG .error (
725+ "KafkaCallback queue is full; dropping %s message from %s/%s (queue size: %d)" ,
726+ data_type , exchange , symbol , self ._queue .maxsize ,
727+ extra = {
728+ "exchange" : exchange ,
729+ "symbol" : symbol ,
730+ "data_type" : data_type ,
731+ "queue_size" : self ._queue .maxsize ,
732+ "error_type" : "queue_full" ,
733+ }
734+ )
705735 return False
706736 return True
707737
@@ -788,15 +818,22 @@ def _serialize_payload(self, obj: Any, receipt_timestamp: Optional[float]):
788818 return payload , headers
789819
790820 async def _drain_once (self ) -> None :
791- """Process one message from queue using updated pipeline.
821+ """Process one message from queue using updated pipeline with error handling .
792822
793- Pipeline (Task 4.2-4.3):
823+ Pipeline (Task 4.2-4.3 + Critical Issue #2 ):
794824 1. Get message from queue
795- 2. Serialize payload
796- 3. Extract metadata and generate topic using TopicManager
797- 4. Generate partition key using Partitioner
798- 5. Build headers using HeaderEnricher
799- 6. Produce to Kafka with all components
825+ 2. Serialize payload (with exception handling)
826+ 3. Extract metadata and generate topic using TopicManager (with exception handling)
827+ 4. Generate partition key using Partitioner (with exception handling)
828+ 5. Build headers using HeaderEnricher (with exception handling)
829+ 6. Produce to Kafka with all components (with exception handling)
830+
831+ Error Handling Strategy:
832+ - Serialization errors: Log with structured metadata, skip message
833+ - Topic resolution errors: Log and skip message
834+ - Header enrichment errors: Log warning, fall back to base headers
835+ - Kafka produce errors: Log with retry indication, continue processing
836+ - All errors are logged with exchange, symbol, data_type for debugging
800837 """
801838 message = await self ._queue .get ()
802839 try :
@@ -805,29 +842,126 @@ async def _drain_once(self) -> None:
805842
806843 assert isinstance (message , _QueuedMessage )
807844
808- # Serialize payload
809- payload , base_headers = self ._serialize_payload (message .obj , message .receipt_timestamp )
845+ # Extract metadata for error logging
846+ exchange = getattr (message .obj , "exchange" , "unknown" )
847+ symbol = getattr (message .obj , "symbol" , "unknown" )
848+ data_type = message .data_type
849+
850+ # Step 1: Serialize payload
851+ try :
852+ payload , base_headers = self ._serialize_payload (message .obj , message .receipt_timestamp )
853+ except Exception as e :
854+ LOG .error (
855+ "KafkaCallback: Serialization failed for %s message from %s/%s: %s" ,
856+ data_type , exchange , symbol , e ,
857+ extra = {
858+ "exchange" : exchange ,
859+ "symbol" : symbol ,
860+ "data_type" : data_type ,
861+ "error_type" : "serialization_error" ,
862+ "error" : str (e )
863+ }
864+ )
865+ return # Skip this message, continue processing queue
810866
811- # Generate topic name using TopicManager
812- topic = self ._topic_name (message .data_type , message .obj )
867+ # Step 2: Generate topic name using TopicManager
868+ try :
869+ topic = self ._topic_name (data_type , message .obj )
870+ except Exception as e :
871+ LOG .error (
872+ "KafkaCallback: Topic resolution failed for %s message from %s/%s: %s" ,
873+ data_type , exchange , symbol , e ,
874+ extra = {
875+ "exchange" : exchange ,
876+ "symbol" : symbol ,
877+ "data_type" : data_type ,
878+ "error_type" : "topic_resolution_error" ,
879+ "error" : str (e )
880+ }
881+ )
882+ return # Skip this message, continue processing queue
813883
814- # Generate partition key using Partitioner
815- key = self ._partition_key (message .obj )
884+ # Step 3: Generate partition key using Partitioner
885+ try :
886+ key = self ._partition_key (message .obj )
887+ except Exception as e :
888+ LOG .warning (
889+ "KafkaCallback: Partition key generation failed for %s/%s, using None: %s" ,
890+ exchange , symbol , e ,
891+ extra = {
892+ "exchange" : exchange ,
893+ "symbol" : symbol ,
894+ "data_type" : data_type ,
895+ "error_type" : "partition_key_error" ,
896+ "error" : str (e )
897+ }
898+ )
899+ key = None # Fall back to None (round-robin partition assignment)
816900
817- # Build enriched headers using HeaderEnricher (Task 4.3)
901+ # Step 4: Build enriched headers using HeaderEnricher
818902 try :
819903 enriched_headers = self ._header_enricher .build (
820904 message = message .obj ,
821- data_type = message . data_type
905+ data_type = data_type
822906 )
823- except Exception :
824- # Fallback to base headers if enrichment fails
825- enriched_headers = base_headers
907+ except Exception as e :
908+ LOG .warning (
909+ "KafkaCallback: Header enrichment failed for %s/%s, using base headers: %s" ,
910+ exchange , symbol , e ,
911+ extra = {
912+ "exchange" : exchange ,
913+ "symbol" : symbol ,
914+ "data_type" : data_type ,
915+ "error_type" : "header_enrichment_error" ,
916+ "error" : str (e )
917+ }
918+ )
919+ enriched_headers = base_headers # Fallback to base headers
826920
827- self ._producer .produce (topic , payload , key = key , headers = enriched_headers )
828- self ._producer .poll (0.0 )
921+ # Step 5: Produce to Kafka
922+ try :
923+ self ._producer .produce (topic , payload , key = key , headers = enriched_headers )
924+ self ._producer .poll (0.0 )
925+ except Exception as e :
926+ LOG .error (
927+ "KafkaCallback: Kafka produce failed for %s message from %s/%s on topic %s: %s" ,
928+ data_type , exchange , symbol , topic , e ,
929+ extra = {
930+ "exchange" : exchange ,
931+ "symbol" : symbol ,
932+ "data_type" : data_type ,
933+ "topic" : topic ,
934+ "error_type" : "kafka_produce_error" ,
935+ "error" : str (e )
936+ }
937+ )
938+ # Note: Producer retries are configured in KafkaProducer settings
939+ # We continue processing to avoid blocking the queue on transient errors
940+ except Exception as e :
941+ # Catch-all for unexpected errors to prevent writer task collapse
942+ LOG .error (
943+ "KafkaCallback: Unexpected error in _drain_once: %s" ,
944+ e ,
945+ extra = {
946+ "error_type" : "unexpected_drain_error" ,
947+ "error" : str (e )
948+ },
949+ exc_info = True
950+ )
829951 finally :
830- self ._queue .task_done ()
952+ # Ensure task_done() is called even if errors occur
953+ # Wrap in try/except to prevent finally block failures
954+ try :
955+ self ._queue .task_done ()
956+ except Exception as e :
957+ LOG .error (
958+ "KafkaCallback: Failed to mark task as done: %s" ,
959+ e ,
960+ extra = {
961+ "error_type" : "task_done_error" ,
962+ "error" : str (e )
963+ }
964+ )
831965
832966 async def _writer (self ) -> None :
833967 while self ._running :
0 commit comments