@@ -20,17 +20,19 @@ defmodule KafkaesqueClient.Producer do
2020 :batch_timer ,
2121 :callbacks ,
2222 :metrics ,
23- :flush_waiters
23+ :flush_waiters ,
24+ :cleanup_timer
2425 ]
2526
2627 @ type t :: % __MODULE__ {
2728 config: map ( ) ,
2829 pool: atom ( ) | pid ( ) ,
2930 batch: [ ProducerRecord . t ( ) ] ,
3031 batch_timer: reference ( ) | nil ,
31- callbacks: % { reference ( ) => function ( ) } ,
32+ callbacks: % { reference ( ) => { function ( ) | { :sync , GenServer . from ( ) } , integer ( ) } } ,
3233 metrics: map ( ) ,
33- flush_waiters: [ GenServer . from ( ) ]
34+ flush_waiters: [ GenServer . from ( ) ] ,
35+ cleanup_timer: reference ( ) | nil
3436 }
3537
3638 @ type send_callback :: (
@@ -52,6 +54,7 @@ defmodule KafkaesqueClient.Producer do
5254 - `:linger_ms` - Time to wait before sending incomplete batches (default: 100)
5355 - `:max_retries` - Maximum number of retries (default: 3)
5456 - `:retry_backoff_ms` - Backoff between retries (default: 100)
57+ - `:callback_timeout_ms` - Timeout for callbacks before cleanup (default: 30000)
5558 """
5659 def start_link ( config ) when is_map ( config ) do
5760 case Config . validate_producer_config ( config ) do
@@ -81,13 +84,22 @@ defmodule KafkaesqueClient.Producer do
8184 end
8285
8386 @ doc """
84- Flushes any pending records in the batch.
87+ Flushes any pending records in the batch synchronously .
8588 """
8689 @ spec flush ( GenServer . server ( ) , timeout ( ) ) :: :ok
8790 def flush ( producer , timeout \\ 5_000 ) do
8891 GenServer . call ( producer , :flush , timeout )
8992 end
9093
94+ @ doc """
95+ Flushes any pending records in the batch asynchronously.
96+ Returns immediately without waiting for the batch to be sent.
97+ """
98+ @ spec flush_async ( GenServer . server ( ) ) :: :ok
99+ def flush_async ( producer ) do
100+ GenServer . cast ( producer , :flush_async )
101+ end
102+
91103 @ doc """
92104 Closes the producer, flushing any pending records.
93105 """
@@ -118,14 +130,20 @@ defmodule KafkaesqueClient.Producer do
118130 records_sent: 0 ,
119131 batches_sent: 0 ,
120132 errors: 0 ,
121- retries: 0
133+ retries: 0 ,
134+ callbacks_timeout: 0
122135 } ,
123- flush_waiters: [ ]
136+ flush_waiters: [ ] ,
137+ cleanup_timer: nil
124138 }
125139
126140 # Start batch timer if linger_ms > 0
127141 state = maybe_start_batch_timer ( state )
128142
143+ # Start callback cleanup timer
144+ cleanup_timer = Process . send_after ( self ( ) , :cleanup_callbacks , 5_000 )
145+ state = % { state | cleanup_timer: cleanup_timer }
146+
129147 { :ok , state }
130148 end
131149
@@ -135,7 +153,7 @@ defmodule KafkaesqueClient.Producer do
135153
136154 new_callbacks =
137155 if callback do
138- Map . put ( state . callbacks , ref , callback )
156+ Map . put ( state . callbacks , ref , { callback , System . monotonic_time ( :millisecond ) } )
139157 else
140158 state . callbacks
141159 end
@@ -154,12 +172,20 @@ defmodule KafkaesqueClient.Producer do
154172 end
155173 end
156174
175+ def handle_cast ( :flush_async , state ) do
176+ if Enum . empty? ( state . batch ) do
177+ { :noreply , state }
178+ else
179+ { :noreply , send_batch ( state ) }
180+ end
181+ end
182+
157183 @ impl true
158184 def handle_call ( { :send_sync , record } , from , state ) do
159185 ref = make_ref ( )
160186
161187 # Store the caller to reply when batch is sent
162- new_callbacks = Map . put ( state . callbacks , ref , { :sync , from } )
188+ new_callbacks = Map . put ( state . callbacks , ref , { { :sync , from } , System . monotonic_time ( :millisecond ) } )
163189
164190 record_with_ref = { record , ref }
165191 new_batch = [ record_with_ref | state . batch ]
@@ -187,10 +213,13 @@ defmodule KafkaesqueClient.Producer do
187213 # Send any pending batch
188214 final_state = if Enum . empty? ( state . batch ) , do: state , else: send_batch ( state )
189215
190- # Cancel timer if active
216+ # Cancel timers if active
191217 if final_state . batch_timer do
192218 Process . cancel_timer ( final_state . batch_timer )
193219 end
220+ if final_state . cleanup_timer do
221+ Process . cancel_timer ( final_state . cleanup_timer )
222+ end
194223
195224 { :stop , :normal , :ok , final_state }
196225 end
@@ -204,6 +233,33 @@ defmodule KafkaesqueClient.Producer do
204233 { :noreply , send_batch ( % { state | batch_timer: nil } ) }
205234 end
206235
236+ def handle_info ( :cleanup_callbacks , state ) do
237+ now = System . monotonic_time ( :millisecond )
238+ timeout_ms = Map . get ( state . config , :callback_timeout_ms , 30_000 )
239+
240+ { expired , active } =
241+ Map . split_with ( state . callbacks , fn { _ref , { _cb , timestamp } } ->
242+ now - timestamp > timeout_ms
243+ end )
244+
245+ # Execute expired callbacks with timeout error
246+ Enum . each ( expired , fn { _ref , { callback , _timestamp } } ->
247+ execute_callback_with_error ( callback , :timeout )
248+ end )
249+
250+ # Update metrics
251+ new_metrics = if map_size ( expired ) > 0 do
252+ % { state . metrics | callbacks_timeout: state . metrics . callbacks_timeout + map_size ( expired ) }
253+ else
254+ state . metrics
255+ end
256+
257+ # Schedule next cleanup
258+ cleanup_timer = Process . send_after ( self ( ) , :cleanup_callbacks , 5_000 )
259+
260+ { :noreply , % { state | callbacks: active , cleanup_timer: cleanup_timer , metrics: new_metrics } }
261+ end
262+
207263 # Private Functions
208264
209265 defp should_send_batch? ( state ) do
@@ -330,10 +386,10 @@ defmodule KafkaesqueClient.Producer do
330386 case Map . get ( state . callbacks , ref ) do
331387 nil -> :ok
332388
333- { :sync , from } ->
389+ { { :sync , from } , _timestamp } ->
334390 GenServer . reply ( from , { :ok , metadata } )
335391
336- callback when is_function ( callback , 1 ) ->
392+ { callback , _timestamp } when is_function ( callback , 1 ) ->
337393 Task . start ( fn -> callback . ( { :ok , metadata } ) end )
338394 end
339395 end )
@@ -345,15 +401,23 @@ defmodule KafkaesqueClient.Producer do
345401 case Map . get ( state . callbacks , ref ) do
346402 nil -> :ok
347403
348- { :sync , from } ->
404+ { { :sync , from } , _timestamp } ->
349405 GenServer . reply ( from , { :error , reason } )
350406
351- callback when is_function ( callback , 1 ) ->
407+ { callback , _timestamp } when is_function ( callback , 1 ) ->
352408 Task . start ( fn -> callback . ( { :error , reason } ) end )
353409 end
354410 end )
355411 end
356412
413+ defp execute_callback_with_error ( { :sync , from } , reason ) do
414+ GenServer . reply ( from , { :error , reason } )
415+ end
416+
417+ defp execute_callback_with_error ( callback , reason ) when is_function ( callback , 1 ) do
418+ Task . start ( fn -> callback . ( { :error , reason } ) end )
419+ end
420+
357421 defp map_acks ( :none ) , do: :ACKS_NONE
358422 defp map_acks ( :leader ) , do: :ACKS_LEADER
359423 defp map_acks ( :all ) , do: :ACKS_LEADER
0 commit comments