saluki_core/observability/metrics/
reflector.rs1use std::sync::Arc;
3
4use async_trait::async_trait;
5use futures::{Stream, StreamExt};
6use saluki_common::sync::shutdown::ShutdownHandle;
7use tokio::{
8 select,
9 sync::{Mutex, Notify},
10};
11
12use crate::runtime::{InitializationError, Supervisable, SupervisorFuture};
13
14pub trait Processor: Send + Sync {
16 type Input;
18
19 type State: Send + Sync;
21
22 fn build_initial_state(&self) -> Self::State;
24
25 fn process(&self, input: Self::Input, state: &Self::State);
27}
28
29struct StoreInner<P: Processor> {
30 processor: P,
31 state: P::State,
32 notify_update: Notify,
33}
34
35struct Store<P: Processor> {
41 inner: Arc<StoreInner<P>>,
42}
43
44impl<P: Processor> Store<P> {
45 fn from_processor(processor: P) -> Self {
46 let state = processor.build_initial_state();
47 Self {
48 inner: Arc::new(StoreInner {
49 processor,
50 state,
51 notify_update: Notify::const_new(),
52 }),
53 }
54 }
55
56 pub fn process<I>(&self, inputs: I)
57 where
58 I: IntoIterator<Item = P::Input>,
59 {
60 for input in inputs {
61 self.inner.processor.process(input, &self.inner.state);
62 }
63 self.inner.notify_update.notify_waiters();
64 }
65
66 pub async fn wait_for_update(&self) {
67 self.inner.notify_update.notified().await;
68 }
69
70 pub fn state(&self) -> &P::State {
71 &self.inner.state
72 }
73}
74
75impl<P: Processor> Clone for Store<P> {
76 fn clone(&self) -> Self {
77 Self {
78 inner: Arc::clone(&self.inner),
79 }
80 }
81}
82
83pub struct Reflector<P: Processor> {
94 store: Store<P>,
95}
96
97impl<P: Processor> Reflector<P> {
98 pub fn new<S, I>(source: S, processor: P) -> (Self, ReflectorWorker<P, S>)
113 where
114 S: Stream<Item = I> + Unpin + Send + 'static,
115 I: IntoIterator<Item = P::Input> + Send,
116 P: 'static,
117 {
118 let store = Store::from_processor(processor);
119 let worker = ReflectorWorker {
120 store: store.clone(),
121 source: Arc::new(Mutex::new(source)),
122 };
123
124 (Self { store }, worker)
125 }
126}
127
128impl<P: Processor> Clone for Reflector<P> {
129 fn clone(&self) -> Self {
130 Self {
131 store: self.store.clone(),
132 }
133 }
134}
135
136pub struct ReflectorWorker<P: Processor, S> {
152 store: Store<P>,
153 source: Arc<Mutex<S>>,
154}
155
156#[async_trait]
157impl<P, S, I> Supervisable for ReflectorWorker<P, S>
158where
159 P: Processor + 'static,
160 S: Stream<Item = I> + Unpin + Send + 'static,
161 I: IntoIterator<Item = P::Input> + Send,
162{
163 fn name(&self) -> &str {
164 "reflector"
165 }
166
167 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
168 let store = self.store.clone();
169 let source = Arc::clone(&self.source);
170
171 Ok(Box::pin(async move {
172 let mut source = source.lock_owned().await;
173
174 select! {
175 _ = process_shutdown => {},
176 _ = drive_source(&mut *source, &store) => {},
177 }
178
179 Ok(())
180 }))
181 }
182}
183
184async fn drive_source<P, S, I>(source: &mut S, store: &Store<P>)
186where
187 P: Processor,
188 S: Stream<Item = I> + Unpin,
189 I: IntoIterator<Item = P::Input>,
190{
191 while let Some(inputs) = source.next().await {
192 store.process(inputs);
193 }
194}
195
196impl<P: Processor> Reflector<P> {
197 pub async fn wait_for_update(&self) {
201 self.store.wait_for_update().await;
202 }
203
204 pub fn state(&self) -> &P::State {
206 self.store.state()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use std::{
213 pin::Pin,
214 sync::Mutex as StdMutex,
215 task::{Context, Poll},
216 time::Duration,
217 };
218
219 use tokio::{sync::mpsc, time::timeout};
220
221 use super::*;
222
223 struct TestProcessor;
225
226 impl Processor for TestProcessor {
227 type Input = u32;
228 type State = StdMutex<Vec<u32>>;
229
230 fn build_initial_state(&self) -> Self::State {
231 StdMutex::new(Vec::new())
232 }
233
234 fn process(&self, input: Self::Input, state: &Self::State) {
235 state.lock().unwrap().push(input);
236 }
237 }
238
239 struct TestSource(mpsc::UnboundedReceiver<Vec<u32>>);
241
242 impl Stream for TestSource {
243 type Item = Vec<u32>;
244
245 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
246 self.0.poll_recv(cx)
247 }
248 }
249
250 fn build() -> (
251 mpsc::UnboundedSender<Vec<u32>>,
252 Reflector<TestProcessor>,
253 ReflectorWorker<TestProcessor, TestSource>,
254 ) {
255 let (tx, rx) = mpsc::unbounded_channel();
256 let (reflector, worker) = Reflector::new(TestSource(rx), TestProcessor);
257 (tx, reflector, worker)
258 }
259
260 fn observed(reflector: &Reflector<TestProcessor>) -> Vec<u32> {
261 reflector.state().lock().unwrap().clone()
262 }
263
264 async fn wait_for_len(reflector: &Reflector<TestProcessor>, expected: usize) {
267 timeout(Duration::from_secs(5), async {
268 while observed(reflector).len() < expected {
269 tokio::task::yield_now().await;
270 }
271 })
272 .await
273 .expect("timed out waiting for the reflector to observe the expected items");
274 }
275
276 #[tokio::test]
277 async fn reports_initial_state_before_worker_runs() {
278 let (tx, reflector, _worker) = build();
281
282 tx.send(vec![1, 2, 3]).unwrap();
283
284 assert_eq!(observed(&reflector), Vec::<u32>::new());
285 }
286
287 #[tokio::test]
288 async fn worker_feeds_source_items_into_shared_state() {
289 let (tx, reflector, worker) = build();
290
291 tx.send(vec![1, 2]).unwrap();
292 tx.send(vec![3]).unwrap();
293 drop(tx);
294
295 let fut = worker.initialize(ShutdownHandle::noop()).await.unwrap();
296 fut.await.unwrap();
297
298 assert_eq!(observed(&reflector), vec![1, 2, 3]);
299 }
300
301 #[tokio::test]
302 async fn worker_completes_when_source_ends() {
303 let (tx, _reflector, worker) = build();
304 drop(tx);
305
306 let fut = worker.initialize(ShutdownHandle::noop()).await.unwrap();
307
308 timeout(Duration::from_secs(5), fut)
310 .await
311 .expect("worker should complete once the source is exhausted")
312 .unwrap();
313 }
314
315 #[tokio::test]
316 async fn worker_stops_on_shutdown_signal() {
317 let (_tx, _reflector, worker) = build();
319 let (coordinator, shutdown) = ShutdownHandle::paired();
320
321 let fut = worker.initialize(shutdown).await.unwrap();
322 let handle = tokio::spawn(fut);
323
324 coordinator.shutdown();
325
326 timeout(Duration::from_secs(5), handle)
327 .await
328 .expect("worker should stop once shutdown is signalled")
329 .unwrap()
330 .unwrap();
331 }
332
333 #[tokio::test]
334 async fn restarted_worker_resumes_from_the_same_source() {
335 let (tx, reflector, worker) = build();
338
339 tx.send(vec![1]).unwrap();
340
341 let fut = worker.initialize(ShutdownHandle::noop()).await.unwrap();
342 let handle = tokio::spawn(fut);
343 wait_for_len(&reflector, 1).await;
344
345 handle.abort();
347 let _ = handle.await;
348
349 tx.send(vec![2]).unwrap();
351 drop(tx);
352
353 let fut = worker.initialize(ShutdownHandle::noop()).await.unwrap();
354 fut.await.unwrap();
355
356 assert_eq!(observed(&reflector), vec![1, 2]);
357 }
358
359 #[tokio::test]
360 async fn state_is_shared_across_clones() {
361 let (tx, reflector, worker) = build();
362 let cloned = reflector.clone();
363
364 tx.send(vec![7]).unwrap();
365 drop(tx);
366
367 let fut = worker.initialize(ShutdownHandle::noop()).await.unwrap();
368 fut.await.unwrap();
369
370 assert_eq!(observed(&cloned), vec![7]);
371 }
372}