Skip to main content

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