ipc_broker/worker.rs
1use async_trait::async_trait;
2use serde_json::Value;
3use std::{collections::HashMap, future, sync::Arc};
4#[cfg(unix)]
5use tokio::net::UnixStream;
6#[cfg(windows)]
7use tokio::net::windows::named_pipe::ClientOptions;
8use tokio::{
9 io::{AsyncRead, AsyncWrite},
10 net::TcpStream,
11 sync::watch,
12};
13
14use crate::{
15 broker::{read_packet, write_packet},
16 rpc::{RpcRequest, RpcResponse},
17};
18
19/// Abstraction for any asynchronous stream that can both read and write.
20///
21/// This trait is implemented for all types that implement
22/// [`AsyncRead`] and [`AsyncWrite`] with [`Unpin`].
23///
24/// This allows the worker runtime to remain agnostic of the underlying
25/// transport (TCP, Unix socket, or Windows named pipe).
26trait AsyncStream: AsyncRead + AsyncWrite {}
27impl<T: AsyncRead + AsyncWrite + Unpin> AsyncStream for T {}
28
29/// Trait for an object that can be exposed to the RPC worker.
30///
31/// A `SharedObject` defines how a given object responds to remote
32/// procedure calls. Each object must implement a single method:
33///
34/// - [`call`](SharedObject::call): Handles a method call with JSON arguments
35/// and returns a JSON value as a result.
36///
37/// Objects must be thread-safe (`Send + Sync`) since they may be shared
38/// across async tasks.
39#[async_trait]
40pub trait SharedObject: Send + Sync {
41 /// Handle an RPC call for this object.
42 ///
43 /// # Arguments
44 ///
45 /// * `method` - The method name to invoke on this object.
46 /// * `args` - JSON-encoded arguments to the method.
47 ///
48 /// # Returns
49 ///
50 /// JSON value representing the result of the call.
51 async fn call(&self, method: &str, args: &Value) -> Value;
52}
53
54/// Builder pattern for constructing and spawning a worker runtime.
55///
56/// A `WorkerBuilder` allows you to register multiple [`SharedObject`]s
57/// under a given name, and then spawn a worker that connects to an RPC
58/// broker and handles incoming requests.
59///
60/// # Example
61/// ```ignore
62/// struct Calculator;
63///
64/// #[async_trait]
65/// impl SharedObject for Calculator {
66/// async fn call(&self, method: &str, args: &Value) -> Value {
67/// match method {
68/// "add" => {
69/// let a = args["a"].as_i64().unwrap();
70/// let b = args["b"].as_i64().unwrap();
71/// Value::from(a + b)
72/// }
73/// _ => Value::Null,
74/// }
75/// }
76/// }
77///
78/// WorkerBuilder::new()
79/// .add("Calculator", Calculator)
80/// .spawn()
81/// .await
82/// .unwrap();
83/// ```
84pub struct WorkerBuilder {
85 objects: HashMap<String, Arc<dyn SharedObject>>,
86 shutdown_rx: Option<watch::Receiver<bool>>,
87}
88
89impl WorkerBuilder {
90 /// Create a new, empty worker builder.
91 pub fn new() -> Self {
92 Self {
93 objects: HashMap::new(),
94 shutdown_rx: None,
95 }
96 }
97
98 /// Add a named object to the worker.
99 ///
100 /// # Arguments
101 ///
102 /// * `name` - The name under which this object will be registered.
103 /// * `obj` - The object implementing [`SharedObject`].
104 ///
105 /// # Returns
106 ///
107 /// Updated [`WorkerBuilder`] so calls can be chained.
108 pub fn add<T>(mut self, name: &str, obj: T) -> Self
109 where
110 T: SharedObject + 'static,
111 {
112 self.objects.insert(name.to_string(), Arc::new(obj));
113 self
114 }
115
116 pub fn with_graceful_shutdown(mut self) -> (Self, watch::Sender<bool>) {
117 let (shutdown_tx, shutdown_rx) = watch::channel(false);
118 self.shutdown_rx = Some(shutdown_rx);
119 (self, shutdown_tx)
120 }
121
122 /// Spawn the worker runtime with all registered objects.
123 ///
124 /// This connects to the broker (via TCP, Unix socket, or Windows named
125 /// pipe depending on platform and environment) and begins handling
126 /// RPC requests.
127 pub async fn spawn(self) -> std::io::Result<()> {
128 worker_supervisor(self.objects, self.shutdown_rx).await
129 }
130}
131
132impl Default for WorkerBuilder {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138/// This supervisor will manage the worker whether to retry if connection failed or stop gracefully
139/// with an OS signal to stop.
140async fn worker_supervisor(
141 objects: HashMap<String, Arc<dyn SharedObject>>,
142 shutdown_rx: Option<watch::Receiver<bool>>,
143) -> std::io::Result<()> {
144 use std::time::Duration;
145
146 loop {
147 let shutdown_rx = shutdown_rx.clone();
148 match run_worker(objects.clone(), shutdown_rx.clone()).await {
149 Ok(_) => {
150 log::warn!("Worker exited normally, ending service now...");
151 break;
152 }
153 Err(e) => {
154 log::error!("Worker has failed to connect: {e}, retrying. . .");
155 }
156 }
157
158 tokio::select! {
159 _ = tokio::signal::ctrl_c() => {
160 log::info!("Supervisor shutting down");
161 break;
162 }
163 // ✅ Optional graceful shutdown
164 _ = async {
165 if let Some(mut rx) = shutdown_rx {
166 let _ = rx.changed().await;
167 } else {
168 future::pending::<()>().await;
169 }
170 } => {
171 log::info!("Shutdown signal received (watch)");
172 break;
173 }
174 _ = tokio::time::sleep(Duration::from_secs(2)) => {}
175 }
176 }
177
178 Ok(())
179}
180
181/// Worker runtime function that handles broker connection and RPC calls.
182///
183/// This function is not intended to be used directly. Instead, use
184/// [`WorkerBuilder::spawn`].
185///
186/// The worker will:
187/// 1. Connect to the broker (TCP if `BROKER_ADDR` env var is set, otherwise
188/// Unix socket on Unix or named pipe on Windows).
189/// 2. Register all shared objects.
190/// 3. Enter a loop processing incoming requests:
191/// - [`RpcRequest::Call`] will invoke the corresponding object’s method.
192/// - Results are sent back as [`RpcResponse::Result`].
193/// - Other requests and responses are logged.
194///
195/// # Errors
196///
197/// Returns an error if the connection to the broker fails or if I/O errors occur.
198async fn run_worker(
199 objects: HashMap<String, Arc<dyn SharedObject>>,
200 shutdown_rx: Option<watch::Receiver<bool>>,
201) -> std::io::Result<()> {
202 let mut stream: Box<dyn AsyncStream + Send + Unpin> =
203 if let Ok(ip) = std::env::var("BROKER_ADDR") {
204 let tcp = TcpStream::connect(ip.as_str()).await?;
205 log::info!("Connected into TCP: {ip}");
206 Box::new(tcp)
207 } else {
208 #[cfg(unix)]
209 {
210 use crate::rpc::UNIX_PATH;
211 let unix = UnixStream::connect(UNIX_PATH).await?;
212 log::info!("Connected into Unix: {UNIX_PATH}");
213 Box::new(unix)
214 }
215
216 #[cfg(windows)]
217 {
218 use crate::rpc::PIPE_PATH;
219 loop {
220 let res = match ClientOptions::new().open(PIPE_PATH) {
221 Ok(pipe) => {
222 log::info!("Connected into NamedPipe: {PIPE_PATH}");
223 Box::new(pipe)
224 }
225 Err(e) if e.raw_os_error() == Some(231) => {
226 use std::time::Duration;
227 log::error!("All pipe instances busy, retrying...");
228 tokio::time::sleep(Duration::from_millis(100)).await;
229 continue;
230 }
231 Err(e) => {
232 use std::time::Duration;
233 log::error!("Failed to connect to pipe: {}", e);
234 tokio::time::sleep(Duration::from_millis(100)).await;
235 continue;
236 }
237 };
238 break res;
239 }
240 }
241 };
242
243 // Register all objects
244 for name in objects.keys() {
245 let reg = RpcRequest::RegisterObject {
246 object_name: name.clone(),
247 };
248 let data = match serde_json::to_vec(®) {
249 Ok(d) => d,
250 Err(e) => {
251 log::error!("Failed to serialize RegisterObject for {}: {e}", name);
252 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
253 }
254 };
255 write_packet(&mut stream, &data).await?;
256 }
257
258 let mut error = None;
259 loop {
260 let shutdown_rx = shutdown_rx.clone();
261 tokio::select! {
262 _ = tokio::signal::ctrl_c() => {
263 log::info!("Ctrl-C received, shutting down worker loop.");
264 break;
265 }
266 // ✅ Optional graceful shutdown
267 _ = async {
268 if let Some(mut rx) = shutdown_rx {
269 let _ = rx.changed().await;
270 } else {
271 future::pending::<()>().await;
272 }
273 } => {
274 log::info!("Shutdown signal received (watch)");
275 break;
276 }
277 result = read_packet(&mut stream) => {
278 let buf = match result {
279 Ok(data) => data,
280 Err(err) => {
281 // ✅ Log once
282 log::error!("Connection lost: {err}");
283 // ✅ Exit loop
284 error = Some(err);
285 break;
286 }
287 };
288
289 if let Ok(req) = serde_json::from_slice::<RpcRequest>(&buf) {
290 match req {
291 RpcRequest::Call {
292 call_id,
293 object_name,
294 method,
295 args,
296 } => {
297 if let Some(obj) = objects.get(&object_name) {
298 let result = obj.call(&method, &args).await;
299
300 let cid = call_id.clone();
301 let resp = RpcResponse::Result {
302 call_id,
303 object_name,
304 value: result,
305 };
306
307 let resp_bytes = match serde_json::to_vec(&resp) {
308 Ok(b) => b,
309 Err(e) => {
310 log::error!("Failed to serialize response for call {:?}: {e}", cid);
311 continue;
312 }
313 };
314
315 if let Err(err) = write_packet(&mut stream, &resp_bytes).await {
316 log::error!("Write error (closing connection): {err}");
317 error = Some(err);
318 break;
319 }
320 } else {
321 log::error!("Unknown object: {object_name}");
322 }
323 }
324 _ => {
325 log::debug!("Unsupported request: {req:?}");
326 }
327 }
328 } else if let Ok(resp) = serde_json::from_slice::<RpcResponse>(&buf) {
329 log::debug!("Worker got response: {resp:?}");
330 } else {
331 log::error!("Invalid JSON value from broker");
332 }
333 }
334 }
335 }
336
337 if let Some(error) = error {
338 Err(error)
339 } else {
340 Ok(())
341 }
342}