@@ -122,6 +122,11 @@ pub struct KafkaTransport {
122122 healthy : Arc < AtomicBool > ,
123123 /// Topics we're subscribed to (for cache warming).
124124 subscribed_topics : Vec < String > ,
125+ /// Shutdown token — cancelled on close() to stop background tasks.
126+ shutdown_token : tokio_util:: sync:: CancellationToken ,
127+ /// Periodic topic refresh handle (auto-discovery mode only).
128+ /// Checked on each recv() call to detect new/removed topics.
129+ topic_refresh : Option < std:: sync:: Mutex < TopicRefreshHandle > > ,
125130}
126131
127132impl KafkaTransport {
@@ -217,20 +222,45 @@ impl KafkaTransport {
217222 . create_with_context ( StatsContext :: new ( ) )
218223 . map_err ( |e| TransportError :: Connection ( format ! ( "Failed to create consumer: {e}" ) ) ) ?;
219224
220- // Resolve effective topics: use explicit list or auto-discover from broker
221- let effective_topics = if config. topics . is_empty ( ) {
222- tracing:: info!( "Topics empty — auto-discovering from broker" ) ;
223- let resolver = topic_resolver:: TopicResolver :: new ( config) ?;
224- let discovered = resolver. resolve ( ) ?;
225- if discovered. is_empty ( ) {
226- return Err ( TransportError :: Config (
227- "Auto-discovery found no matching topics" . into ( ) ,
228- ) ) ;
229- }
230- discovered
231- } else {
232- config. topics . clone ( )
233- } ;
225+ // Resolve effective topics:
226+ // - Explicit list → subscribe to those
227+ // - Empty + auto_discover → auto-discover from broker
228+ // - Empty + !auto_discover → no subscription (producer-only)
229+ let ( effective_topics, topic_refresh, shutdown_token) =
230+ if config. topics . is_empty ( ) && config. auto_discover {
231+ tracing:: info!( "Topics empty — auto-discovering from broker" ) ;
232+ let resolver = topic_resolver:: TopicResolver :: new ( config) ?;
233+ let discovered = resolver. resolve ( ) ?;
234+ if discovered. is_empty ( ) {
235+ return Err ( TransportError :: Config (
236+ "Auto-discovery found no matching topics" . into ( ) ,
237+ ) ) ;
238+ }
239+
240+ let token = tokio_util:: sync:: CancellationToken :: new ( ) ;
241+ let refresh = if config. topic_refresh_secs > 0 {
242+ let refresh_resolver = topic_resolver:: TopicResolver :: new ( config) ?;
243+ let handle = refresh_resolver. start_refresh_loop (
244+ Duration :: from_secs ( config. topic_refresh_secs ) ,
245+ token. clone ( ) ,
246+ ) ;
247+ tracing:: info!(
248+ interval_secs = config. topic_refresh_secs,
249+ "Started periodic topic refresh"
250+ ) ;
251+ Some ( std:: sync:: Mutex :: new ( handle) )
252+ } else {
253+ None
254+ } ;
255+
256+ ( discovered, refresh, token)
257+ } else {
258+ (
259+ config. topics . clone ( ) ,
260+ None ,
261+ tokio_util:: sync:: CancellationToken :: new ( ) ,
262+ )
263+ } ;
234264
235265 // Subscribe to topics
236266 let subscribed_topics = effective_topics;
@@ -273,6 +303,8 @@ impl KafkaTransport {
273303 closed : AtomicBool :: new ( false ) ,
274304 healthy,
275305 subscribed_topics,
306+ shutdown_token,
307+ topic_refresh,
276308 } )
277309 }
278310
@@ -290,6 +322,7 @@ impl TransportBase for KafkaTransport {
290322 async fn close ( & self ) -> TransportResult < ( ) > {
291323 self . closed . store ( true , Ordering :: Relaxed ) ;
292324 self . healthy . store ( false , Ordering :: Relaxed ) ;
325+ self . shutdown_token . cancel ( ) ;
293326 // rdkafka handles cleanup on drop
294327 Ok ( ( ) )
295328 }
@@ -386,6 +419,23 @@ impl TransportReceiver for KafkaTransport {
386419 return Err ( TransportError :: Closed ) ;
387420 }
388421
422+ // Check for topic changes from the background refresh loop
423+ if let Some ( ref refresh) = self . topic_refresh {
424+ if let Ok ( mut handle) = refresh. lock ( ) {
425+ if let Some ( new_topics) = handle. check_changed ( ) {
426+ let topics: Vec < & str > = new_topics. iter ( ) . map ( String :: as_str) . collect ( ) ;
427+ match self . consumer . subscribe ( & topics) {
428+ Ok ( ( ) ) => {
429+ tracing:: info!( ?new_topics, "Re-subscribed after topic refresh" ) ;
430+ }
431+ Err ( e) => {
432+ tracing:: warn!( error = %e, "Failed to re-subscribe after topic refresh" ) ;
433+ }
434+ }
435+ }
436+ }
437+ }
438+
389439 let timeout = Duration :: from_millis ( tuning:: POLL_TIMEOUT_MS ) ;
390440 let max_msgs = max;
391441
@@ -542,6 +592,7 @@ impl std::fmt::Debug for KafkaTransport {
542592 . field ( "subscribed_topics" , & self . subscribed_topics )
543593 . field ( "closed" , & self . closed . load ( Ordering :: Relaxed ) )
544594 . field ( "healthy" , & self . healthy . load ( Ordering :: Relaxed ) )
595+ . field ( "topic_refresh_active" , & self . topic_refresh . is_some ( ) )
545596 . finish_non_exhaustive ( )
546597 }
547598}
0 commit comments