Skip to content

Commit 5a148af

Browse files
author
developerworks
committed
Refactor shutdown coordinator with pluggable exit handler and Kani proof
- Add src/exit_handler.rs for pluggable child exit handling - Refactor shutdown pipeline stages for deterministic ordering - Add Kani verification model for shutdown coordinator correctness - Update examples (job, service, sidecar, supervisor, worker) for new API - Add check-yield-points.sh for yield point audit - Fix join timeout and shutdown orphan tests for new coordinator
1 parent d4b2c8d commit 5a148af

20 files changed

Lines changed: 763 additions & 29 deletions

clippy.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Disallow blocking I/O and synchronous locks in asynchronous contexts.
2+
# Subtask code must use tokio::fs, tokio::net, tokio::sync::Mutex,
3+
# and tokio::time::sleep instead.
4+
disallowed-methods = [
5+
"std::fs::read",
6+
"std::fs::write",
7+
"std::fs::File",
8+
"std::net::TcpStream",
9+
"std::thread::sleep",
10+
"std::sync::Mutex",
11+
]

examples/job/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ async fn main() -> ExampleResult {
3333
// Keep enough event buffer for the shutdown observation sequence.
3434
spec.event_channel_capacity = 32;
3535
// Use short shutdown windows so the cleanup path finishes quickly.
36-
let shutdown_policy =
37-
ShutdownPolicy::new(Duration::from_millis(250), Duration::from_millis(50), true);
36+
let shutdown_policy = ShutdownPolicy::new(
37+
Duration::from_millis(250),
38+
Duration::from_millis(50),
39+
true,
40+
Duration::from_millis(250),
41+
3,
42+
);
3843
// Start the runtime with the job child.
3944
let handle = Supervisor::start_with_policy(spec, shutdown_policy).await?;
4045
// Subscribe to lifecycle event text before commands are sent.

examples/service/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ async fn main() -> ExampleResult {
3636
// Keep enough event buffer for the full shutdown observation sequence.
3737
spec.event_channel_capacity = 32;
3838
// Use short shutdown windows so the example finishes quickly.
39-
let shutdown_policy =
40-
ShutdownPolicy::new(Duration::from_millis(250), Duration::from_millis(50), true);
39+
let shutdown_policy = ShutdownPolicy::new(
40+
Duration::from_millis(250),
41+
Duration::from_millis(50),
42+
true,
43+
Duration::from_millis(250),
44+
3,
45+
);
4146
// Start the runtime with the service child.
4247
let handle = Supervisor::start_with_policy(spec, shutdown_policy).await?;
4348
// Subscribe to lifecycle event text before commands are sent.

examples/shutdown_pipeline_demo.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ fn main() {
3232
Duration::from_secs(5), // graceful_timeout
3333
Duration::from_secs(1), // abort_wait
3434
true, // abort_after_timeout
35+
Duration::from_secs(5), // force_kill_margin
36+
3, // max_orphan_threshold
3537
);
3638

3739
println!(" graceful_timeout = {:?}", policy.graceful_timeout);
@@ -92,7 +94,13 @@ fn main() {
9294
println!("--- ShutdownCoordinator ---");
9395
println!();
9496

95-
let coord_policy = ShutdownPolicy::new(Duration::from_secs(5), Duration::from_secs(1), true);
97+
let coord_policy = ShutdownPolicy::new(
98+
Duration::from_secs(5),
99+
Duration::from_secs(1),
100+
true,
101+
Duration::from_secs(5),
102+
3,
103+
);
96104
let mut coordinator = ShutdownCoordinator::new(coord_policy);
97105

98106
let cause = ShutdownCause::new("operator", "scheduled maintenance");

examples/sidecar/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ async fn main() -> ExampleResult {
3737
// Keep enough event buffer for the full shutdown observation sequence.
3838
spec.event_channel_capacity = 64;
3939
// Use short shutdown windows so the example finishes quickly.
40-
let shutdown_policy =
41-
ShutdownPolicy::new(Duration::from_millis(250), Duration::from_millis(50), true);
40+
let shutdown_policy = ShutdownPolicy::new(
41+
Duration::from_millis(250),
42+
Duration::from_millis(50),
43+
true,
44+
Duration::from_millis(250),
45+
3,
46+
);
4247
// Start the runtime with the primary service and sidecar.
4348
let handle = Supervisor::start_with_policy(spec, shutdown_policy).await?;
4449
// Subscribe to lifecycle event text before commands are sent.

examples/supervisor/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ async fn main() -> ExampleResult {
3636
// Keep enough event buffer for the full shutdown observation sequence.
3737
spec.event_channel_capacity = 32;
3838
// Use short shutdown windows so the example finishes quickly.
39-
let shutdown_policy =
40-
ShutdownPolicy::new(Duration::from_millis(250), Duration::from_millis(50), true);
39+
let shutdown_policy = ShutdownPolicy::new(
40+
Duration::from_millis(250),
41+
Duration::from_millis(50),
42+
true,
43+
Duration::from_millis(250),
44+
3,
45+
);
4146
// Start the runtime with the supervisor role child.
4247
let handle = Supervisor::start_with_policy(spec, shutdown_policy).await?;
4348
// Subscribe to lifecycle event text before commands are sent.

examples/worker/main.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ async fn main() -> ExampleResult {
3333
// Keep enough event buffer for the shutdown observation sequence.
3434
spec.event_channel_capacity = 32;
3535
// Use short shutdown windows so the cleanup path finishes quickly.
36-
let shutdown_policy =
37-
ShutdownPolicy::new(Duration::from_millis(250), Duration::from_millis(50), true);
36+
let shutdown_policy = ShutdownPolicy::new(
37+
Duration::from_millis(250),
38+
Duration::from_millis(50),
39+
true,
40+
Duration::from_millis(250),
41+
3,
42+
);
3843
// Start the runtime with the worker child.
3944
let handle = Supervisor::start_with_policy(spec, shutdown_policy).await?;
4045
// Subscribe to lifecycle event text before commands are sent.

scripts/check-yield-points.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
# 检查 src/ 下 Rust 源码中 loop 块是否在 50 行内含有 yield 点
3+
# 非阻断告警, CI 中仅输出警告信息
4+
set -uo pipefail
5+
6+
src_dir="src"
7+
violations=0
8+
9+
while IFS= read -r file; do
10+
# 找到 loop { 的行号
11+
while IFS=: read -r line_no content; do
12+
# 检查接下来 50 行内是否有 yield_now() 或 .await
13+
if ! tail -n +"$line_no" "$file" | head -n 50 | grep -q 'yield_now\|\.await'; then
14+
echo "WARNING: $file:$line_no: loop without yield point in next 50 lines"
15+
((violations++))
16+
fi
17+
done < <(grep -n 'loop\s*{' "$file" || true)
18+
done < <(find "$src_dir" -name '*.rs' -type f)
19+
20+
if [ "$violations" -gt 0 ]; then
21+
echo "⚠️ Found $violations loop(s) without yield points (non-blocking)"
22+
fi

src/exit_handler.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//! Exit handler abstraction for testable process termination.
2+
//!
3+
//! The default implementation calls `std::process::exit(1)` as a last resort
4+
//! when orphaned tasks exceed the configured threshold. Tests can swap in a
5+
//! stub that records the exit request instead of terminating the process.
6+
7+
use std::sync::Arc;
8+
use std::sync::atomic::{AtomicBool, Ordering};
9+
10+
/// Abstraction for process exit, allowing tests to observe exit requests
11+
/// without actually terminating the process.
12+
pub trait ExitHandler: Send + Sync + std::fmt::Debug {
13+
/// Terminates the process (or records the request in test mode).
14+
fn exit(&self, code: i32);
15+
}
16+
17+
/// Default exit handler that calls `std::process::exit(code)`.
18+
#[derive(Debug, Clone, Copy)]
19+
pub struct DefaultExitHandler;
20+
21+
impl ExitHandler for DefaultExitHandler {
22+
fn exit(&self, code: i32) {
23+
std::process::exit(code);
24+
}
25+
}
26+
27+
/// Test-friendly exit handler that records a flag instead of terminating.
28+
#[derive(Debug, Clone)]
29+
pub struct TestExitHandler {
30+
/// Set to `true` when `exit()` was called.
31+
pub called: Arc<AtomicBool>,
32+
/// Captured exit code.
33+
pub exit_code: Arc<std::sync::Mutex<Option<i32>>>,
34+
}
35+
36+
impl TestExitHandler {
37+
/// Creates a new test exit handler with `called = false`.
38+
pub fn new() -> Self {
39+
Self {
40+
called: Arc::new(AtomicBool::new(false)),
41+
exit_code: Arc::new(std::sync::Mutex::new(None)),
42+
}
43+
}
44+
45+
/// Returns `true` when `exit()` was called since the last reset.
46+
pub fn was_called(&self) -> bool {
47+
self.called.load(Ordering::SeqCst)
48+
}
49+
50+
/// Returns the exit code if `exit()` was called.
51+
pub fn last_exit_code(&self) -> Option<i32> {
52+
*self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
53+
}
54+
55+
/// Resets the recorded state.
56+
pub fn reset(&self) {
57+
self.called.store(false, Ordering::SeqCst);
58+
*self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = None;
59+
}
60+
}
61+
62+
impl ExitHandler for TestExitHandler {
63+
fn exit(&self, code: i32) {
64+
self.called.store(true, Ordering::SeqCst);
65+
*self.exit_code.lock().unwrap_or_else(|e| e.into_inner()) = Some(code);
66+
// Do NOT call std::process::exit — let the test continue.
67+
}
68+
}
69+
70+
impl Default for TestExitHandler {
71+
fn default() -> Self {
72+
Self::new()
73+
}
74+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod control;
1111
pub mod dashboard;
1212
pub mod error;
1313
pub mod event;
14+
pub mod exit_handler;
1415
pub mod health;
1516
pub mod id;
1617
#[cfg(unix)]

0 commit comments

Comments
 (0)