Skip to main content

epics_libcom_rs/runtime/background/
mod.rs

1//! RTEMS-side background-execution infrastructure (CA sans-io refactor,
2//! increment W3a — the seam backend).
3//!
4//! # Why this exists (decision A2)
5//!
6//! The CA server is being made runnable on RTEMS (armv7-rtems-eabihf) with
7//! **one** async engine. On a hosted target, async *tails* — PACT device
8//! completion, FLNK/scanOnce chains, SDLY/ODLY/watchdog timers, WRITE_NOTIFY
9//! completion — run as tokio tasks via [`crate::runtime::task::Reactor::spawn`]. RTEMS
10//! has no tokio runtime (`tokio::spawn`/`tokio::time` need one), so those tails
11//! need a runtime-free home. This module is that home: three C-parity
12//! facilities built from **plain `std` threads + `Mutex`/`Condvar`**, carrying
13//! no tokio dependency.
14//!
15//! | Facility | C source | Rust |
16//! |----------|----------|------|
17//! | [`callback_executor`] | `callback.c` `callbackQueue[]`/`callbackTask` | priority-banded worker pool |
18//! | [`delayed_timer`] | `callback.c` `callbackRequestDelayed`/`timerQueue` | one deadline-ordered timer thread |
19//! | [`scan_once`] | `dbScan.c` `onceQ`/`scanOnce`/`onceTask` | bounded ring + one worker |
20//!
21//! # The seam handle
22//!
23//! [`BackgroundExecutor`] owns the three facilities and their threads;
24//! [`BackgroundExecutor::handle`] hands out a cheap, clonable
25//! [`BackgroundHandle`] that the future seam wiring routes synchronous-tail
26//! hand-offs through. The hosted (tokio) build keeps calling
27//! [`crate::runtime::task::Reactor::spawn`] — this module is **only** the RTEMS route,
28//! and this increment adds the infrastructure without switching any call site
29//! over to it (that is a later increment).
30
31pub mod callback_executor;
32pub mod delayed_timer;
33// `pub`, not `pub(crate)`: the record system's periodic-scan threads
34// (`epics_base_rs::server::scan`) and the database write gate run under the
35// same poison-recovery and panic-isolation rules as the facilities here, and
36// they call [`facility::recover`] / [`facility::run_isolated`] to get them.
37// Those call sites were inside this crate before the runtime layer was split
38// out; the split moved the callers across a crate boundary, and one shared
39// helper reached through a wider door is what keeps the rule single-sourced
40// rather than restated in the crate above.
41pub mod facility;
42pub mod future_exec;
43pub mod scan_once;
44pub mod timer_sleep;
45
46use std::time::Duration;
47
48pub use callback_executor::{
49    Callback, CallbackError, CallbackHandle, CallbackPool, CallbackPriority, CallbackQueueStats,
50    DEFAULT_QUEUE_SIZE, DEFAULT_THREADS_PER_PRIORITY, NUM_CALLBACK_PRIORITIES,
51};
52pub use delayed_timer::{DelayedTimer, TimerHandle};
53pub use future_exec::{
54    AbortHandle, DEFAULT_SPAWN_PRIORITY, JoinError, JoinFuture, spawn_blocking_on, spawn_future,
55};
56pub use timer_sleep::{Sleep, TimerInterval};
57
58pub use scan_once::{
59    DEFAULT_ONCE_QUEUE_SIZE, OnceCallback, ScanOnceHandle, ScanOnceOverflow, ScanOnceQueue,
60    ScanOnceQueueStats,
61};
62
63/// Owns the three background facilities (callback pool, delayed timer, scanOnce
64/// worker) and every thread backing them. Constructing it starts the threads;
65/// dropping it stops and joins them.
66///
67/// The delayed timer fires its due callbacks into the callback pool, exactly as
68/// C's `notify` routes an expired `epicsTimer` back through `callbackRequest`
69/// (`callback.c:404-419`).
70pub struct BackgroundExecutor {
71    callbacks: CallbackPool,
72    timer: DelayedTimer,
73    scan_once: ScanOnceQueue,
74}
75
76impl BackgroundExecutor {
77    /// Start all three facilities with their C-default sizing.
78    pub fn new() -> Self {
79        let callbacks = CallbackPool::new();
80        let timer = DelayedTimer::new(callbacks.handle());
81        let scan_once = ScanOnceQueue::new();
82        BackgroundExecutor {
83            callbacks,
84            timer,
85            scan_once,
86        }
87    }
88
89    /// A cheap, clonable handle over all three facilities — the seam route for
90    /// RTEMS synchronous-tail hand-offs.
91    pub fn handle(&self) -> BackgroundHandle {
92        BackgroundHandle {
93            callbacks: self.callbacks.handle(),
94            timer: self.timer.handle(),
95            scan_once: self.scan_once.handle(),
96        }
97    }
98
99    /// The callback executor pool.
100    pub fn callbacks(&self) -> &CallbackPool {
101        &self.callbacks
102    }
103
104    /// The delayed-callback timer.
105    pub fn timer(&self) -> &DelayedTimer {
106        &self.timer
107    }
108
109    /// The `scanOnce` facility.
110    pub fn scan_once(&self) -> &ScanOnceQueue {
111        &self.scan_once
112    }
113}
114
115impl Default for BackgroundExecutor {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// Cheap, clonable submission side of a [`BackgroundExecutor`]. Every method
122/// enqueues and returns immediately; the work runs on a background thread.
123///
124/// This is the type the sans-io seam routes RTEMS tail hand-offs into — the
125/// runtime-free counterpart of [`crate::runtime::task::Reactor::spawn`].
126#[derive(Clone)]
127pub struct BackgroundHandle {
128    callbacks: CallbackHandle,
129    timer: TimerHandle,
130    scan_once: ScanOnceHandle,
131}
132
133impl BackgroundHandle {
134    /// Enqueue an immediate callback on `priority` — C `callbackRequest`
135    /// (`callback.c:341`).
136    pub fn callback(&self, priority: CallbackPriority, cb: Callback) -> Result<(), CallbackError> {
137        self.callbacks.request(priority, cb)
138    }
139
140    /// Enqueue a callback to run after `delay` on `priority` — C
141    /// `callbackRequestDelayed` (`callback.c:410`).
142    pub fn callback_delayed(&self, delay: Duration, priority: CallbackPriority, cb: Callback) {
143        self.timer.schedule(delay, priority, cb);
144    }
145
146    /// Enqueue a one-shot record-process tail — C `scanOnce` (`dbScan.c:660`).
147    pub fn scan_once(&self, cb: OnceCallback) -> Result<(), ScanOnceOverflow> {
148        self.scan_once.scan_once(cb)
149    }
150
151    /// The callback-submission handle alone.
152    pub fn callbacks(&self) -> &CallbackHandle {
153        &self.callbacks
154    }
155
156    /// The delayed-timer handle alone.
157    pub fn timer(&self) -> &TimerHandle {
158        &self.timer
159    }
160
161    /// The scanOnce-submission handle alone.
162    pub fn scan_once_handle(&self) -> &ScanOnceHandle {
163        &self.scan_once
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::sync::mpsc;
171
172    const T: Duration = Duration::from_secs(5);
173
174    #[test]
175    fn executor_routes_all_three_facilities() {
176        let exec = BackgroundExecutor::new();
177        let h = exec.handle();
178
179        let (tx, rx) = mpsc::channel();
180        let tx_cb = tx.clone();
181        h.callback(
182            CallbackPriority::Medium,
183            Box::new(move || tx_cb.send("cb").unwrap()),
184        )
185        .unwrap();
186        let tx_once = tx.clone();
187        h.scan_once(Box::new(move || tx_once.send("once").unwrap()))
188            .unwrap();
189        h.callback_delayed(
190            Duration::from_millis(20),
191            CallbackPriority::High,
192            Box::new(move || tx.send("delayed").unwrap()),
193        );
194
195        let mut seen = std::collections::HashSet::new();
196        for _ in 0..3 {
197            seen.insert(rx.recv_timeout(T).unwrap());
198        }
199        assert!(seen.contains("cb"));
200        assert!(seen.contains("once"));
201        assert!(seen.contains("delayed"));
202    }
203}