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}