1use std::error;
11use std::marker::PhantomData;
12
13use time::OffsetDateTime;
14use tracing::{debug, warn};
15
16use super::FlushPolicy;
17use crate::sink::{Sink, WindowMeta};
18use crate::store::{Segment as _, Store};
19
20#[derive(Debug, thiserror::Error)]
22pub enum TierError<St, S>
23where
24 St: error::Error + Send + Sync + 'static,
25 S: error::Error + Send + Sync + 'static,
26{
27 #[error("store error: {0}")]
29 Store(#[source] St),
30 #[error("downstream sink error: {0}")]
32 Downstream(#[source] S),
33}
34
35pub struct Tier<R, St, S> {
66 store: St,
68 policy: FlushPolicy,
70 inner: S,
72 pipeline: Option<String>,
74 initialized: bool,
76 active_window: Option<i64>,
78 active_count: usize,
80 _record: PhantomData<fn() -> R>,
82}
83
84impl<R, St, S> Tier<R, St, S> {
85 #[must_use]
87 pub fn new(store: St, policy: FlushPolicy, inner: S) -> Self {
88 Self {
89 store,
90 policy,
91 inner,
92 pipeline: None,
93 initialized: false,
94 active_window: None,
95 active_count: 0,
96 _record: PhantomData,
97 }
98 }
99
100 #[must_use]
104 pub fn with_pipeline_name(mut self, name: impl Into<String>) -> Self {
105 self.pipeline = Some(name.into());
106 self
107 }
108
109 #[must_use]
111 pub fn inner(&self) -> &S {
112 &self.inner
113 }
114
115 fn window_secs(&self) -> i64 {
116 i64::try_from(self.policy.every.as_secs())
117 .unwrap_or(i64::MAX)
118 .max(1)
119 }
120
121 fn align(&self, unix: i64) -> i64 {
122 unix - unix.rem_euclid(self.window_secs())
123 }
124}
125
126impl<R, St, S> Tier<R, St, S>
127where
128 R: Send + 'static,
129 St: Store<R>,
130 S: Sink<R>,
131{
132 fn pipeline_name(&self) -> String {
133 self.pipeline
134 .as_deref()
135 .or_else(|| self.store.pipeline_hint())
136 .unwrap_or("unknown")
137 .to_owned()
138 }
139
140 async fn ensure_init(&mut self) {
145 if self.initialized {
146 return;
147 }
148 self.initialized = true;
149 if let Err(error) = self.drain(true).await {
150 warn!(
151 pipeline = %self.pipeline_name(),
152 %error,
153 "startup replay failed; stored windows retained for retry"
154 );
155 }
156 }
157
158 async fn drain(&mut self, include_active: bool) -> Result<(), TierError<St::Error, S::Error>> {
166 let pipeline = self.pipeline_name();
169 let window_secs = self.window_secs();
170 let mut cursor = None;
174 let mut first_error = None;
175 loop {
176 let Some(mut seg) = self.store.oldest(cursor).await.map_err(TierError::Store)? else {
177 break;
178 };
179 let window = seg.window();
180 if !include_active && Some(window) == self.active_window {
181 cursor = Some(window);
184 continue;
185 }
186 let records = match seg.records().await {
187 Ok(records) => records,
188 Err(error) => {
189 warn!(
190 pipeline = %pipeline,
191 window,
192 %error,
193 "failed to read stored window; retained for retry"
194 );
195 first_error.get_or_insert(TierError::Store(error));
196 cursor = Some(window);
197 continue;
198 }
199 };
200 if !records.is_empty() {
201 let start = OffsetDateTime::from_unix_timestamp(window)
202 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
203 let meta = WindowMeta {
204 pipeline: pipeline.clone(),
205 start,
206 end: start.saturating_add(time::Duration::seconds(window_secs)),
210 };
211 debug!(
212 pipeline = %pipeline,
213 window,
214 records = records.len(),
215 "tier draining window downstream"
216 );
217 if let Err(error) = self.inner.ingest(&meta, records).await {
218 warn!(
219 pipeline = %pipeline,
220 window,
221 %error,
222 "downstream rejected window; retained for retry"
223 );
224 first_error.get_or_insert(TierError::Downstream(error));
225 cursor = Some(window);
226 continue;
227 }
228 }
229 if let Err(error) = seg.commit().await {
230 warn!(
231 pipeline = %pipeline,
232 window,
233 %error,
234 "failed to remove drained window from store"
235 );
236 first_error.get_or_insert(TierError::Store(error));
237 cursor = Some(window);
238 continue;
239 }
240 if Some(window) == self.active_window {
241 self.active_window = None;
242 self.active_count = 0;
243 }
244 }
245 match first_error {
246 None => Ok(()),
247 Some(error) => Err(error),
248 }
249 }
250}
251
252impl<R, St, S> Sink<R> for Tier<R, St, S>
253where
254 R: Send + 'static,
255 St: Store<R>,
256 S: Sink<R>,
257{
258 type Error = TierError<St::Error, S::Error>;
259
260 async fn ingest(&mut self, meta: &WindowMeta, records: Vec<R>) -> Result<(), Self::Error> {
261 if self.pipeline.is_none() {
262 self.pipeline = Some(meta.pipeline.clone());
263 }
264 self.ensure_init().await;
265
266 if !records.is_empty() {
267 let window = self.align(meta.end.unix_timestamp());
268 let count = records.len();
269 self.store
270 .append(window, records)
271 .await
272 .map_err(TierError::Store)?;
273 if self.active_window != Some(window) {
274 self.active_window = Some(window);
275 self.active_count = 0;
276 }
277 self.active_count += count;
278 }
279
280 if self.active_count >= self.policy.max_records {
281 self.drain(true).await
282 } else {
283 self.drain(false).await
284 }
285 }
286
287 async fn flush(&mut self) -> Result<(), Self::Error> {
288 self.initialized = true;
291 self.drain(true).await?;
292 self.inner.flush().await.map_err(TierError::Downstream)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use std::sync::Arc;
299 use std::sync::atomic::{AtomicBool, Ordering};
300 use std::time::Duration;
301
302 use super::*;
303 use crate::store::MemStore;
304 use crate::test_util::{SharedSink, TestSinkFailure, meta_at};
305
306 struct PoisonSink {
309 inner: SharedSink<i32>,
310 armed: Arc<AtomicBool>,
311 }
312
313 impl Sink<i32> for PoisonSink {
314 type Error = TestSinkFailure;
315
316 async fn ingest(
317 &mut self,
318 meta: &WindowMeta,
319 records: Vec<i32>,
320 ) -> Result<(), TestSinkFailure> {
321 if self.armed.load(Ordering::SeqCst) && records.contains(&13) {
322 return Err(TestSinkFailure);
323 }
324 self.inner.ingest(meta, records).await
325 }
326
327 async fn flush(&mut self) -> Result<(), TestSinkFailure> {
328 self.inner.flush().await
329 }
330 }
331
332 #[tokio::test]
333 async fn mem_tier_holds_until_max_records() {
334 let inner = SharedSink::new();
335 let mut sink = Tier::new(
336 MemStore::new(),
337 FlushPolicy::new(Duration::from_secs(3600), 3),
338 inner.clone(),
339 );
340
341 sink.ingest(&meta_at("p", 10), vec![1, 2]).await.unwrap();
342 assert!(inner.batches().is_empty());
343
344 sink.ingest(&meta_at("p", 20), vec![3]).await.unwrap();
345 let batches = inner.batches();
346 assert_eq!(batches.len(), 1);
347 assert_eq!(batches[0].1, vec![1, 2, 3]);
348 }
349
350 #[tokio::test]
351 async fn time_unbounded_policy_drains_on_record_count() {
352 let inner = SharedSink::new();
353 let mut sink = Tier::new(
354 MemStore::new(),
355 FlushPolicy::new(Duration::MAX, 2),
356 inner.clone(),
357 );
358
359 sink.ingest(&meta_at("p", 10), vec![1]).await.unwrap();
363 assert!(inner.batches().is_empty());
364
365 sink.ingest(&meta_at("p", 20), vec![2]).await.unwrap();
366 let batches = inner.batches();
367 assert_eq!(batches.len(), 1);
368 assert_eq!(batches[0].0.start.unix_timestamp(), 0);
369 assert_eq!(
371 batches[0].0.end,
372 batches[0]
373 .0
374 .start
375 .saturating_add(time::Duration::seconds(i64::MAX))
376 );
377 assert_eq!(batches[0].1, vec![1, 2]);
378 }
379
380 #[tokio::test]
381 async fn mem_tier_drains_closed_window_on_next_ingest() {
382 let inner = SharedSink::new();
383 let mut sink = Tier::new(
384 MemStore::new(),
385 FlushPolicy::every(Duration::from_secs(300)),
386 inner.clone(),
387 );
388
389 sink.ingest(&meta_at("p", 10), vec![1]).await.unwrap();
390 assert!(inner.batches().is_empty());
391
392 sink.ingest(&meta_at("p", 310), vec![2]).await.unwrap();
395 let batches = inner.batches();
396 assert_eq!(batches.len(), 1);
397 assert_eq!(batches[0].0.pipeline, "p");
398 assert_eq!(batches[0].0.start.unix_timestamp(), 0);
399 assert_eq!(batches[0].0.end.unix_timestamp(), 300);
400 assert_eq!(batches[0].1, vec![1]);
401 }
402
403 #[tokio::test]
404 async fn mem_tier_retains_records_across_failing_downstream() {
405 let inner = SharedSink::new();
406 let mut sink = Tier::new(
407 MemStore::new(),
408 FlushPolicy::new(Duration::from_secs(3600), 2),
409 inner.clone(),
410 );
411
412 inner.set_fail(true);
413 assert!(sink.ingest(&meta_at("p", 10), vec![1, 2]).await.is_err());
414 assert!(inner.batches().is_empty());
415
416 inner.set_fail(false);
417 sink.ingest(&meta_at("p", 20), vec![3]).await.unwrap();
418 let batches = inner.batches();
419 assert_eq!(batches.len(), 1);
420 assert_eq!(batches[0].1, vec![1, 2, 3]);
421 }
422
423 #[tokio::test]
424 async fn flush_drains_the_whole_stack() {
425 let bottom = SharedSink::new();
426 let mut sink = Tier::new(
427 MemStore::new(),
428 FlushPolicy::hourly(),
429 Tier::new(MemStore::new(), FlushPolicy::hourly(), bottom.clone()),
430 );
431
432 sink.ingest(&meta_at("p", 10), vec![1, 2, 3]).await.unwrap();
433 assert!(bottom.batches().is_empty());
434
435 sink.flush().await.unwrap();
436 assert_eq!(bottom.batches()[0].1, vec![1, 2, 3]);
437 assert!(bottom.flushed());
438 }
439
440 #[tokio::test]
441 async fn poison_window_does_not_block_newer_windows() {
442 let inner = SharedSink::new();
443 let armed = Arc::new(AtomicBool::new(true));
444 let mut sink = Tier::new(
445 MemStore::new(),
446 FlushPolicy::every(Duration::from_secs(300)),
447 PoisonSink {
448 inner: inner.clone(),
449 armed: Arc::clone(&armed),
450 },
451 );
452
453 sink.ingest(&meta_at("p", 10), vec![13]).await.unwrap();
455 assert!(sink.ingest(&meta_at("p", 310), vec![2]).await.is_err());
456 assert!(inner.batches().is_empty());
457
458 assert!(sink.ingest(&meta_at("p", 610), vec![3]).await.is_err());
461 let batches = inner.batches();
462 assert_eq!(batches.len(), 1);
463 assert_eq!(batches[0].0.start.unix_timestamp(), 300);
464 assert_eq!(batches[0].1, vec![2]);
465
466 armed.store(false, Ordering::SeqCst);
469 sink.flush().await.unwrap();
470 let batches = inner.batches();
471 assert_eq!(batches.len(), 3);
472 assert_eq!(batches[1].0.start.unix_timestamp(), 0);
473 assert_eq!(batches[1].1, vec![13]);
474 assert_eq!(batches[2].0.start.unix_timestamp(), 600);
475 assert_eq!(batches[2].1, vec![3]);
476 }
477
478 #[tokio::test]
479 async fn outage_retains_all_windows_and_recovers_in_order() {
480 let inner = SharedSink::new();
481 let mut sink = Tier::new(
482 MemStore::new(),
483 FlushPolicy::every(Duration::from_secs(300)),
484 inner.clone(),
485 );
486
487 sink.ingest(&meta_at("p", 10), vec![1]).await.unwrap();
488 inner.set_fail(true);
489 assert!(sink.ingest(&meta_at("p", 310), vec![2]).await.is_err());
490 assert!(sink.ingest(&meta_at("p", 610), vec![3]).await.is_err());
491 assert!(inner.batches().is_empty());
492
493 inner.set_fail(false);
494 sink.flush().await.unwrap();
495 let starts = inner
496 .batches()
497 .iter()
498 .map(|(meta, records)| (meta.start.unix_timestamp(), records.clone()))
499 .collect::<Vec<_>>();
500 assert_eq!(starts, vec![(0, vec![1]), (300, vec![2]), (600, vec![3])]);
501 }
502}