Skip to main content

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(&reg) {
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}