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