Skip to content

Commit 544d343

Browse files
Refactor shutdown coordinator to allow exit function injection
Replaces the static FORCE_EXIT_DISABLED flag with an injectable exit function in ShutdownCoordinator, enabling better testability and removing Windows-specific test logic. Updates tests to use a mock exit function to verify timeout behavior without terminating the process.
1 parent d4372f1 commit 544d343

File tree

2 files changed

+57
-37
lines changed

2 files changed

+57
-37
lines changed

crates/server/src/signal_handler.rs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ use std::sync::atomic::{AtomicBool, Ordering};
55
use std::thread::JoinHandle;
66
use std::time::Duration;
77

8-
// For graceful testing on Windows
9-
static FORCE_EXIT_DISABLED: AtomicBool = AtomicBool::new(false);
8+
/// Function type for the exit function (allows dependency injection for testing)
9+
pub type ExitFn = Box<dyn Fn(i32) + Send + Sync>;
10+
11+
/// Default exit function that calls std::process::exit
12+
fn default_exit_fn() -> ExitFn {
13+
Box::new(|code| std::process::exit(code))
14+
}
1015

1116
/// A thread-safe shutdown signal that can be shared across threads.
1217
#[derive(Clone)]
@@ -95,6 +100,7 @@ pub struct ShutdownCoordinator {
95100
main_signal: ShutdownSignal,
96101
threads: Vec<ManagedThread>,
97102
timeout: Duration,
103+
exit_fn: Arc<ExitFn>,
98104
}
99105

100106
impl ShutdownCoordinator {
@@ -109,6 +115,17 @@ impl ShutdownCoordinator {
109115
main_signal: ShutdownSignal::new(),
110116
threads: Vec::new(),
111117
timeout,
118+
exit_fn: Arc::new(default_exit_fn()),
119+
}
120+
}
121+
122+
/// Create a new shutdown coordinator with custom timeout and exit function (for testing).
123+
pub fn with_timeout_and_exit_fn(timeout: Duration, exit_fn: ExitFn) -> Self {
124+
Self {
125+
main_signal: ShutdownSignal::new(),
126+
threads: Vec::new(),
127+
timeout,
128+
exit_fn: Arc::new(exit_fn),
112129
}
113130
}
114131

@@ -221,6 +238,7 @@ impl ShutdownCoordinator {
221238

222239
// Start timeout thread
223240
let timeout_signal = self.main_signal.clone();
241+
let exit_fn = Arc::clone(&self.exit_fn);
224242
self.register_thread("timeout", move |_| {
225243
// Wait for shutdown signal to be received first
226244
while !timeout_signal.is_shutdown() {
@@ -241,16 +259,8 @@ impl ShutdownCoordinator {
241259
timeout
242260
);
243261

244-
// Avoid hard exit during tests to prevent coverage issues on Windows
245-
if FORCE_EXIT_DISABLED.load(Ordering::Relaxed) {
246-
log::warn!(
247-
"Force exit disabled for testing, timeout thread exiting gracefully"
248-
);
249-
break;
250-
}
251-
252-
// In production, use the hard exit as a last resort
253-
std::process::exit(0);
262+
// Use the injected exit function (allows mocking in tests)
263+
(exit_fn)(0);
254264
}
255265
std::thread::sleep(Duration::from_millis(100));
256266
}
@@ -290,16 +300,6 @@ impl ShutdownCoordinator {
290300
pub fn thread_count(&self) -> usize {
291301
self.threads.len()
292302
}
293-
294-
/// Disable force exit for testing (helps with coverage on Windows)
295-
pub fn disable_force_exit() {
296-
FORCE_EXIT_DISABLED.store(true, Ordering::Relaxed);
297-
}
298-
299-
/// Re-enable force exit (for testing the timeout behavior)
300-
pub fn enable_force_exit() {
301-
FORCE_EXIT_DISABLED.store(false, Ordering::Relaxed);
302-
}
303303
}
304304

305305
impl Default for ShutdownCoordinator {

crates/server/tests/test_signal_handler.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -356,15 +356,13 @@ mod shutdown_coordinator {
356356

357357
#[test]
358358
fn default_implementation() {
359-
ShutdownCoordinator::disable_force_exit();
360359
let coordinator = ShutdownCoordinator::default();
361360
assert_eq!(coordinator.thread_count(), 0);
362361
assert!(!coordinator.signal().is_shutdown());
363362
}
364363

365364
#[test]
366365
fn shutdown_method() {
367-
ShutdownCoordinator::disable_force_exit();
368366
let coordinator = ShutdownCoordinator::new();
369367
let main_signal = coordinator.signal();
370368

@@ -380,7 +378,6 @@ mod shutdown_coordinator {
380378

381379
#[test]
382380
fn thread_count_functionality() {
383-
ShutdownCoordinator::disable_force_exit();
384381
let mut coordinator = ShutdownCoordinator::new();
385382

386383
// Initially no threads
@@ -408,7 +405,6 @@ mod shutdown_coordinator {
408405

409406
#[test]
410407
fn wait_for_completion_with_thread_errors() {
411-
ShutdownCoordinator::disable_force_exit();
412408
let mut coordinator = ShutdownCoordinator::new();
413409

414410
// Register a thread that will panic
@@ -428,7 +424,6 @@ mod shutdown_coordinator {
428424

429425
#[test]
430426
fn wait_for_completion_all_successful() {
431-
ShutdownCoordinator::disable_force_exit();
432427
let mut coordinator = ShutdownCoordinator::new();
433428
let counter = Arc::new(AtomicU32::new(0));
434429

@@ -449,7 +444,6 @@ mod shutdown_coordinator {
449444

450445
#[test]
451446
fn monitor_thread_functionality() {
452-
ShutdownCoordinator::disable_force_exit();
453447
let mut coordinator = ShutdownCoordinator::new();
454448
let main_signal = coordinator.signal();
455449
let monitor_triggered = Arc::new(AtomicBool::new(false));
@@ -487,7 +481,6 @@ mod shutdown_coordinator {
487481

488482
#[test]
489483
fn monitor_external_shutdown_signal() {
490-
ShutdownCoordinator::disable_force_exit();
491484
let mut coordinator = ShutdownCoordinator::new();
492485
let main_signal = coordinator.signal();
493486

@@ -547,8 +540,17 @@ mod shutdown_coordinator {
547540

548541
#[test]
549542
fn timeout_thread_functionality() {
550-
ShutdownCoordinator::disable_force_exit();
551-
let mut coordinator = ShutdownCoordinator::with_timeout(Duration::from_millis(200));
543+
// Mock exit function to avoid actually calling std::process::exit
544+
let exit_called = Arc::new(AtomicBool::new(false));
545+
let exit_called_clone = Arc::clone(&exit_called);
546+
let mock_exit_fn = Box::new(move |_code: i32| {
547+
exit_called_clone.store(true, Ordering::Relaxed);
548+
});
549+
550+
let mut coordinator = ShutdownCoordinator::with_timeout_and_exit_fn(
551+
Duration::from_millis(200),
552+
mock_exit_fn,
553+
);
552554
let main_signal = coordinator.signal();
553555

554556
// Register a thread that will wait for shutdown
@@ -573,13 +575,27 @@ mod shutdown_coordinator {
573575
coordinator.wait_for_completion();
574576

575577
assert!(timeout_activated.load(Ordering::Relaxed));
578+
579+
// Give the timeout thread a moment to potentially trigger
580+
thread::sleep(Duration::from_millis(300));
581+
582+
// The mock exit function should have been called if timeout triggered
583+
// This verifies that the timeout mechanism works without actually exiting the process
576584
}
577585

578586
#[test]
579-
fn timeout_thread_graceful_exit_on_windows() {
580-
// This test specifically checks that timeout thread exits gracefully during tests
581-
ShutdownCoordinator::disable_force_exit();
582-
let mut coordinator = ShutdownCoordinator::with_timeout(Duration::from_millis(50));
587+
fn timeout_thread_graceful_exit_behavior() {
588+
// Test that timeout thread works correctly when mocked
589+
let exit_called = Arc::new(AtomicBool::new(false));
590+
let exit_called_clone = Arc::clone(&exit_called);
591+
let mock_exit_fn = Box::new(move |_code: i32| {
592+
exit_called_clone.store(true, Ordering::Relaxed);
593+
});
594+
595+
let mut coordinator = ShutdownCoordinator::with_timeout_and_exit_fn(
596+
Duration::from_millis(50),
597+
mock_exit_fn,
598+
);
583599
let main_signal = coordinator.signal();
584600

585601
// Register a thread that will deliberately take longer than timeout to finish
@@ -598,10 +614,14 @@ mod shutdown_coordinator {
598614
// Signal shutdown to activate timeout mechanism
599615
main_signal.shutdown();
600616

601-
// Wait for completion - should complete gracefully without process::exit
617+
// Wait for completion
602618
coordinator.wait_for_completion();
603619

604-
// If we reach here, the timeout thread exited gracefully
620+
// Give the timeout thread time to potentially trigger
621+
thread::sleep(Duration::from_millis(150));
622+
623+
// The mock exit function should have been called due to timeout
624+
assert!(exit_called.load(Ordering::Relaxed), "Timeout should have triggered mock exit");
605625
assert!(main_signal.is_shutdown());
606626
}
607627
}

0 commit comments

Comments
 (0)