@@ -763,6 +763,88 @@ mod tests {
763763 // socket. That runtime is only dropped when the block_on future returns,
764764 // which requires the forwarding loop to exit.
765765 //
766+ // A following read has to notice interruption. Its iterator parks
767+ // waiting for the next frame on the topic, and on an idle topic that wait
768+ // has no end. http-nu gives each request its own `Signals` and triggers
769+ // them when the client disconnects, so without this the thread and the
770+ // store's follow task stay parked until some unrelated append happens to
771+ // land on that topic.
772+ #[ test]
773+ fn test_cat_follow_stops_when_interrupted ( ) {
774+ use crate :: nu:: commands;
775+ use nu_protocol:: Signals ;
776+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
777+ use std:: sync:: Arc ;
778+ use std:: time:: Duration ;
779+
780+ let ( store, mut engine) = setup_test_env ( ) ;
781+ engine
782+ . add_commands ( vec ! [ Box :: new( commands:: cat_command:: CatCommand :: new(
783+ store. clone( ) ,
784+ ) ) ] )
785+ . unwrap ( ) ;
786+
787+ let flag = Arc :: new ( AtomicBool :: new ( false ) ) ;
788+ engine. state . set_signals ( Signals :: new ( flag. clone ( ) ) ) ;
789+
790+ // Nothing is ever appended to this topic, so the only way out of the
791+ // follow is the signal.
792+ let ( done_tx, done_rx) = std:: sync:: mpsc:: channel ( ) ;
793+ let engine_clone = engine. clone ( ) ;
794+ std:: thread:: spawn ( move || {
795+ let out = engine_clone. eval ( PipelineData :: empty ( ) , ".cat -f -T idle" . to_string ( ) ) ;
796+ let drained = out. map ( |d| d. into_iter ( ) . count ( ) ) ;
797+ let _ = done_tx. send ( drained. is_ok ( ) ) ;
798+ } ) ;
799+
800+ // Let it reach the park before interrupting.
801+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
802+ flag. store ( true , Ordering :: SeqCst ) ;
803+
804+ let finished = done_rx. recv_timeout ( Duration :: from_secs ( 5 ) ) ;
805+ assert ! (
806+ finished. is_ok( ) ,
807+ "a following read ignored its interrupt and stayed parked" ,
808+ ) ;
809+ }
810+
811+ // `.last -f` has the same follow branch and the same wait.
812+ #[ test]
813+ fn test_last_follow_stops_when_interrupted ( ) {
814+ use crate :: nu:: commands;
815+ use nu_protocol:: Signals ;
816+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
817+ use std:: sync:: Arc ;
818+ use std:: time:: Duration ;
819+
820+ let ( store, mut engine) = setup_test_env ( ) ;
821+ engine
822+ . add_commands ( vec ! [ Box :: new( commands:: last_command:: LastCommand :: new(
823+ store. clone( ) ,
824+ ) ) ] )
825+ . unwrap ( ) ;
826+
827+ let flag = Arc :: new ( AtomicBool :: new ( false ) ) ;
828+ engine. state . set_signals ( Signals :: new ( flag. clone ( ) ) ) ;
829+
830+ let ( done_tx, done_rx) = std:: sync:: mpsc:: channel ( ) ;
831+ let engine_clone = engine. clone ( ) ;
832+ std:: thread:: spawn ( move || {
833+ let out = engine_clone. eval ( PipelineData :: empty ( ) , ".last idle -f" . to_string ( ) ) ;
834+ // Assert on the count, not just that the thread ended: a bad
835+ // invocation would error out at once and pass vacuously.
836+ let _ = done_tx. send ( out. map ( |d| d. into_iter ( ) . count ( ) ) ) ;
837+ } ) ;
838+
839+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
840+ flag. store ( true , Ordering :: SeqCst ) ;
841+
842+ let finished = done_rx
843+ . recv_timeout ( Duration :: from_secs ( 5 ) )
844+ . expect ( "a following read ignored its interrupt and stayed parked" ) ;
845+ assert ! ( finished. is_ok( ) , ".last -f failed to run: {finished:?}" ) ;
846+ }
847+
766848 // In `--follow` mode the store's read() future parks on the broadcast
767849 // receiver waiting for the next append. When the ListStream consumer is
768850 // dropped (client disconnects, pipeline ends early), the spawned thread is
0 commit comments