@@ -16,8 +16,11 @@ use cli::KeypairType;
1616pub use config:: Config ;
1717pub use constants:: UDP_BUFFER_SIZE ;
1818
19- use anyhow:: Result ;
20- use tokio:: sync:: { broadcast, mpsc} ;
19+ use anyhow:: { anyhow, Context , Result } ;
20+ use tokio:: {
21+ sync:: { broadcast, mpsc} ,
22+ task:: { JoinError , JoinSet } ,
23+ } ;
2124use tracing:: { debug, info} ;
2225
2326#[ cfg( feature = "client" ) ]
@@ -78,44 +81,98 @@ pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> Result<()
7881 // shutdown_tx owns the instance
7982 let ( shutdown_tx, _) = broadcast:: channel ( 1 ) ;
8083
81- // (The join handle of the last instance, The service update channel sender)
82- let mut last_instance: Option < ( tokio:: task:: JoinHandle < _ > , mpsc:: Sender < ConfigChange > ) > = None ;
83-
84- while let Some ( e) = cfg_watcher. event_rx . recv ( ) . await {
85- match e {
86- ConfigChange :: General ( config) => {
87- if let Some ( ( i, _) ) = last_instance {
88- info ! ( "General configuration change detected. Restarting..." ) ;
89- shutdown_tx. send ( true ) ?;
90- i. await ??;
84+ // Exactly one instance can be active. JoinSet lets the outer loop observe
85+ // an instance failure without waiting for another configuration event.
86+ let mut instance_tasks = JoinSet :: new ( ) ;
87+ let mut service_update_tx = None ;
88+
89+ loop {
90+ tokio:: select! {
91+ event = cfg_watcher. event_rx. recv( ) => {
92+ match event {
93+ Some ( ConfigChange :: General ( config) ) => {
94+ if !instance_tasks. is_empty( ) {
95+ info!( "General configuration change detected. Restarting..." ) ;
96+ stop_active_instance( & shutdown_tx, & mut instance_tasks) . await ?;
97+ }
98+
99+ debug!( "{:?}" , config) ;
100+
101+ let ( update_tx, update_rx) = mpsc:: channel( 1024 ) ;
102+ instance_tasks. spawn( run_instance(
103+ * config,
104+ args. clone( ) ,
105+ shutdown_tx. subscribe( ) ,
106+ update_rx,
107+ ) ) ;
108+ service_update_tx = Some ( update_tx) ;
109+ }
110+ Some ( event) => {
111+ info!( "Service change detected. {:?}" , event) ;
112+ if let Some ( update_tx) = & service_update_tx {
113+ let _ = update_tx. send( event) . await ;
114+ }
115+ }
116+ None => {
117+ let watcher_result = cfg_watcher. wait( ) . await ;
118+ let instance_result =
119+ stop_active_instance( & shutdown_tx, & mut instance_tasks) . await ;
120+ return finish_shutdown( watcher_result, instance_result) ;
121+ }
91122 }
92-
93- debug ! ( "{:?}" , config) ;
94-
95- let ( service_update_tx, service_update_rx) = mpsc:: channel ( 1024 ) ;
96-
97- last_instance = Some ( (
98- tokio:: spawn ( run_instance (
99- * config,
100- args. clone ( ) ,
101- shutdown_tx. subscribe ( ) ,
102- service_update_rx,
103- ) ) ,
104- service_update_tx,
105- ) ) ;
106123 }
107- ev => {
108- info ! ( "Service change detected. {:?}" , ev) ;
109- if let Some ( ( _, service_update_tx) ) = & last_instance {
110- let _ = service_update_tx. send ( ev) . await ;
111- }
124+ instance_result = instance_tasks. join_next( ) , if !instance_tasks. is_empty( ) => {
125+ return unexpected_instance_exit(
126+ instance_result. ok_or_else( || anyhow!( "active instance task disappeared" ) ) ?,
127+ ) ;
112128 }
113129 }
114130 }
131+ }
115132
116- let _ = shutdown_tx. send ( true ) ;
133+ fn finish_shutdown ( watcher_result : Result < ( ) > , instance_result : Result < ( ) > ) -> Result < ( ) > {
134+ match ( watcher_result, instance_result) {
135+ ( Ok ( ( ) ) , result) => result,
136+ ( Err ( watcher_error) , Ok ( ( ) ) ) => Err ( watcher_error) . context ( "configuration watcher failed" ) ,
137+ ( Err ( watcher_error) , Err ( instance_error) ) => Err ( instance_error) . context ( format ! (
138+ "configuration watcher also failed: {watcher_error:#}"
139+ ) ) ,
140+ }
141+ }
117142
118- Ok ( ( ) )
143+ async fn stop_active_instance (
144+ shutdown_tx : & broadcast:: Sender < bool > ,
145+ instance_tasks : & mut JoinSet < Result < ( ) > > ,
146+ ) -> Result < ( ) > {
147+ if instance_tasks. is_empty ( ) {
148+ return Ok ( ( ) ) ;
149+ }
150+
151+ // A failed instance may already have dropped its receiver. Awaiting the
152+ // task below preserves that failure instead of returning "channel closed".
153+ let shutdown_sent = shutdown_tx. send ( true ) . is_ok ( ) ;
154+ let instance_result = instance_tasks
155+ . join_next ( )
156+ . await
157+ . ok_or_else ( || anyhow ! ( "active instance task disappeared while shutting down" ) ) ?
158+ . context ( "active instance task panicked" ) ?;
159+
160+ match instance_result {
161+ Ok ( ( ) ) if shutdown_sent => Ok ( ( ) ) ,
162+ Ok ( ( ) ) => Err ( anyhow ! (
163+ "active instance exited before receiving the shutdown signal"
164+ ) ) ,
165+ Err ( error) => Err ( error) . context ( "active instance failed while shutting down" ) ,
166+ }
167+ }
168+
169+ fn unexpected_instance_exit (
170+ instance_result : std:: result:: Result < Result < ( ) > , JoinError > ,
171+ ) -> Result < ( ) > {
172+ match instance_result. context ( "active instance task panicked" ) ? {
173+ Ok ( ( ) ) => Err ( anyhow ! ( "active instance exited unexpectedly" ) ) ,
174+ Err ( error) => Err ( error) . context ( "active instance exited unexpectedly" ) ,
175+ }
119176}
120177
121178async fn run_instance (
@@ -169,6 +226,35 @@ fn determine_run_mode(config: &Config, args: &Cli) -> RunMode {
169226mod tests {
170227 use super :: * ;
171228
229+ #[ cfg( feature = "server" ) ]
230+ fn server_args ( bind_addr : std:: net:: SocketAddr ) -> ( tempfile:: TempDir , Cli ) {
231+ let config_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
232+ let config_path = config_dir. path ( ) . join ( "server.toml" ) ;
233+ std:: fs:: write (
234+ & config_path,
235+ format ! (
236+ r#"[server]
237+ bind_addr = "{bind_addr}"
238+
239+ [server.transport]
240+ type = "tcp"
241+
242+ [server.services.test]
243+ bind_addr = "127.0.0.1:0"
244+ token = "test-token"
245+ "# ,
246+ ) ,
247+ )
248+ . unwrap ( ) ;
249+
250+ let args = Cli {
251+ config_path : Some ( config_path) ,
252+ server : true ,
253+ ..Default :: default ( )
254+ } ;
255+ ( config_dir, args)
256+ }
257+
172258 #[ test]
173259 fn test_determine_run_mode ( ) {
174260 use config:: * ;
@@ -256,4 +342,147 @@ mod tests {
256342 assert_eq ! ( determine_run_mode( & config, & args) , t. run_mode) ;
257343 }
258344 }
345+
346+ #[ cfg( feature = "server" ) ]
347+ #[ tokio:: test]
348+ async fn run_surfaces_instance_startup_failure ( ) {
349+ use std:: time:: Duration ;
350+ use tokio:: { net:: TcpListener , time} ;
351+
352+ let occupied_listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
353+ let occupied_addr = occupied_listener. local_addr ( ) . unwrap ( ) ;
354+ let ( _shutdown_tx, shutdown_rx) = broadcast:: channel ( 1 ) ;
355+ let ( _config_dir, args) = server_args ( occupied_addr) ;
356+
357+ let result = time:: timeout ( Duration :: from_secs ( 5 ) , run ( args, shutdown_rx) )
358+ . await
359+ . expect ( "startup failure should be reported promptly" ) ;
360+ let error = format ! (
361+ "{:#}" ,
362+ result. expect_err( "an occupied listener must fail startup" )
363+ ) ;
364+ assert ! (
365+ error. contains( "active instance exited unexpectedly" ) ,
366+ "{error}"
367+ ) ;
368+ assert ! (
369+ error. contains( "Failed to listen at `server.bind_addr`" ) ,
370+ "{error}"
371+ ) ;
372+ }
373+
374+ #[ cfg( feature = "server" ) ]
375+ #[ tokio:: test]
376+ async fn run_releases_listener_before_shutdown_returns ( ) {
377+ use std:: time:: Duration ;
378+ use tokio:: { net:: TcpListener , net:: TcpStream , time} ;
379+
380+ let reservation = TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
381+ let bind_addr = reservation. local_addr ( ) . unwrap ( ) ;
382+ drop ( reservation) ;
383+
384+ let ( _config_dir, args) = server_args ( bind_addr) ;
385+ let ( shutdown_tx, shutdown_rx) = broadcast:: channel ( 1 ) ;
386+ let run_task = tokio:: spawn ( run ( args, shutdown_rx) ) ;
387+
388+ time:: timeout ( Duration :: from_secs ( 5 ) , async {
389+ loop {
390+ if let Ok ( stream) = TcpStream :: connect ( bind_addr) . await {
391+ drop ( stream) ;
392+ break ;
393+ }
394+ time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
395+ }
396+ } )
397+ . await
398+ . expect ( "server listener should start promptly" ) ;
399+
400+ shutdown_tx. send ( true ) . unwrap ( ) ;
401+ time:: timeout ( Duration :: from_secs ( 5 ) , run_task)
402+ . await
403+ . expect ( "shutdown should complete promptly" )
404+ . expect ( "rathole task should not panic" )
405+ . expect ( "rathole should shut down cleanly" ) ;
406+
407+ TcpListener :: bind ( bind_addr)
408+ . await
409+ . expect ( "listener must be released before shutdown returns" ) ;
410+ }
411+
412+ #[ tokio:: test]
413+ async fn shutdown_waits_for_the_active_instance ( ) {
414+ use std:: time:: Duration ;
415+ use tokio:: { sync:: oneshot, time} ;
416+
417+ let ( shutdown_tx, mut shutdown_rx) = broadcast:: channel ( 1 ) ;
418+ let ( finish_tx, finish_rx) = oneshot:: channel ( ) ;
419+ let mut instance_tasks = JoinSet :: new ( ) ;
420+ instance_tasks. spawn ( async move {
421+ shutdown_rx. recv ( ) . await . unwrap ( ) ;
422+ finish_rx. await . unwrap ( ) ;
423+ Ok ( ( ) )
424+ } ) ;
425+
426+ let mut stopping = Box :: pin ( stop_active_instance ( & shutdown_tx, & mut instance_tasks) ) ;
427+ assert ! (
428+ time:: timeout( Duration :: from_millis( 25 ) , & mut stopping)
429+ . await
430+ . is_err( ) ,
431+ "shutdown returned before the active instance finished"
432+ ) ;
433+
434+ finish_tx. send ( ( ) ) . unwrap ( ) ;
435+ time:: timeout ( Duration :: from_secs ( 1 ) , stopping)
436+ . await
437+ . expect ( "shutdown should finish promptly after the instance exits" )
438+ . unwrap ( ) ;
439+ }
440+
441+ #[ tokio:: test]
442+ async fn restart_does_not_overlap_instances ( ) {
443+ use std:: time:: Duration ;
444+ use tokio:: { sync:: oneshot, time} ;
445+
446+ let ( shutdown_tx, mut shutdown_rx) = broadcast:: channel ( 1 ) ;
447+ let ( finish_tx, finish_rx) = oneshot:: channel ( ) ;
448+ let ( replacement_started_tx, mut replacement_started_rx) = oneshot:: channel ( ) ;
449+ let mut instance_tasks = JoinSet :: new ( ) ;
450+ instance_tasks. spawn ( async move {
451+ shutdown_rx. recv ( ) . await . unwrap ( ) ;
452+ finish_rx. await . unwrap ( ) ;
453+ Ok ( ( ) )
454+ } ) ;
455+
456+ let mut restarting = Box :: pin ( async {
457+ stop_active_instance ( & shutdown_tx, & mut instance_tasks) . await ?;
458+ instance_tasks. spawn ( async move {
459+ replacement_started_tx. send ( ( ) ) . unwrap ( ) ;
460+ Ok ( ( ) )
461+ } ) ;
462+ Result :: < ( ) > :: Ok ( ( ) )
463+ } ) ;
464+
465+ assert ! (
466+ time:: timeout( Duration :: from_millis( 25 ) , & mut restarting)
467+ . await
468+ . is_err( ) ,
469+ "restart completed before the old instance finished"
470+ ) ;
471+ assert ! (
472+ matches!(
473+ replacement_started_rx. try_recv( ) ,
474+ Err ( oneshot:: error:: TryRecvError :: Empty )
475+ ) ,
476+ "replacement started while the old instance was active"
477+ ) ;
478+
479+ finish_tx. send ( ( ) ) . unwrap ( ) ;
480+ time:: timeout ( Duration :: from_secs ( 1 ) , restarting)
481+ . await
482+ . expect ( "restart should continue after the old instance exits" )
483+ . unwrap ( ) ;
484+ replacement_started_rx
485+ . await
486+ . expect ( "replacement instance should start" ) ;
487+ }
259488}
0 commit comments