Skip to main content

subetha_cxc/
pass_registry.rs

1//! Closure registry for cross-process `Pass<F>` dispatch.
2//!
3//! Rust closures cannot be safely serialised across process
4//! boundaries; they reference function pointers that are not
5//! position-stable, and they may capture variables of arbitrary
6//! types. The PSC / Ray / Akka pattern is to register closures by
7//! ID at startup; the wire protocol carries the ID + serialised
8//! args, not the closure code.
9//!
10//! Each process must register the SAME ID -> closure mapping at
11//! startup (typically via a macro that all participating binaries
12//! call). A `Pass { id, args }` can then be dispatched by any
13//! process, including failover targets.
14
15use std::collections::HashMap;
16use std::sync::RwLock;
17
18use once_cell::sync::Lazy;
19
20/// A passable unit of work: a closure ID plus its serialised args.
21#[derive(Debug, Clone)]
22pub struct Pass {
23    pub closure_id: u32,
24    pub args: Vec<u8>,
25}
26
27/// Outcome of running a Pass; arbitrary bytes that the originator
28/// can deserialise.
29pub type PassResult = Result<Vec<u8>, PassError>;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PassError {
33    UnknownClosureId(u32),
34    ExecutionError(String),
35}
36
37/// Closure handler signature. Args are raw bytes; result is raw
38/// bytes. Caller-supplied (de)serialisation.
39pub type PassHandler = Box<dyn Fn(&[u8]) -> PassResult + Send + Sync + 'static>;
40
41struct Registry {
42    handlers: HashMap<u32, PassHandler>,
43}
44
45static REGISTRY: Lazy<RwLock<Registry>> = Lazy::new(|| RwLock::new(Registry {
46    handlers: HashMap::new(),
47}));
48
49/// Register a closure under `id`. Subsequent calls with the same id
50/// overwrite. Returns the previous handler if any.
51pub fn register<F>(id: u32, f: F) -> Option<PassHandler>
52where F: Fn(&[u8]) -> PassResult + Send + Sync + 'static,
53{
54    let mut g = REGISTRY.write().expect("registry write lock poisoned");
55    g.handlers.insert(id, Box::new(f))
56}
57
58/// Unregister a closure. Returns the handler if any.
59pub fn unregister(id: u32) -> Option<PassHandler> {
60    let mut g = REGISTRY.write().expect("registry write lock poisoned");
61    g.handlers.remove(&id)
62}
63
64/// True when `id` is registered in this process.
65pub fn is_registered(id: u32) -> bool {
66    let g = REGISTRY.read().expect("registry read lock poisoned");
67    g.handlers.contains_key(&id)
68}
69
70/// Number of registered handlers in this process.
71pub fn registered_count() -> usize {
72    let g = REGISTRY.read().expect("registry read lock poisoned");
73    g.handlers.len()
74}
75
76/// Execute a Pass. Returns the closure's result, or
77/// `PassError::UnknownClosureId` when the id is not registered.
78pub fn execute(pass: &Pass) -> PassResult {
79    let g = REGISTRY.read().expect("registry read lock poisoned");
80    match g.handlers.get(&pass.closure_id) {
81        Some(handler) => handler(&pass.args),
82        None => Err(PassError::UnknownClosureId(pass.closure_id)),
83    }
84}
85
86/// Macro helper for static-registry style registration. Each
87/// participating binary should call `register_pass!(ID, "name",
88/// |args| { ... })` at startup.
89#[macro_export]
90macro_rules! register_pass {
91    ($id:expr, $name:expr, $handler:expr) => {{
92        // Registration returns Option<PassHandler>: Some = replaced
93        // prior handler, None = first registration. The prior must
94        // be dropped IMMEDIATELY (not held for the macro scope) so
95        // a `let _prior =` binding is wrong - use explicit drop.
96        drop($crate::pass_registry::register($id, $handler));
97        let _name = $name;  // name is for documentation; not stored
98    }};
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn register_then_execute_round_trips_bytes() {
107        // Use distinct ids per test to avoid registry contention
108        // across parallel runs.
109        let id = 0x1000_0001;
110        register(id, |args| {
111            let mut out = args.to_vec();
112            out.reverse();
113            Ok(out)
114        });
115        let pass = Pass { closure_id: id, args: b"hello".to_vec() };
116        let r = execute(&pass).unwrap();
117        assert_eq!(r, b"olleh");
118        unregister(id);
119    }
120
121    #[test]
122    fn unknown_closure_id_returns_error() {
123        let pass = Pass { closure_id: 0xDEAD_BEEF, args: vec![] };
124        assert_eq!(execute(&pass), Err(PassError::UnknownClosureId(0xDEAD_BEEF)));
125    }
126
127    #[test]
128    fn re_register_overwrites_previous() {
129        let id = 0x1000_0002;
130        register(id, |_| Ok(b"v1".to_vec()));
131        register(id, |_| Ok(b"v2".to_vec()));
132        let pass = Pass { closure_id: id, args: vec![] };
133        assert_eq!(execute(&pass).unwrap(), b"v2");
134        unregister(id);
135    }
136
137    #[test]
138    fn handler_can_return_execution_error() {
139        let id = 0x1000_0003;
140        register(id, |_| Err(PassError::ExecutionError("nope".to_string())));
141        let pass = Pass { closure_id: id, args: vec![] };
142        match execute(&pass) {
143            Err(PassError::ExecutionError(msg)) => assert_eq!(msg, "nope"),
144            other => panic!("expected ExecutionError, got {other:?}"),
145        }
146        unregister(id);
147    }
148
149    #[test]
150    fn is_registered_and_count_accurate() {
151        let id_a = 0x1000_0004;
152        let id_b = 0x1000_0005;
153        register(id_a, |_| Ok(vec![]));
154        register(id_b, |_| Ok(vec![]));
155        assert!(is_registered(id_a));
156        assert!(is_registered(id_b));
157        // The registry is GLOBAL and sibling tests register and
158        // unregister concurrently, so a before/after count delta is
159        // not a stable property. What must hold: the count includes
160        // the two registrations this test owns right now.
161        assert!(registered_count() >= 2);
162        unregister(id_a);
163        unregister(id_b);
164        assert!(!is_registered(id_a));
165    }
166
167    #[test]
168    fn macro_registration_works() {
169        let id = 0x1000_0006;
170        register_pass!(id, "test_pass", |args: &[u8]| {
171            Ok(args.iter().map(|b| b.wrapping_add(1)).collect())
172        });
173        assert!(is_registered(id));
174        let pass = Pass { closure_id: id, args: vec![1, 2, 3] };
175        assert_eq!(execute(&pass).unwrap(), vec![2, 3, 4]);
176        unregister(id);
177    }
178}