rust_supervisor/child_runner/runner.rs
1//! Minimal child runner.
2//!
3//! This module starts one child child_start_count, advances readiness state, and records
4//! the resulting task exit.
5
6use crate::child_runner::run_exit::TaskExit;
7use crate::error::types::SupervisorError;
8use crate::readiness::signal::{ReadinessPolicy, ReadinessState, ReadySignal};
9use crate::registry::entry::{ChildRuntime, ChildRuntimeStatus};
10use crate::task::context::TaskContext;
11use tokio::sync::{watch, watch::Receiver};
12use tokio::task::{AbortHandle, JoinHandle};
13use tokio::time::Instant;
14use tokio_util::sync::CancellationToken;
15
16/// Result of running one child child_start_count.
17#[derive(Debug, Clone)]
18pub struct ChildRunReport {
19 /// Runtime record after the child_start_count.
20 pub runtime: ChildRuntime,
21 /// Final task exit classification.
22 pub exit: TaskExit,
23 /// Whether the task became ready during the child_start_count.
24 pub became_ready: bool,
25}
26
27/// Handle for one running child child_start_count.
28#[derive(Debug)]
29pub struct ChildRunHandle {
30 /// Runtime cancellation token shared with the task context.
31 pub cancellation_token: CancellationToken,
32 /// Abort handle attached to the real child future.
33 pub abort_handle: AbortHandle,
34 /// Receiver that observes the completed child run report.
35 pub completion_receiver: Receiver<Option<Result<ChildRunReport, SupervisorError>>>,
36 /// Receiver that observes the latest child heartbeat.
37 pub heartbeat_receiver: watch::Receiver<Option<Instant>>,
38 /// Receiver that observes the latest child readiness state.
39 pub readiness_receiver: watch::Receiver<ReadinessState>,
40}
41
42/// Runner that executes one child child_start_count.
43#[derive(Debug, Clone, Default)]
44pub struct ChildRunner;
45
46impl ChildRunner {
47 /// Creates a child runner.
48 ///
49 /// # Arguments
50 ///
51 /// This function has no arguments.
52 ///
53 /// # Returns
54 ///
55 /// Returns a [`ChildRunner`].
56 ///
57 /// # Examples
58 ///
59 /// ```
60 /// let _runner = rust_supervisor::child_runner::runner::ChildRunner::new();
61 /// ```
62 pub fn new() -> Self {
63 Self
64 }
65
66 /// Runs one child child_start_count.
67 ///
68 /// # Arguments
69 ///
70 /// - `runtime`: Runtime record for the child task.
71 ///
72 /// # Returns
73 ///
74 /// Returns a [`ChildRunReport`] when the child owns a task factory.
75 pub async fn run_once(&self, runtime: ChildRuntime) -> Result<ChildRunReport, SupervisorError> {
76 let mut completion_receiver = self.spawn_once(runtime)?.completion_receiver;
77 wait_for_report(&mut completion_receiver).await
78 }
79
80 /// Spawns one child task and returns cancellation and abort handles.
81 ///
82 /// # Arguments
83 ///
84 /// - `runtime`: Runtime record for the child task.
85 ///
86 /// # Returns
87 ///
88 /// Returns a [`ChildRunHandle`] when the child owns a task factory.
89 pub fn spawn_once(&self, mut runtime: ChildRuntime) -> Result<ChildRunHandle, SupervisorError> {
90 #[cfg(any(debug_assertions, feature = "test-support"))]
91 #[cfg(any(test, feature = "test-support"))]
92 if crate::test_support::child_spawn::take_child_spawn_failure_attempt(&runtime.id) {
93 return Err(SupervisorError::InvalidTransition {
94 message: "test hook: child spawn_once failure".to_owned(),
95 });
96 }
97 let factory =
98 runtime.spec.factory.clone().ok_or_else(|| {
99 SupervisorError::fatal_config("worker child requires a task factory")
100 })?;
101 runtime.status = ChildRuntimeStatus::Starting;
102 let (ready_signal, ready_receiver) = ReadySignal::new();
103 let cancellation_token = CancellationToken::new();
104 let (ctx, heartbeat_receiver) = TaskContext::with_ready_signal_and_cancellation_token(
105 runtime.id.clone(),
106 runtime.path.clone(),
107 runtime.generation,
108 runtime.child_start_count,
109 ready_signal,
110 cancellation_token.clone(),
111 );
112 mark_immediate_ready(runtime.spec.readiness_policy, &ctx, &mut runtime);
113 runtime.status = ChildRuntimeStatus::Running;
114 let (completion_sender, completion_receiver) = watch::channel(None);
115
116 // Choose the spawn strategy based on the child's isolation setting.
117 // BlockingPool tasks are spawned on the async worker pool normally but
118 // wrapped with `tokio::task::block_in_place` at the very start. This
119 // signals to the tokio scheduler that the worker thread may block and
120 // allows a replacement worker to be spawned, preventing worker thread
121 // starvation without creating a nested runtime.
122 //
123 // Background: `spawn_blocking` + `spawn_blocking` inner runtime is an
124 // anti-pattern because it allocates a full current-thread runtime per
125 // blocking thread, wasting memory and potentially exhausting the
126 // blocking pool. `block_in_place` is the tokio-approved approach for
127 // CPU-heavy or blocking async tasks.
128 let child_task = match runtime.spec.isolation {
129 crate::spec::child::Isolation::BlockingPool => {
130 let ctx_clone = ctx.clone_for_blocking();
131 let shared_factory = factory.clone();
132 tokio::spawn(async move {
133 // Signal to the tokio scheduler that this task may block.
134 // The scheduler will spawn a replacement worker if needed.
135 tokio::task::block_in_place(move || {
136 // Inside block_in_place we create a minimal one-shot
137 // runtime to drive the factory future to completion.
138 // This is unavoidable because the factory returns an
139 // async future — there is no synchronous variant. The
140 // key difference from the previous approach is that
141 // `block_in_place` tells the outer runtime to lend us
142 // this worker thread rather than permanently stealing it.
143 let rt = tokio::runtime::Builder::new_current_thread()
144 .enable_all()
145 .build()
146 .expect("BlockingPool: failed to build one-shot runtime");
147 rt.block_on(shared_factory.build(ctx_clone))
148 })
149 })
150 }
151 crate::spec::child::Isolation::AsyncWorker => tokio::spawn(factory.build(ctx)),
152 };
153 let abort_handle = child_task.abort_handle();
154 let run_ready_receiver = ready_receiver.clone();
155 tokio::spawn(async move {
156 let report = run_factory(runtime, run_ready_receiver, child_task).await;
157 let _ignored = completion_sender.send(Some(report));
158 });
159 Ok(ChildRunHandle {
160 cancellation_token,
161 abort_handle,
162 completion_receiver,
163 heartbeat_receiver,
164 readiness_receiver: ready_receiver.clone(),
165 })
166 }
167}
168
169/// Marks a runtime ready when policy requires immediate readiness.
170///
171/// # Arguments
172///
173/// - `policy`: Readiness policy attached to the child.
174/// - `ctx`: Task context that owns the readiness sender.
175/// - `runtime`: Runtime record whose status should advance.
176///
177/// # Returns
178///
179/// This function does not return a value.
180fn mark_immediate_ready(policy: ReadinessPolicy, ctx: &TaskContext, runtime: &mut ChildRuntime) {
181 if policy.is_immediate() {
182 ctx.mark_ready();
183 runtime.status = ChildRuntimeStatus::Ready;
184 }
185}
186
187/// Runs a factory and classifies the result.
188///
189/// # Arguments
190///
191/// - `factory`: Task factory for this child.
192/// - `ctx`: Per-child_start_count task context.
193///
194/// # Returns
195///
196/// Returns the classified task exit.
197async fn run_factory(
198 mut runtime: ChildRuntime,
199 ready_receiver: watch::Receiver<ReadinessState>,
200 task: JoinHandle<crate::task::factory::TaskResult>,
201) -> Result<ChildRunReport, SupervisorError> {
202 match task.await {
203 Ok(result) => {
204 let exit = TaskExit::from_task_result(result);
205 let became_ready = observe_ready(ready_receiver);
206 if became_ready {
207 runtime.status = ChildRuntimeStatus::Ready;
208 }
209 runtime.last_exit = Some(exit.clone());
210 Ok(ChildRunReport {
211 runtime,
212 exit,
213 became_ready,
214 })
215 }
216 Err(error) if error.is_panic() => {
217 let exit = TaskExit::Panicked(String::from("task panicked"));
218 runtime.last_exit = Some(exit.clone());
219 Ok(ChildRunReport {
220 runtime,
221 exit,
222 became_ready: observe_ready(ready_receiver),
223 })
224 }
225 Err(_error) => {
226 let exit = TaskExit::Cancelled;
227 runtime.last_exit = Some(exit.clone());
228 Ok(ChildRunReport {
229 runtime,
230 exit,
231 became_ready: observe_ready(ready_receiver),
232 })
233 }
234 }
235}
236
237/// Observes whether readiness was reported.
238///
239/// # Arguments
240///
241/// - `ready_receiver`: Receiver that stores the latest readiness value.
242///
243/// # Returns
244///
245/// Returns `true` when the receiver observed readiness.
246fn observe_ready(ready_receiver: watch::Receiver<ReadinessState>) -> bool {
247 matches!(*ready_receiver.borrow(), ReadinessState::Ready)
248}
249
250/// Waits for the report sender to publish a child run report.
251///
252/// # Arguments
253///
254/// - `completion_receiver`: Receiver published by the run observer task.
255///
256/// # Returns
257///
258/// Returns the completed run report.
259pub(crate) async fn wait_for_report(
260 completion_receiver: &mut Receiver<Option<Result<ChildRunReport, SupervisorError>>>,
261) -> Result<ChildRunReport, SupervisorError> {
262 loop {
263 if let Some(result) = completion_receiver.borrow().clone() {
264 return result;
265 }
266 if completion_receiver.changed().await.is_err() {
267 return Err(SupervisorError::InvalidTransition {
268 message: "child run report channel closed before completion".to_owned(),
269 });
270 }
271 }
272}