2626_INITIAL_BACKOFF_TIME = 0.02 # seconds
2727_MAXIMUM_BACKOFF_TIME = 1.0 # seconds
2828_STREAMING_HEADER_KEY = "streaming-imas-metadata"
29+ # Kafka server will wait maximal _FETCH_WAIT_MAX_MS before sending new messages to the
30+ # consumer. Adjusted from the default (500ms) to decrease latency, especially when using
31+ # "most_recent_only" since a seek() needs to wait at least this amount of time before it
32+ # is effective (see KafkaConsumer._fast_forward()).
33+ _FETCH_WAIT_MAX_MS = 50 # milli-seconds
2934
3035
3136class KafkaSettings (BaseModel ):
@@ -151,7 +156,8 @@ def __init__(
151156 settings : KafkaSettings ,
152157 stream_consumer_cls : type [StreamConsumer ],
153158 * ,
154- timeout = DEFAULT_KAFKA_CONSUMER_TIMEOUT ,
159+ timeout : int = DEFAULT_KAFKA_CONSUMER_TIMEOUT ,
160+ most_recent_only : bool = False ,
155161 ** stream_consumer_kwargs ,
156162 ) -> None :
157163 """Create a new KafkaConsumer.
@@ -163,13 +169,23 @@ def __init__(
163169 settings: Kafka host and topic to connect to.
164170 stream_consumer_cls: StreamConsumer type used for processing the received
165171 messages.
172+
173+ Keyword Args:
166174 timeout: Maximum time (in seconds) to wait for the topic.
175+ most_recent_only: Set to True to only receive the most recent message with
176+ every iteration of ``stream()``.
177+ stream_consumser_kwargs: any additional keyword arguments are forwarded to
178+ the constructor of ``stream_consumer_cls``.
167179 """
168180 self ._settings = settings
181+ self ._most_recent_only = most_recent_only
169182 conf = {
170183 "bootstrap.servers" : settings .host ,
171184 "auto.offset.reset" : "earliest" ,
172185 "group.id" : str (uuid .uuid4 ()),
186+ # This influences the latency of receiving messages. Also impacts the seek()
187+ # in self._fast_forward()!
188+ "fetch.wait.max.ms" : _FETCH_WAIT_MAX_MS ,
173189 }
174190 self ._consumer = confluent_kafka .Consumer (conf )
175191
@@ -220,7 +236,7 @@ def _subscribe(self, timeout) -> StreamingIMASMetadata:
220236 raise RuntimeError ("Timeout reached while waiting for streaming metadata." )
221237 if msg .error () is not None :
222238 raise msg .error ()
223- headers = dict (msg .headers ())
239+ headers = dict (msg .headers () or [] )
224240 if _STREAMING_HEADER_KEY not in headers :
225241 raise RuntimeError (
226242 f"Topic '{ topic_name } ' does not contain IMAS streaming metadata."
@@ -244,6 +260,8 @@ def stream(self, *, timeout=DEFAULT_KAFKA_CONSUMER_TIMEOUT) -> Iterator[Any]:
244260 """
245261 try :
246262 while True :
263+ if self ._most_recent_only :
264+ self ._fast_forward ()
247265 msg = self ._consumer .poll (timeout )
248266 if msg is None :
249267 logger .info (
@@ -263,3 +281,19 @@ def stream(self, *, timeout=DEFAULT_KAFKA_CONSUMER_TIMEOUT) -> Iterator[Any]:
263281 finally :
264282 self ._consumer .commit ()
265283 self ._consumer .close ()
284+
285+ def _fast_forward (self ) -> None :
286+ """Fast forward the Kafka stream, so the last available message will be returned
287+ next."""
288+ assignment = self ._consumer .assignment ()
289+ if len (assignment ) != 1 :
290+ raise RuntimeError (f"Expected a single topic assignment, got { assignment } " )
291+ cur_offset = self ._consumer .position (assignment )[0 ]
292+ _ , high_watermark = self ._consumer .get_watermark_offsets (assignment [0 ])
293+ # Check if we're not already at the end
294+ if cur_offset .offset < high_watermark - 1 :
295+ cur_offset .offset = high_watermark - 1
296+ self ._consumer .seek (cur_offset )
297+ # Wait for the in-flight request to return, seek takes effect afterwards
298+ # See: https://www.feldera.com/blog/seeking-in-kafka
299+ time .sleep (_FETCH_WAIT_MAX_MS / 1000 )
0 commit comments