Skip to main content

rust_supervisor/control/
handle.rs

1//! Public runtime control handle.
2//!
3//! The handle owns the command sender side and exposes asynchronous control
4//! methods. It keeps command construction separate from runtime execution.
5
6use crate::control::command::{CommandMeta, CommandResult, ControlCommand};
7use crate::dashboard::runtime::DashboardIpcRuntimeGuard;
8use crate::error::types::SupervisorError;
9use crate::id::types::{ChildId, SupervisorPath};
10use crate::runtime::control_loop::RuntimeCommand;
11use std::sync::Arc;
12use tokio::sync::{broadcast, mpsc, oneshot};
13
14/// Cloneable handle used to control a running supervisor.
15#[derive(Debug, Clone)]
16pub struct SupervisorHandle {
17    /// Sender side used to submit runtime control commands.
18    command_sender: mpsc::Sender<RuntimeCommand>,
19    /// Broadcast sender used to create lifecycle event subscriptions.
20    event_sender: broadcast::Sender<String>,
21    /// Optional dashboard IPC runtime guard.
22    dashboard_runtime: Option<Arc<DashboardIpcRuntimeGuard>>,
23}
24
25impl SupervisorHandle {
26    /// Creates a runtime handle from channel senders.
27    ///
28    /// # Arguments
29    ///
30    /// - `command_sender`: Sender used to submit runtime commands.
31    /// - `event_sender`: Sender used to subscribe to lifecycle events.
32    ///
33    /// # Returns
34    ///
35    /// Returns a [`SupervisorHandle`].
36    pub fn new(
37        command_sender: mpsc::Sender<RuntimeCommand>,
38        event_sender: broadcast::Sender<String>,
39    ) -> Self {
40        Self {
41            command_sender,
42            event_sender,
43            dashboard_runtime: None,
44        }
45    }
46
47    /// Attaches a dashboard IPC runtime guard to this handle.
48    ///
49    /// # Arguments
50    ///
51    /// - `dashboard_runtime`: Guard that owns dashboard IPC runtime tasks.
52    ///
53    /// # Returns
54    ///
55    /// Returns this handle with dashboard runtime lifecycle attached.
56    pub(crate) fn with_dashboard_runtime(
57        mut self,
58        dashboard_runtime: Arc<DashboardIpcRuntimeGuard>,
59    ) -> Self {
60        self.dashboard_runtime = Some(dashboard_runtime);
61        self
62    }
63
64    /// Adds a child manifest under a supervisor path.
65    ///
66    /// # Arguments
67    ///
68    /// - `target`: Supervisor path that should receive the child.
69    /// - `child_manifest`: Child manifest text supplied by the caller.
70    /// - `requested_by`: Actor that requested the command.
71    /// - `reason`: Human-readable command reason.
72    ///
73    /// # Returns
74    ///
75    /// Returns a [`CommandResult`] after the runtime accepts the command.
76    pub async fn add_child(
77        &self,
78        target: SupervisorPath,
79        child_manifest: impl Into<String>,
80        requested_by: impl Into<String>,
81        reason: impl Into<String>,
82    ) -> Result<CommandResult, SupervisorError> {
83        self.send(ControlCommand::AddChild {
84            meta: CommandMeta::new(requested_by, reason),
85            target,
86            child_manifest: child_manifest.into(),
87        })
88        .await
89    }
90
91    /// Removes a child from runtime governance.
92    ///
93    /// # Arguments
94    ///
95    /// - `child_id`: Target child identifier.
96    /// - `requested_by`: Actor that requested the command.
97    /// - `reason`: Human-readable command reason.
98    ///
99    /// # Returns
100    ///
101    /// Returns a [`CommandResult`] after removal or idempotent reuse.
102    pub async fn remove_child(
103        &self,
104        child_id: ChildId,
105        requested_by: impl Into<String>,
106        reason: impl Into<String>,
107    ) -> Result<CommandResult, SupervisorError> {
108        self.child_command(child_id, requested_by, reason, |meta, child_id| {
109            ControlCommand::RemoveChild { meta, child_id }
110        })
111        .await
112    }
113
114    /// Restarts a child explicitly.
115    ///
116    /// # Arguments
117    ///
118    /// - `child_id`: Target child identifier.
119    /// - `requested_by`: Actor that requested the command.
120    /// - `reason`: Human-readable command reason.
121    ///
122    /// # Returns
123    ///
124    /// Returns a [`CommandResult`] after restart dispatch.
125    pub async fn restart_child(
126        &self,
127        child_id: ChildId,
128        requested_by: impl Into<String>,
129        reason: impl Into<String>,
130    ) -> Result<CommandResult, SupervisorError> {
131        self.child_command(child_id, requested_by, reason, |meta, child_id| {
132            ControlCommand::RestartChild { meta, child_id }
133        })
134        .await
135    }
136
137    /// Pauses a child idempotently.
138    ///
139    /// # Arguments
140    ///
141    /// - `child_id`: Target child identifier.
142    /// - `requested_by`: Actor that requested the command.
143    /// - `reason`: Human-readable command reason.
144    ///
145    /// # Returns
146    ///
147    /// Returns the current child state after the command.
148    pub async fn pause_child(
149        &self,
150        child_id: ChildId,
151        requested_by: impl Into<String>,
152        reason: impl Into<String>,
153    ) -> Result<CommandResult, SupervisorError> {
154        self.child_command(child_id, requested_by, reason, |meta, child_id| {
155            ControlCommand::PauseChild { meta, child_id }
156        })
157        .await
158    }
159
160    /// Resumes a child idempotently.
161    ///
162    /// # Arguments
163    ///
164    /// - `child_id`: Target child identifier.
165    /// - `requested_by`: Actor that requested the command.
166    /// - `reason`: Human-readable command reason.
167    ///
168    /// # Returns
169    ///
170    /// Returns the current child state after the command.
171    pub async fn resume_child(
172        &self,
173        child_id: ChildId,
174        requested_by: impl Into<String>,
175        reason: impl Into<String>,
176    ) -> Result<CommandResult, SupervisorError> {
177        self.child_command(child_id, requested_by, reason, |meta, child_id| {
178            ControlCommand::ResumeChild { meta, child_id }
179        })
180        .await
181    }
182
183    /// Quarantines a child idempotently.
184    ///
185    /// # Arguments
186    ///
187    /// - `child_id`: Target child identifier.
188    /// - `requested_by`: Actor that requested the command.
189    /// - `reason`: Human-readable command reason.
190    ///
191    /// # Returns
192    ///
193    /// Returns the current child state after the command.
194    pub async fn quarantine_child(
195        &self,
196        child_id: ChildId,
197        requested_by: impl Into<String>,
198        reason: impl Into<String>,
199    ) -> Result<CommandResult, SupervisorError> {
200        self.child_command(child_id, requested_by, reason, |meta, child_id| {
201            ControlCommand::QuarantineChild { meta, child_id }
202        })
203        .await
204    }
205
206    /// Shuts down the supervisor tree idempotently.
207    ///
208    /// # Arguments
209    ///
210    /// - `requested_by`: Actor that requested shutdown.
211    /// - `reason`: Human-readable shutdown reason.
212    ///
213    /// # Returns
214    ///
215    /// Returns the current shutdown result.
216    pub async fn shutdown_tree(
217        &self,
218        requested_by: impl Into<String>,
219        reason: impl Into<String>,
220    ) -> Result<CommandResult, SupervisorError> {
221        self.send(ControlCommand::ShutdownTree {
222            meta: CommandMeta::new(requested_by, reason),
223        })
224        .await
225    }
226
227    /// Queries the current runtime state.
228    ///
229    /// # Arguments
230    ///
231    /// This function has no arguments.
232    ///
233    /// # Returns
234    ///
235    /// Returns a [`CommandResult::CurrentState`] value.
236    pub async fn current_state(&self) -> Result<CommandResult, SupervisorError> {
237        self.send(ControlCommand::CurrentState {
238            meta: CommandMeta::new("system", "current_state"),
239        })
240        .await
241    }
242
243    /// Subscribes to runtime event text emitted by the control loop.
244    ///
245    /// # Arguments
246    ///
247    /// This function has no arguments.
248    ///
249    /// # Returns
250    ///
251    /// Returns a broadcast receiver for event text.
252    pub fn subscribe_events(&self) -> broadcast::Receiver<String> {
253        self.event_sender.subscribe()
254    }
255
256    /// Sends one control command and waits for the result.
257    ///
258    /// # Arguments
259    ///
260    /// - `command`: Command that should be executed by the runtime loop.
261    ///
262    /// # Returns
263    ///
264    /// Returns a command result or a supervisor error when the runtime is gone.
265    async fn send(&self, command: ControlCommand) -> Result<CommandResult, SupervisorError> {
266        let (reply_sender, reply_receiver) = oneshot::channel();
267        self.command_sender
268            .send(RuntimeCommand::Control {
269                command,
270                reply_sender,
271            })
272            .await
273            .map_err(|_| SupervisorError::InvalidTransition {
274                message: "runtime control loop is closed".to_owned(),
275            })?;
276        reply_receiver
277            .await
278            .map_err(|_| SupervisorError::InvalidTransition {
279                message: "runtime control loop dropped command reply".to_owned(),
280            })?
281    }
282
283    /// Builds and sends a child-targeted command.
284    ///
285    /// # Arguments
286    ///
287    /// - `child_id`: Target child identifier.
288    /// - `requested_by`: Actor that requested the command.
289    /// - `reason`: Human-readable command reason.
290    /// - `builder`: Command builder for the child operation.
291    ///
292    /// # Returns
293    ///
294    /// Returns a command result from the runtime loop.
295    async fn child_command<F>(
296        &self,
297        child_id: ChildId,
298        requested_by: impl Into<String>,
299        reason: impl Into<String>,
300        builder: F,
301    ) -> Result<CommandResult, SupervisorError>
302    where
303        F: FnOnce(CommandMeta, ChildId) -> ControlCommand,
304    {
305        let meta = CommandMeta::new(requested_by, reason);
306        self.send(builder(meta, child_id)).await
307    }
308}