Skip to content

Commit d260d9d

Browse files
committed
Fix a race condition when launching the same backend concurrently from two clients.
Merging PR#880 introduced a regression bug in the rust backend manager. When two clients try to connect to the same backend and it hasn't been launched, the backend process might run twice. The CL solves the issue along with minor refactor to improve code readability.
1 parent 68c543f commit d260d9d

1 file changed

Lines changed: 41 additions & 36 deletions

File tree

PIMELauncher/src/backend_manager.rs

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::collections::HashMap;
22
use std::sync::atomic::{AtomicU64, Ordering};
3-
use std::sync::Arc;
3+
use std::sync::{Arc, Mutex};
44
use std::time::{Duration, SystemTime, UNIX_EPOCH};
55
use tokio::process::Command;
6-
use tokio::sync::{mpsc, Mutex};
6+
use tokio::sync::mpsc;
77
use tracing::{debug, error, info, warn};
88

99
use 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+
2635
struct 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

Comments
 (0)