tycho_simulation/snapshot_feed/
publisher.rs1use std::{future::Future, time::Duration};
4
5use futures::{Stream, StreamExt};
6use tokio::{
7 select,
8 sync::watch,
9 time::{sleep_until, Instant},
10};
11use tracing::{debug, info, warn};
12
13pub struct Publisher<T> {
25 tx: watch::Sender<Option<T>>,
26 published_at: Option<Instant>,
28}
29
30impl<T> Publisher<T> {
31 pub(crate) fn channel() -> (Self, watch::Receiver<Option<T>>) {
34 let (tx, rx) = watch::channel(None);
35 (Publisher { tx, published_at: None }, rx)
36 }
37
38 pub async fn publishing<E>(
51 mut self,
52 max_age: Option<Duration>,
53 snapshots: impl Stream<Item = Result<T, E>>,
54 ) -> Result<(), E> {
55 tokio::pin!(snapshots);
56 loop {
57 match self
58 .next_snapshot(max_age, snapshots.next())
59 .await
60 {
61 Some(Ok(snapshot)) => self.publish(snapshot),
62 Some(Err(e)) => return Err(e),
63 None => return Ok(()),
64 }
65 }
66 }
67
68 async fn next_snapshot<E>(
77 &mut self,
78 max_age: Option<Duration>,
79 next: impl Future<Output = Option<Result<T, E>>>,
80 ) -> Option<Result<T, E>> {
81 tokio::pin!(next);
82 let Publisher { tx, published_at } = self;
85
86 select! {
87 biased;
88 _ = tx.closed() => None,
89 max_age = going_stale(*published_at, max_age) => {
90 warn!(
91 ?max_age,
92 "no snapshot received within max_snapshot_age, withdrawing the published one"
93 );
94 withdraw(tx, published_at);
95
96 next.await
99 }
100 out = &mut next => out,
101 }
102 }
103
104 fn publish(&mut self, snapshot: T) {
106 if self.tx.send(Some(snapshot)).is_err() {
107 return;
111 }
112
113 match self.published_at {
114 Some(_) => debug!("snapshot refreshed"),
117 None => info!("serving a snapshot"),
118 }
119
120 self.published_at = Some(Instant::now());
121 }
122}
123
124async fn going_stale(published_at: Option<Instant>, max_age: Option<Duration>) -> Duration {
128 let Some((published_at, max_age)) = Option::zip(published_at, max_age) else {
129 return std::future::pending().await;
130 };
131 sleep_until(published_at + max_age).await;
132 max_age
133}
134
135fn withdraw<T>(tx: &watch::Sender<Option<T>>, published_at: &mut Option<Instant>) {
138 *published_at = None;
139 tx.send_if_modified(|snapshot| snapshot.take().is_some());
140}
141
142impl<T> Drop for Publisher<T> {
143 fn drop(&mut self) {
147 withdraw(&self.tx, &mut self.published_at);
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use std::sync::{
154 atomic::{AtomicBool, Ordering},
155 Arc,
156 };
157
158 use futures::{stream, StreamExt};
159 use rstest::rstest;
160 use tokio::time::sleep;
161 use tokio_stream::wrappers::WatchStream;
162
163 use super::{super::expect_to_finish, *};
164
165 #[tokio::test(start_paused = true)]
166 async fn next_snapshot_withdraws_at_max_age_and_keeps_waiting() {
167 let (mut publisher, mut rx) = Publisher::channel();
168 publisher.publish(1);
169 assert!(rx.borrow_and_update().is_some(), "a snapshot must be published to go stale");
170
171 let start = Instant::now();
172 let next = publisher
173 .next_snapshot(Some(Duration::from_millis(100)), async {
174 rx.changed()
175 .await
176 .expect("the publisher outlives this wait");
177 assert!(rx.borrow_and_update().is_none(), "the change must be the withdrawal");
178 assert_eq!(
179 start.elapsed(),
180 Duration::from_millis(100),
181 "the snapshot must be withdrawn once it reaches max_age"
182 );
183
184 sleep(Duration::from_millis(200)).await;
185 Some(Ok::<u32, String>(2))
186 })
187 .await;
188
189 assert_eq!(next, Some(Ok(2)), "the snapshot the feed was waiting for still arrives");
190 assert_eq!(
191 start.elapsed(),
192 Duration::from_millis(300),
193 "going stale must not cut the wait short: the call returns when the feed answers"
194 );
195 }
196
197 #[rstest]
198 #[case::no_max_age_never_goes_stale(None, Duration::from_secs(3600))]
199 #[case::snapshot_arrives_before_max_age(
200 Some(Duration::from_millis(100)),
201 Duration::from_millis(50)
202 )]
203 #[tokio::test(start_paused = true)]
204 async fn next_snapshot_keeps_a_snapshot_that_has_not_gone_stale(
205 #[case] max_age: Option<Duration>,
206 #[case] wait: Duration,
207 ) {
208 let (mut publisher, mut rx) = Publisher::channel();
209 publisher.publish(1);
210 rx.borrow_and_update();
211
212 publisher
213 .next_snapshot(max_age, async move {
214 sleep(wait).await;
215 Some(Ok::<u32, String>(2))
216 })
217 .await;
218
219 assert!(rx.borrow().is_some());
220 assert!(!rx.has_changed().unwrap(), "the watch must not be disturbed");
221 }
222
223 #[tokio::test(start_paused = true)]
224 async fn publishing_stops_as_soon_as_the_last_receiver_is_gone() {
225 let asked = Arc::new(AtomicBool::new(false));
228 let asked_clone = Arc::clone(&asked);
229 let snapshots = stream::once(async move {
230 asked_clone.store(true, Ordering::SeqCst);
231 std::future::pending::<Result<u32, String>>().await
232 });
233
234 let (publisher, rx) = Publisher::channel();
235 drop(rx);
236
237 expect_to_finish(
238 "the feed kept waiting although nobody was reading",
239 publisher.publishing(None, snapshots),
240 )
241 .await
242 .expect("no reader left is not a failure of the feed");
243
244 assert!(!asked.load(Ordering::SeqCst), "the stream is not even asked for a snapshot");
245 }
246
247 #[tokio::test]
248 async fn dropping_the_publisher_withdraws_what_it_published() {
249 let (mut publisher, rx) = Publisher::channel();
252 let mut changes = WatchStream::from_changes(rx);
253 publisher.publish(1);
254 assert_eq!(changes.next().await, Some(Some(1)));
255
256 drop(publisher);
257
258 assert_eq!(changes.next().await, Some(None), "dropping it withdraws what it published");
259 assert_eq!(changes.next().await, None, "and ends the stream");
260 }
261
262 #[tokio::test]
263 async fn dropping_a_publisher_that_published_nothing_leaves_the_watch_quiet() {
264 let (publisher, rx) = Publisher::<u32>::channel();
265 let mut changes = WatchStream::from_changes(rx);
266 drop(publisher);
267
268 assert_eq!(changes.next().await, None, "there was no snapshot to take away");
269 }
270}