11use std:: collections:: HashMap ;
22use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
3- use std:: sync:: Arc ;
3+ use std:: sync:: { Arc , Mutex } ;
44use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
55use tokio:: process:: Command ;
6- use tokio:: sync:: { mpsc, Mutex } ;
6+ use tokio:: sync:: mpsc;
77use tracing:: { debug, error, info, warn} ;
88
99use crate :: backend_registry:: { BackendConfig , BackendRegistry } ;
@@ -23,6 +23,15 @@ struct BackendManagerState {
2323 clients : HashMap < String , mpsc:: Sender < String > > ,
2424}
2525
26+ /// The reason why the backend process was terminated.
27+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
28+ enum BackendExitReason {
29+ /// Normal shutdown. The input channel was closed because the launcher is shutting down.
30+ Normal ,
31+ /// An error occurred (e.g., process crashed, write timeout, watchdog timeout). The process should be restarted.
32+ Error ,
33+ }
34+
2635struct BackendProcess {
2736 stdin_tx : mpsc:: Sender < String > ,
2837}
@@ -54,7 +63,7 @@ impl BackendManager {
5463
5564 /// Registers a client with its response channel.
5665 pub async fn register_client ( & self , client_id : String ) -> mpsc:: Receiver < String > {
57- let mut state = self . state . lock ( ) . await ;
66+ let mut state = self . state . lock ( ) . unwrap ( ) ;
5867 let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < String > ( 1024 ) ;
5968 state. clients . insert ( client_id, tx) ;
6069 rx
@@ -66,38 +75,28 @@ impl BackendManager {
6675 self . send_to_backend ( backend_name, client_id, r#"{"method":"close"}"# )
6776 . await ;
6877
69- let mut state = self . state . lock ( ) . await ;
78+ let mut state = self . state . lock ( ) . unwrap ( ) ;
7079 state. clients . remove ( client_id) ;
7180 }
7281
7382 /// Retrieves a channel to send messages directly to the backend.
7483 pub async fn get_backend_input ( & self , backend_name : & str ) -> Option < mpsc:: Sender < String > > {
75- // Fast path: backend already running — take and release the lock immediately.
76- {
77- let state = self . state . lock ( ) . await ;
78- if let Some ( b) = state. backends . get ( backend_name) {
79- return Some ( b. stdin_tx . clone ( ) ) ;
80- }
84+ let mut state = self . state . lock ( ) . unwrap ( ) ;
85+ if let Some ( b) = state. backends . get ( backend_name) {
86+ return Some ( b. stdin_tx . clone ( ) ) ;
8187 }
8288
83- // Slow path: spawn the backend without holding the lock, so other clients
84- // are not blocked during the (potentially multi-second) process startup.
8589 let config = match self . registry . get_backend ( backend_name) {
8690 Some ( c) => c. clone ( ) ,
8791 None => {
8892 error ! ( "Unknown backend requested: {}" , backend_name) ;
8993 return None ;
9094 }
9195 } ;
92- let backend = self . spawn_backend_process ( & config) . await ;
93-
94- // Re-acquire lock to insert; use entry() so a concurrent spawn doesn't overwrite.
95- let mut state = self . state . lock ( ) . await ;
96- state
97- . backends
98- . entry ( backend_name. to_string ( ) )
99- . or_insert ( backend) ;
100- state. backends . get ( backend_name) . map ( |b| b. stdin_tx . clone ( ) )
96+ let backend = self . spawn_backend_process ( & config) ;
97+ let stdin_tx = backend. stdin_tx . clone ( ) ;
98+ state. backends . insert ( backend_name. to_string ( ) , backend) ;
99+ Some ( stdin_tx)
101100 }
102101
103102 /// Sends a message to a specific backend, spawning it if necessary.
@@ -117,15 +116,15 @@ impl BackendManager {
117116 "Failed to send to internal channel for backend {}: {}" ,
118117 backend_name, e
119118 ) ;
120- // The backend task seems to be dead.
121- let mut state = self . state . lock ( ) . await ;
122- state . backends . remove ( backend_name ) ;
119+ // The backend task seems to be dead. The background loop will auto-recover it.
120+ // The backend_stdin is an MPSC channel, not the stdin of the crashed process,
121+ // so it will remain valid after the backend crashes.
123122 }
124123 }
125124 }
126125
127126 /// Spawns a backend process and maintains its lifetime in a background task.
128- async fn spawn_backend_process ( & self , config : & BackendConfig ) -> BackendProcess {
127+ fn spawn_backend_process ( & self , config : & BackendConfig ) -> BackendProcess {
129128 let backend_name = config. name . clone ( ) ;
130129 let ( stdin_tx, mut stdin_rx) = mpsc:: channel :: < String > ( 1024 ) ;
131130 let backend_name_clone = backend_name. to_string ( ) ;
@@ -165,7 +164,7 @@ impl BackendManager {
165164 Self :: log_backend_stderr ( stderr, backend_name_for_stderr) . await ;
166165 } ) ;
167166
168- Self :: forward_inputs_to_backend (
167+ let exit_reason = Self :: forward_inputs_to_backend (
169168 & mut stdin_rx, // Inputs received from client connections.
170169 stdin, // Backend stdin.
171170 & mut child_process, // Backend process.
@@ -176,6 +175,12 @@ impl BackendManager {
176175
177176 stdout_task. abort ( ) ;
178177 stderr_task. abort ( ) ;
178+
179+ if exit_reason == BackendExitReason :: Normal {
180+ info ! ( "Backend {} input channel closed permanently. Stopping manager loop." , backend_name_clone) ;
181+ break ; // Exit the loop entirely to prevent infinite restarts!
182+ }
183+
179184 warn ! ( "Restarting backend {}" , backend_name_clone) ;
180185 tokio:: time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
181186 }
@@ -263,7 +268,7 @@ impl BackendManager {
263268 debug ! ( "Routing to client {}: {:?}" , client_id, payload) ;
264269
265270 let tx = {
266- let state = self . state . lock ( ) . await ;
271+ let state = self . state . lock ( ) . unwrap ( ) ;
267272 state. clients . get ( & client_id) . cloned ( )
268273 } ;
269274
@@ -275,7 +280,7 @@ impl BackendManager {
275280 tokio:: time:: timeout ( Duration :: from_millis ( 500 ) , tx. send ( payload) ) . await
276281 {
277282 warn ! ( "Client {} buffer full for 500ms. Force disconnecting to prevent backend stall." , client_id) ;
278- let mut state = self . state . lock ( ) . await ;
283+ let mut state = self . state . lock ( ) . unwrap ( ) ;
279284 state. clients . remove ( & client_id) ;
280285 }
281286 } else {
@@ -295,7 +300,7 @@ impl BackendManager {
295300 child_process : & mut tokio:: process:: Child ,
296301 backend_name : & str ,
297302 last_output_time : Arc < AtomicU64 > ,
298- ) {
303+ ) -> BackendExitReason {
299304 let mut stdin_writer = FramedWrite :: new ( stdin, LinesCodec :: new_with_max_length ( 1048576 ) ) ;
300305 let mut last_request_time: Option < u64 > = None ;
301306
@@ -306,7 +311,7 @@ impl BackendManager {
306311 msg = stdin_rx. recv( ) => {
307312 let Some ( data) = msg else {
308313 info!( "Backend {} stdin channel closed. Exiting input loop." , backend_name) ;
309- break ;
314+ return BackendExitReason :: Normal ;
310315 } ;
311316 let now = Self :: current_ms( ) ;
312317 last_request_time = Some ( now) ;
@@ -317,12 +322,12 @@ impl BackendManager {
317322 if let Err ( _) = write_res {
318323 error!( "Timeout writing to backend {}. Forcing restart." , backend_name) ;
319324 let _ = child_process. kill( ) . await ;
320- break ;
325+ return BackendExitReason :: Error ;
321326 }
322327 if let Err ( e) = write_res. unwrap( ) {
323328 error!( "Failed to write to backend {}: {}" , backend_name, e) ;
324329 let _ = child_process. kill( ) . await ;
325- break ;
330+ return BackendExitReason :: Error ;
326331 }
327332 }
328333 _ = watchdog_interval. tick( ) => {
@@ -338,13 +343,13 @@ impl BackendManager {
338343 error!( "Backend {} seems to be hung (no output for 15s after request). last_out={}, req_t={}, now={}. Forcing restart." ,
339344 backend_name, last_out, req_t, now) ;
340345 let _ = child_process. kill( ) . await ;
341- break ;
346+ return BackendExitReason :: Error ;
342347 }
343348 }
344349 }
345350 status = child_process. wait( ) => {
346351 warn!( "Backend {} exited with status {:?}" , backend_name, status) ;
347- break ;
352+ return BackendExitReason :: Error ;
348353 }
349354 }
350355 }
@@ -371,13 +376,13 @@ mod tests {
371376 let _ = manager. register_client ( "client1" . to_string ( ) ) . await ;
372377
373378 {
374- let state = manager. state . lock ( ) . await ;
379+ let state = manager. state . lock ( ) . unwrap ( ) ;
375380 assert ! ( state. clients. contains_key( "client1" ) ) ;
376381 }
377382
378383 manager. unregister_client ( "client1" , "dummy" ) . await ;
379384 {
380- let state = manager. state . lock ( ) . await ;
385+ let state = manager. state . lock ( ) . unwrap ( ) ;
381386 assert ! ( !state. clients. contains_key( "client1" ) ) ;
382387 }
383388 }
0 commit comments