@@ -481,7 +481,12 @@ async fn run_worker(
481481 _ = sleep. fuse( ) => {
482482 // eprintln!("<worker> sleep");
483483 if keep_alive && is_open {
484- message_tx. send( WsMessage :: ControlMessage ( ControlMessage :: KeepAlive ) ) . await . expect( "we hold the receiver, so we know it hasn't been dropped" ) ;
484+ // Ignore send errors: the channel may have been closed by
485+ // close_stream() (via close_channel()) before the worker
486+ // processes the pending CloseStream message. In that case
487+ // the next iteration will handle CloseStream, stop sending new
488+ // messages, and proceed toward shutdown.
489+ let _ = message_tx. send( WsMessage :: ControlMessage ( ControlMessage :: KeepAlive ) ) . await ;
485490 last_sent_message = tokio:: time:: Instant :: now( ) ;
486491 } else {
487492 pending:: <( ) >( ) . await ;
@@ -872,8 +877,10 @@ mod file_chunker {
872877
873878#[ cfg( test) ]
874879mod tests {
880+ use std:: time:: Duration ;
881+
875882 use super :: ControlMessage ;
876- use crate :: common:: options:: Options ;
883+ use crate :: common:: options:: { Encoding , Endpointing , Options } ;
877884
878885 #[ test]
879886 fn test_stream_url ( ) {
@@ -910,4 +917,78 @@ mod tests {
910917 r#"{"type":"CloseStream"}"#
911918 ) ;
912919 }
920+
921+ /// Reproduces the worker panic from issue #143: close_stream() calls
922+ /// close_channel(), so when the worker's keep-alive sleep fires it sends
923+ /// into a closed channel. Before the fix, .expect() would panic.
924+ #[ tokio:: test]
925+ #[ ignore = "requires DEEPGRAM_API_KEY and network; run manually" ]
926+ async fn keepalive_then_close_stream_panic_repro ( ) {
927+ let Ok ( api_key) = std:: env:: var ( "DEEPGRAM_API_KEY" ) else {
928+ eprintln ! ( "skipping: DEEPGRAM_API_KEY not set" ) ;
929+ return ;
930+ } ;
931+
932+ let dg = crate :: Deepgram :: new ( & api_key) . expect ( "Deepgram::new" ) ;
933+ let transcription = dg. transcription ( ) ;
934+
935+ let options = Options :: builder ( )
936+ . query_params ( [ ( "mip_opt_out" . to_string ( ) , "true" . to_string ( ) ) ] )
937+ . build ( ) ;
938+ let mut handle = transcription
939+ . stream_request_with_options ( options)
940+ . encoding ( Encoding :: Linear16 )
941+ . endpointing ( Endpointing :: Disabled )
942+ . keep_alive ( )
943+ . handle ( )
944+ . await
945+ . expect ( "handle" ) ;
946+
947+ // No audio sent: worker only has the keep-alive timer (3s interval).
948+ // Close the channel before the keep-alive fires so the
949+ // send(KeepAlive) hits a closed channel.
950+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
951+ let _ = handle. close_stream ( ) . await ;
952+
953+ // Wait longer than the 3s keep-alive interval so the keep-alive
954+ // send path runs after close. Before the fix this would panic.
955+ tokio:: time:: sleep ( Duration :: from_secs ( 4 ) ) . await ;
956+ }
957+
958+ /// Runs the close_stream race in a loop to increase the probability of
959+ /// hitting the timing window (scheduling variance).
960+ #[ tokio:: test]
961+ #[ ignore = "requires DEEPGRAM_API_KEY and network; run manually" ]
962+ async fn keepalive_close_stream_panic_repro_loop ( ) {
963+ let Ok ( api_key) = std:: env:: var ( "DEEPGRAM_API_KEY" ) else {
964+ eprintln ! ( "skipping: DEEPGRAM_API_KEY not set" ) ;
965+ return ;
966+ } ;
967+
968+ const ITERATIONS : u32 = 3 ;
969+
970+ let options = Options :: builder ( )
971+ . query_params ( [ ( "mip_opt_out" . to_string ( ) , "true" . to_string ( ) ) ] )
972+ . build ( ) ;
973+ let dg = crate :: Deepgram :: new ( & api_key) . expect ( "Deepgram::new" ) ;
974+ let transcription = dg. transcription ( ) ;
975+
976+ for _iteration in 0 ..ITERATIONS {
977+ let mut handle = transcription
978+ . stream_request_with_options ( options. clone ( ) )
979+ . encoding ( Encoding :: Linear16 )
980+ . endpointing ( Endpointing :: Disabled )
981+ . keep_alive ( )
982+ . handle ( )
983+ . await
984+ . expect ( "handle" ) ;
985+
986+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
987+ let _ = handle. close_stream ( ) . await ;
988+
989+ // Wait longer than the 3s keep-alive interval so the keep-alive
990+ // send path runs after close. Before the fix this would panic.
991+ tokio:: time:: sleep ( Duration :: from_secs ( 4 ) ) . await ;
992+ }
993+ }
913994}
0 commit comments