agent_data_plane_config/
live.rs

1//! `Live<T>`: a live, typed view of one projection of the current configuration.
2
3use std::fmt;
4use std::ops::Deref;
5use std::sync::Arc;
6
7use arc_swap::ArcSwap;
8use tokio::sync::watch;
9
10use crate::SalukiConfiguration;
11
12// Borrow-in, borrow-out so navigating deeper never clones the whole configuration; the caller clones
13// only the final `T` when it needs an owned value.
14type Projection<T> = Arc<dyn for<'a> Fn(&'a SalukiConfiguration) -> &'a T + Send + Sync>;
15
16/// A typed view of one projection of the current configuration.
17///
18/// A never-dynamic consumer can hold a plain `T`; a dynamic one holds `Live<T>` and never learns
19/// whether it is fixed or tracking the live config. Read the view's cached snapshot with `Deref`;
20/// wait for and receive the next selected update with `changed`.
21///
22/// For a dynamic view, `Deref` does not perform a fresh read from the shared configuration. It
23/// returns the last snapshot processed by this view. If the configuration advances before this
24/// view awaits `changed`, `Deref` continues to return the older snapshot until `changed` processes
25/// the notification.
26pub struct Live<T> {
27    inner: Inner<T>,
28}
29
30enum Inner<T> {
31    Fixed(T),
32    Dynamic {
33        // The shared source. It can advance independently of this view's snapshot.
34        cell: Arc<ArcSwap<SalukiConfiguration>>,
35
36        // A wake-up signal, not the configuration value itself. The view re-projects the source
37        // after receiving it and ignores notifications that do not change T.
38        tick: watch::Receiver<()>,
39
40        // Re-applied to the shared source whenever the view processes a notification.
41        project: Projection<T>,
42
43        // This value belongs to this Live<T>. It is what Deref returns and what changed() compares
44        // against; a newer value in `cell` does not replace it until changed() processes a tick.
45        snapshot: T,
46    },
47}
48
49impl<T: Clone + PartialEq + 'static> Live<T> {
50    /// Creates a view that re-projects the shared configuration after accepted updates.
51    ///
52    /// The projection selects the subtree this view owns. The initial projected value is captured
53    /// immediately; later calls to `changed` load and project the shared configuration, then update
54    /// this view's local snapshot. Until `changed` processes a notification, `Deref` continues to
55    /// return the previous snapshot even if the shared configuration has advanced.
56    pub fn new_dynamic(
57        cell: Arc<ArcSwap<SalukiConfiguration>>, tick: watch::Receiver<()>,
58        project: impl for<'a> Fn(&'a SalukiConfiguration) -> &'a T + Send + Sync + 'static,
59    ) -> Self {
60        let snapshot = project(&cell.load()).clone();
61        Self {
62            inner: Inner::Dynamic {
63                cell,
64                tick,
65                project: Arc::new(project),
66                snapshot,
67            },
68        }
69    }
70
71    /// Creates a view with a value that never changes.
72    pub fn new_fixed(value: T) -> Self {
73        Self {
74            inner: Inner::Fixed(value),
75        }
76    }
77
78    /// Waits for the projected value to change and returns the new value.
79    ///
80    /// The notification only says that the shared source may have changed. This method loads the
81    /// source, applies the projection, compares it with this view's snapshot, and keeps waiting if
82    /// the selected value is unchanged. It parks forever when `Fixed` or the channel is closed, so
83    /// a caller can `select!` on it unconditionally. The returned value and `Deref` reflect the
84    /// same processed snapshot. This is a state-change watcher, not a fresh-read API or an event
85    /// history: multiple source updates may be coalesced before this view processes them.
86    pub async fn changed(&mut self) -> T {
87        match &mut self.inner {
88            Inner::Fixed(_) => std::future::pending::<T>().await,
89            Inner::Dynamic {
90                cell,
91                tick,
92                project,
93                snapshot,
94            } => loop {
95                if tick.changed().await.is_err() {
96                    std::future::pending::<T>().await;
97                }
98                let guard = cell.load();
99                let latest = project(&guard);
100                if *latest != *snapshot {
101                    *snapshot = latest.clone();
102                    return snapshot.clone();
103                }
104            },
105        }
106    }
107
108    /// Creates a child view by composing this view's projection with `f`.
109    ///
110    /// Use this when code already has a broad `Live<T>` but does not have the
111    /// `ConfigurationSystem` needed to create a narrower view
112    /// directly. A dynamic child shares the source and receives its own notification cursor and
113    /// snapshot; it wakes only when the selected child value changes. A fixed child is projected
114    /// once and remains fixed.
115    pub fn project<U>(&self, f: impl for<'a> Fn(&'a T) -> &'a U + Send + Sync + 'static) -> Live<U>
116    where
117        U: Clone + PartialEq + 'static,
118    {
119        match &self.inner {
120            Inner::Fixed(value) => Live::new_fixed(f(value).clone()),
121            Inner::Dynamic {
122                cell, tick, project, ..
123            } => {
124                let parent = Arc::clone(project);
125                Live::new_dynamic(Arc::clone(cell), tick.clone(), move |c| f(parent(c)))
126            }
127        }
128    }
129}
130
131impl<T> Deref for Live<T> {
132    type Target = T;
133    fn deref(&self) -> &T {
134        match &self.inner {
135            Inner::Fixed(value) => value,
136            Inner::Dynamic { snapshot, .. } => snapshot,
137        }
138    }
139}
140
141impl<T: Clone> Clone for Live<T> {
142    fn clone(&self) -> Self {
143        let inner = match &self.inner {
144            Inner::Fixed(value) => Inner::Fixed(value.clone()),
145            Inner::Dynamic {
146                cell,
147                tick,
148                project,
149                snapshot,
150            } => Inner::Dynamic {
151                cell: Arc::clone(cell),
152                tick: tick.clone(),
153                project: Arc::clone(project),
154                snapshot: snapshot.clone(),
155            },
156        };
157        Self { inner }
158    }
159}
160
161impl<T: fmt::Debug> fmt::Debug for Live<T> {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match &self.inner {
164            Inner::Fixed(value) => f.debug_tuple("Live::Fixed").field(value).finish(),
165            Inner::Dynamic { snapshot, .. } => f
166                .debug_struct("Live::Dynamic")
167                .field("snapshot", snapshot)
168                .finish_non_exhaustive(),
169        }
170    }
171}