Skip to main content

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}