Skip to main content

byteflow/scheduler/
monitor.rs

1//! Monitor table: one-way lifecycle watches (`A ──monitor──> B`).
2//!
3//! A monitor is a **runtime relation**, not a field on [`crate::Message`].
4//! When the target exits, [`MonitorTable::notify_target_exit`] yields
5//! [`DownEvent`]s that [`super::finalize`] delivers as Atomic Hop
6//! [`crate::TAG_SYS_DOWN`] messages.
7//!
8//! Relations are keyed by [`FlowId`] (identity). Bytecode addresses the
9//! target through a Cap; the worker resolves Cap → FlowId before calling
10//! here. Caps are revoked on exit; FlowIds are never reused.
11
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use super::error::{LifecycleError, RuntimeError};
16use super::process::FlowId;
17use super::sync_lock;
18
19/// Opaque monitor reference, echoed on the `DOWN` hop as `request_id`.
20///
21/// The inner id is runtime-minted (`pub(crate)`): bytecode only ever sees
22/// it as `Value::Int` after `Monitor`, never as a host-constructed token.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct MonitorRef(pub(crate) u64);
25
26static NEXT_MONITOR: AtomicU64 = AtomicU64::new(1);
27
28impl MonitorRef {
29    #[inline]
30    pub fn as_u64(self) -> u64 {
31        self.0
32    }
33
34    #[inline]
35    pub(crate) fn from_u64(raw: u64) -> Self {
36        Self(raw)
37    }
38}
39
40impl std::fmt::Display for MonitorRef {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(f, "monitor#{}", self.0)
43    }
44}
45
46/// Why a flow left the directory. Encoded as `Message.payload` on system hops.
47///
48/// Named [`FlowExitReason`] so it does not collide with the JIT
49/// `ExitReason` (trace-compiler control flow).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[repr(u8)]
52pub enum FlowExitReason {
53    Normal = 0,
54    /// Reserved: host-wide runtime teardown (not produced today).
55    Shutdown = 1,
56    Killed = 2,
57    Fault = 3,
58    /// Reserved: overflow-kill policy (today overflow is `MailboxFull` / park).
59    MailboxOverflow = 4,
60    Supervisor = 5,
61    Link = 6,
62}
63
64impl FlowExitReason {
65    pub fn from_u64(raw: u64) -> Option<Self> {
66        Some(match raw {
67            0 => Self::Normal,
68            1 => Self::Shutdown,
69            2 => Self::Killed,
70            3 => Self::Fault,
71            4 => Self::MailboxOverflow,
72            5 => Self::Supervisor,
73            6 => Self::Link,
74            _ => return None,
75        })
76    }
77
78    #[inline]
79    pub fn as_u64(self) -> u64 {
80        self as u64
81    }
82
83    /// Abnormal exits propagate through links (BEAM: `normal` does not).
84    ///
85    /// `Shutdown` / `Killed` / `Supervisor` are host-policy reasons;
86    /// they still count as abnormal so a supervisor abort is not
87    /// silently swallowed by links.
88    #[inline]
89    pub fn is_abnormal(self) -> bool {
90        !matches!(self, Self::Normal)
91    }
92}
93
94impl std::fmt::Display for FlowExitReason {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let name = match self {
97            Self::Normal => "normal",
98            Self::Shutdown => "shutdown",
99            Self::Killed => "killed",
100            Self::Fault => "fault",
101            Self::MailboxOverflow => "mailbox-overflow",
102            Self::Supervisor => "supervisor",
103            Self::Link => "link",
104        };
105        f.write_str(name)
106    }
107}
108
109/// One monitor firing after the target left the runtime.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct DownEvent {
112    pub monitor: MonitorRef,
113    pub owner: FlowId,
114    pub target: FlowId,
115    pub reason: FlowExitReason,
116}
117
118#[derive(Debug, Clone, Copy)]
119struct MonitorEntry {
120    owner: FlowId,
121    target: FlowId,
122}
123
124/// `MonitorRef → { owner, target }`. Lives on [`super::runtime::Shared`].
125pub struct MonitorTable {
126    monitors: HashMap<MonitorRef, MonitorEntry>,
127}
128
129impl MonitorTable {
130    pub fn new() -> Self {
131        Self {
132            monitors: HashMap::new(),
133        }
134    }
135
136    pub fn create(&mut self, owner: FlowId, target: FlowId) -> MonitorRef {
137        let monitor = MonitorRef(NEXT_MONITOR.fetch_add(1, Ordering::Relaxed));
138        self.monitors.insert(
139            monitor,
140            MonitorEntry { owner, target },
141        );
142        monitor
143    }
144
145    /// Remove `monitor` only if `owner` still owns it.
146    pub fn remove_owned(&mut self, owner: FlowId, monitor: MonitorRef) -> Result<(), LifecycleError> {
147        match self.monitors.get(&monitor) {
148            Some(entry) if entry.owner == owner => {
149                self.monitors.remove(&monitor);
150                Ok(())
151            }
152            Some(_) => Err(LifecycleError::NotOwner),
153            None => Err(LifecycleError::InvalidMonitor),
154        }
155    }
156
157    /// Drop every monitor whose **owner** is `flow` (owner exited).
158    pub fn remove_owned_by(&mut self, owner: FlowId) {
159        self.monitors.retain(|_, entry| entry.owner != owner);
160    }
161
162    /// Collect `DOWN` events for monitors watching `target`, then drop them.
163    pub fn notify_target_exit(
164        &mut self,
165        target: FlowId,
166        reason: FlowExitReason,
167    ) -> Vec<DownEvent> {
168        let mut events = Vec::new();
169        self.monitors.retain(|monitor, entry| {
170            if entry.target != target {
171                return true;
172            }
173            events.push(DownEvent {
174                monitor: *monitor,
175                owner: entry.owner,
176                target,
177                reason,
178            });
179            false
180        });
181        events.sort_unstable_by_key(|event| event.monitor.0);
182        events
183    }
184}
185
186impl Default for MonitorTable {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192/// Fail-closed wrapper around [`MonitorTable`].
193pub struct MonitorStore {
194    inner: std::sync::Mutex<MonitorTable>,
195}
196
197impl MonitorStore {
198    pub fn new() -> Self {
199        Self {
200            inner: std::sync::Mutex::new(MonitorTable::new()),
201        }
202    }
203
204    pub fn create(&self, owner: FlowId, target: FlowId) -> Result<MonitorRef, RuntimeError> {
205        Ok(sync_lock::lock(&self.inner, "MonitorStore::create")?.create(owner, target))
206    }
207
208    pub fn remove_owned(
209        &self,
210        owner: FlowId,
211        monitor: MonitorRef,
212    ) -> Result<Result<(), LifecycleError>, RuntimeError> {
213        Ok(sync_lock::lock(&self.inner, "MonitorStore::remove_owned")?.remove_owned(owner, monitor))
214    }
215
216    pub fn remove_owned_by(&self, owner: FlowId) -> Result<(), RuntimeError> {
217        sync_lock::lock(&self.inner, "MonitorStore::remove_owned_by")?.remove_owned_by(owner);
218        Ok(())
219    }
220
221    pub fn notify_target_exit(
222        &self,
223        target: FlowId,
224        reason: FlowExitReason,
225    ) -> Result<Vec<DownEvent>, RuntimeError> {
226        Ok(sync_lock::lock(&self.inner, "MonitorStore::notify_target_exit")?
227            .notify_target_exit(target, reason))
228    }
229}
230
231impl Default for MonitorStore {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::scheduler::process::next_flow_id;
241
242    #[test]
243    fn notify_removes_and_sorts() {
244        let mut table = MonitorTable::new();
245        let owner = next_flow_id();
246        let target = next_flow_id();
247        let a = table.create(owner, target);
248        let b = table.create(owner, target);
249        let events = table.notify_target_exit(target, FlowExitReason::Fault);
250        assert_eq!(events.len(), 2);
251        assert_eq!(events[0].monitor, a);
252        assert_eq!(events[1].monitor, b);
253        assert!(table.notify_target_exit(target, FlowExitReason::Fault).is_empty());
254    }
255
256    #[test]
257    fn remove_owned_rejects_other_flow() {
258        let mut table = MonitorTable::new();
259        let owner = next_flow_id();
260        let other = next_flow_id();
261        let target = next_flow_id();
262        let mon = table.create(owner, target);
263        assert_eq!(table.remove_owned(other, mon), Err(LifecycleError::NotOwner));
264        assert!(table.remove_owned(owner, mon).is_ok());
265    }
266}