Skip to main content

net_lattice_model/
event.rs

1use crate::ifaddr::InterfaceAddressId;
2use crate::interface::InterfaceId;
3use crate::neighbor::NeighborId;
4use crate::route::RouteId;
5
6/// What kind of change an [`Event`] reports.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum ChangeKind {
10    Added,
11    Removed,
12    /// Reserved for a future field-mask payload (`Changed { fields: ... }`)
13    /// — see ARCHITECTURE.md's note on `Event`. Carries no detail yet: a
14    /// consumer that needs to know what changed re-reads the object through
15    /// the relevant provider.
16    Changed,
17}
18
19/// A domain whose state must be re-read after an overflow notification.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum EventDomain {
23    Route,
24    Interface,
25    Neighbor,
26    Address,
27    All,
28}
29
30/// A composable filter for event domains and individual network objects.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct EventFilter {
33    routes: bool,
34    interfaces: bool,
35    neighbors: bool,
36    addresses: bool,
37    route_ids: Option<Vec<RouteId>>,
38    interface_ids: Option<Vec<InterfaceId>>,
39    neighbor_ids: Option<Vec<NeighborId>>,
40    address_ids: Option<Vec<InterfaceAddressId>>,
41}
42
43impl EventFilter {
44    pub const ALL: Self = Self {
45        routes: true,
46        interfaces: true,
47        neighbors: true,
48        addresses: true,
49        route_ids: None,
50        interface_ids: None,
51        neighbor_ids: None,
52        address_ids: None,
53    };
54    pub const fn none() -> Self {
55        Self {
56            routes: false,
57            interfaces: false,
58            neighbors: false,
59            addresses: false,
60            route_ids: None,
61            interface_ids: None,
62            neighbor_ids: None,
63            address_ids: None,
64        }
65    }
66    pub const fn routes(mut self) -> Self {
67        self.routes = true;
68        self
69    }
70    pub const fn interfaces(mut self) -> Self {
71        self.interfaces = true;
72        self
73    }
74    pub const fn neighbors(mut self) -> Self {
75        self.neighbors = true;
76        self
77    }
78    pub const fn addresses(mut self) -> Self {
79        self.addresses = true;
80        self
81    }
82
83    /// Narrows route delivery to `id`. Repeated object selectors compose as
84    /// a union within their domain.
85    pub fn route(mut self, id: RouteId) -> Self {
86        self.routes = true;
87        add_selector(&mut self.route_ids, id);
88        self
89    }
90    /// Narrows interface delivery to `id`.
91    pub fn interface(mut self, id: InterfaceId) -> Self {
92        self.interfaces = true;
93        add_selector(&mut self.interface_ids, id);
94        self
95    }
96    /// Narrows neighbor delivery to `id`.
97    pub fn neighbor(mut self, id: NeighborId) -> Self {
98        self.neighbors = true;
99        add_selector(&mut self.neighbor_ids, id);
100        self
101    }
102    /// Narrows interface-address delivery to `id`.
103    pub fn address(mut self, id: InterfaceAddressId) -> Self {
104        self.addresses = true;
105        add_selector(&mut self.address_ids, id);
106        self
107    }
108
109    /// Whether this filter selects at least one event domain.
110    pub const fn is_empty(&self) -> bool {
111        !self.routes && !self.interfaces && !self.neighbors && !self.addresses
112    }
113
114    /// Whether this filter delivers `event`. Object selectors are applied
115    /// before a backend enqueues an ordinary event. A resynchronization event
116    /// has no object ID, so it is selected by its affected domain.
117    pub fn matches(&self, event: Event) -> bool {
118        match event {
119            Event::Route { id, .. } => self.routes && selected(&self.route_ids, id),
120            Event::Interface { id, .. } => self.interfaces && selected(&self.interface_ids, id),
121            Event::Neighbor { id, .. } => self.neighbors && selected(&self.neighbor_ids, id),
122            Event::Address { id, .. } => self.addresses && selected(&self.address_ids, id),
123            Event::ResyncRequired { domain } => match domain {
124                EventDomain::Route => self.routes,
125                EventDomain::Interface => self.interfaces,
126                EventDomain::Neighbor => self.neighbors,
127                EventDomain::Address => self.addresses,
128                EventDomain::All => {
129                    self.routes || self.interfaces || self.neighbors || self.addresses
130                }
131            },
132        }
133    }
134}
135
136fn add_selector<T: PartialEq>(selectors: &mut Option<Vec<T>>, value: T) {
137    let selectors = selectors.get_or_insert_with(Vec::new);
138    if !selectors.contains(&value) {
139        selectors.push(value);
140    }
141}
142
143fn selected<T: PartialEq>(selectors: &Option<Vec<T>>, value: T) -> bool {
144    selectors
145        .as_ref()
146        .is_none_or(|selectors| selectors.contains(&value))
147}
148impl Default for EventFilter {
149    fn default() -> Self {
150        Self::ALL
151    }
152}
153
154/// A signal that something changed in the system's networking state —
155/// carrying an ID and a [`ChangeKind`], not a clone of the full domain
156/// object.
157///
158/// Deliberately signal-shaped rather than snapshot-shaped (e.g.
159/// `Event::Route { id, kind }`, not `Event::RouteAdded(Route)`): native
160/// change notifications frequently don't hand over the full object in the
161/// first place (an `RTM_NEWROUTE` message or a Windows route-change
162/// callback can carry only what changed, not a complete record), and a
163/// consumer that only cares *that* something changed before deciding
164/// whether to re-query it shouldn't pay for a clone of the full object on
165/// every event. A consumer that needs the current value re-reads it
166/// through the relevant provider (`Lattice::routes()`, keyed by the ID
167/// carried here).
168///
169/// `#[non_exhaustive]`: DNS resolver configuration changes are a stated
170/// long-term goal for this enum (see ARCHITECTURE.md) but are not emitted
171/// by any backend as of Stage 0.13. Consequently, DNS mutation has no
172/// corresponding watcher-delivery guarantee. No supported
173/// platform exposes a single native push signal for all resolver-manager and
174/// per-adapter changes without filesystem polling, which is not a genuine
175/// push signal. Adding a `Dns` variant later is not a breaking change for
176/// consumers who already `match` non-exhaustively.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
178#[non_exhaustive]
179pub enum Event {
180    Route {
181        id: RouteId,
182        kind: ChangeKind,
183    },
184    Interface {
185        id: InterfaceId,
186        kind: ChangeKind,
187    },
188    Neighbor {
189        id: NeighborId,
190        kind: ChangeKind,
191    },
192    Address {
193        id: InterfaceAddressId,
194        kind: ChangeKind,
195    },
196    /// Notifications were dropped because the bounded queue filled; re-read
197    /// the indicated domain before relying on later signals.
198    ResyncRequired {
199        domain: EventDomain,
200    },
201}
202impl Event {
203    pub const fn resync_all() -> Self {
204        Self::ResyncRequired {
205            domain: EventDomain::All,
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn events_carry_id_and_kind() {
216        let event = Event::Route {
217            id: RouteId::new(1),
218            kind: ChangeKind::Added,
219        };
220        assert_eq!(
221            event,
222            Event::Route {
223                id: RouteId::new(1),
224                kind: ChangeKind::Added,
225            }
226        );
227        assert_ne!(
228            event,
229            Event::Route {
230                id: RouteId::new(1),
231                kind: ChangeKind::Removed,
232            }
233        );
234    }
235
236    #[test]
237    fn object_selectors_compose_without_leaking_other_objects() {
238        let filter = EventFilter::none()
239            .route(RouteId::new(1))
240            .route(RouteId::new(2));
241        assert!(filter.matches(Event::Route {
242            id: RouteId::new(1),
243            kind: ChangeKind::Changed,
244        }));
245        assert!(filter.matches(Event::Route {
246            id: RouteId::new(2),
247            kind: ChangeKind::Changed,
248        }));
249        assert!(!filter.matches(Event::Route {
250            id: RouteId::new(3),
251            kind: ChangeKind::Changed,
252        }));
253    }
254
255    #[test]
256    fn resync_is_selected_by_domain_even_for_object_filter() {
257        let filter = EventFilter::none().route(RouteId::new(1));
258        assert!(filter.matches(Event::ResyncRequired {
259            domain: EventDomain::Route,
260        }));
261        assert!(!filter.matches(Event::ResyncRequired {
262            domain: EventDomain::Address,
263        }));
264    }
265
266    #[test]
267    fn domain_builders_default_and_empty_state_cover_every_domain() {
268        let empty = EventFilter::none();
269        assert!(empty.is_empty());
270        assert!(!empty.matches(Event::Route {
271            id: RouteId::new(1),
272            kind: ChangeKind::Added,
273        }));
274
275        let domains = EventFilter::none()
276            .routes()
277            .interfaces()
278            .neighbors()
279            .addresses();
280        assert!(!domains.is_empty());
281        assert!(domains.matches(Event::Route {
282            id: RouteId::new(1),
283            kind: ChangeKind::Added,
284        }));
285        assert!(domains.matches(Event::Interface {
286            id: InterfaceId::new(1),
287            kind: ChangeKind::Removed,
288        }));
289        assert!(domains.matches(Event::Neighbor {
290            id: NeighborId::new(1),
291            kind: ChangeKind::Changed,
292        }));
293        assert!(domains.matches(Event::Address {
294            id: InterfaceAddressId::new(1),
295            kind: ChangeKind::Added,
296        }));
297        assert_eq!(EventFilter::default(), EventFilter::ALL);
298    }
299
300    #[test]
301    fn object_selectors_cover_every_domain_and_deduplicate() {
302        let filter = EventFilter::none()
303            .route(RouteId::new(1))
304            .route(RouteId::new(1))
305            .interface(InterfaceId::new(2))
306            .neighbor(NeighborId::new(3))
307            .address(InterfaceAddressId::new(4));
308
309        assert!(filter.matches(Event::Route {
310            id: RouteId::new(1),
311            kind: ChangeKind::Added,
312        }));
313        assert!(!filter.matches(Event::Route {
314            id: RouteId::new(2),
315            kind: ChangeKind::Added,
316        }));
317        assert!(filter.matches(Event::Interface {
318            id: InterfaceId::new(2),
319            kind: ChangeKind::Added,
320        }));
321        assert!(!filter.matches(Event::Interface {
322            id: InterfaceId::new(3),
323            kind: ChangeKind::Added,
324        }));
325        assert!(filter.matches(Event::Neighbor {
326            id: NeighborId::new(3),
327            kind: ChangeKind::Added,
328        }));
329        assert!(!filter.matches(Event::Neighbor {
330            id: NeighborId::new(4),
331            kind: ChangeKind::Added,
332        }));
333        assert!(filter.matches(Event::Address {
334            id: InterfaceAddressId::new(4),
335            kind: ChangeKind::Added,
336        }));
337        assert!(!filter.matches(Event::Address {
338            id: InterfaceAddressId::new(5),
339            kind: ChangeKind::Added,
340        }));
341    }
342
343    #[test]
344    fn resync_all_is_selected_only_when_a_domain_is_selected() {
345        assert_eq!(
346            Event::resync_all(),
347            Event::ResyncRequired {
348                domain: EventDomain::All
349            }
350        );
351        assert!(!EventFilter::none().matches(Event::resync_all()));
352        assert!(EventFilter::none().routes().matches(Event::resync_all()));
353    }
354
355    #[test]
356    fn resync_domain_matching_covers_interface_and_neighbor() {
357        assert!(
358            EventFilter::none()
359                .interfaces()
360                .matches(Event::ResyncRequired {
361                    domain: EventDomain::Interface,
362                })
363        );
364        assert!(
365            EventFilter::none()
366                .neighbors()
367                .matches(Event::ResyncRequired {
368                    domain: EventDomain::Neighbor,
369                })
370        );
371    }
372}