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}