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}