1919
2020import datetime
2121import logging
22+ import re
2223from collections import defaultdict
24+ from typing import TypeVar
2325
2426from confluent_kafka import Message , KafkaException , TopicPartition
2527
28+ from .error import SendError
2629from .config import ProducerConfig , ConsumerConfig , ConsumeTopicConfig
2730from .headers import (
2831 TIMESTAMP_HEADER ,
3639
3740
3841LOGGER = logging .getLogger (__name__ )
42+ T = TypeVar ("T" )
3943
4044
4145def _get_retry_timestamp (message : Message ) -> float | None :
@@ -76,6 +80,39 @@ def _get_current_timestamp() -> float:
7680 return datetime .datetime .now (tz = datetime .timezone .utc ).timestamp ()
7781
7882
83+ def _regex_lookup_and_update_in_dict (
84+ input_key : str , lookup_dict : dict [str , T ]
85+ ) -> T | None :
86+ """
87+ Lookup a key in a dictionary. If not present, try matching
88+ the input key with a dictionary key. If matched, update the
89+ lookup dict and return the corresponding value.
90+
91+ This is useful for searching and updating lookup tables
92+ for retry configs and producers.
93+
94+ Args:
95+ input_key: Usually topic name as present in the received message,
96+ the key we search for.
97+ lookup_dict: Lookup dict, it's keys can be either
98+ direct matches or regex patterns
99+ Returns:
100+ The item from the lookup dict, or None if no item was found.
101+ """
102+ result = lookup_dict .get (input_key , None )
103+ if result is not None :
104+ return result
105+ for dict_key , dict_value in lookup_dict .items ():
106+ try :
107+ if re .match (dict_key , input_key ):
108+ # Store the lookup for next time
109+ lookup_dict [input_key ] = dict_value
110+ return dict_value
111+ except re .PatternError :
112+ pass
113+ return None
114+
115+
79116class RetryScheduleCache :
80117 """
81118 Class for storing information about messages that are blocked
@@ -254,7 +291,9 @@ def _get_retry_headers(
254291 message: Kafka message that will be retried
255292 Returns: dictionary of retry headers used for next sending
256293 """
257- relevant_config = self .__topic_lookup .get (message .topic )
294+ relevant_config = _regex_lookup_and_update_in_dict (
295+ message .topic , self .__topic_lookup
296+ )
258297 if relevant_config is None :
259298 return None
260299 previous_attempt = _get_retry_attempt (message )
@@ -277,7 +316,9 @@ def resend_message(self, message: MessageGroup) -> None:
277316 message: the Kafka message that failed to be processed
278317 """
279318 message_topic = message .topic
280- relevant_producer = self .__retry_producers .get (message_topic )
319+ relevant_producer = _regex_lookup_and_update_in_dict (
320+ message_topic , self .__retry_producers
321+ )
281322 if relevant_producer is None :
282323 LOGGER .debug (
283324 "Message %s from topic %s does not have configured retry topic." ,
@@ -287,7 +328,9 @@ def resend_message(self, message: MessageGroup) -> None:
287328 return
288329
289330 # Check if we've exhausted retry attempts
290- relevant_config = self .__topic_lookup .get (message_topic )
331+ relevant_config = _regex_lookup_and_update_in_dict (
332+ message_topic , self .__topic_lookup
333+ )
291334 if relevant_config is not None :
292335 current_attempt = _get_retry_attempt (message )
293336 if current_attempt >= relevant_config .retries :
@@ -309,7 +352,7 @@ def resend_message(self, message: MessageGroup) -> None:
309352 message_topic ,
310353 extra = {"message_raw" : str (message .all_chunks )},
311354 )
312- except (TypeError , BufferError , KafkaException ):
355+ except (TypeError , BufferError , KafkaException , SendError ):
313356 LOGGER .exception (
314357 "Cannot resend message from topic: %s to its retry topic %s" ,
315358 message_topic ,
0 commit comments