1use std::error;
8use std::path;
9use std::time::Duration;
10
11use ::time::OffsetDateTime;
12use tokio::time::Instant;
13use tracing::debug;
14
15use crate::sink::{Sink, WindowMeta};
16
17mod disk;
18
19pub use disk::{DiskSpool, SpoolError};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct FlushPolicy {
27 pub every: Duration,
29 pub max_records: usize,
32}
33
34impl FlushPolicy {
35 #[must_use]
36 pub const fn new(every: Duration, max_records: usize) -> Self {
37 Self { every, max_records }
38 }
39
40 #[must_use]
42 pub const fn every(every: Duration) -> Self {
43 Self::new(every, usize::MAX)
44 }
45
46 #[must_use]
48 pub const fn hourly() -> Self {
49 Self::every(Duration::from_secs(3600))
50 }
51}
52
53pub trait SinkExt<R>: Sink<R> + Sized {
59 #[must_use]
61 fn buffered(self, policy: FlushPolicy) -> Buffered<R, Self> {
62 Buffered::new(policy, self)
63 }
64
65 #[must_use]
71 fn spooled(self, dir: impl Into<path::PathBuf>, policy: FlushPolicy) -> DiskSpool<R, Self> {
72 DiskSpool::new(dir, policy, self)
73 }
74
75 #[must_use]
77 fn tee<B: Sink<R>>(self, other: B) -> Tee<Self, B> {
78 Tee(self, other)
79 }
80}
81
82impl<R, S: Sink<R>> SinkExt<R> for S {}
83
84pub struct Buffered<R, S> {
96 buf: Vec<R>,
97 window: Option<Window>,
98 policy: FlushPolicy,
99 inner: S,
100}
101
102#[derive(Debug, Clone)]
103struct Window {
104 pipeline: String,
105 start: OffsetDateTime,
106 opened_at: Instant,
107}
108
109impl<R, S> Buffered<R, S> {
110 #[must_use]
111 pub fn new(policy: FlushPolicy, inner: S) -> Self {
112 Self {
113 buf: vec![],
114 window: None,
115 policy,
116 inner,
117 }
118 }
119
120 #[must_use]
122 pub fn inner(&self) -> &S {
123 &self.inner
124 }
125}
126
127impl<R, S> Buffered<R, S>
128where
129 R: Clone + Send + 'static,
130 S: Sink<R>,
131{
132 async fn drain(&mut self) -> Result<(), S::Error> {
133 let Some(window) = &self.window else {
134 return Ok(());
135 };
136 let meta = WindowMeta {
137 pipeline: window.pipeline.clone(),
138 start: window.start,
139 end: OffsetDateTime::now_utc(),
140 };
141 debug!(
142 pipeline = %meta.pipeline,
143 records = self.buf.len(),
144 "buffered tier draining downstream"
145 );
146 self.inner.ingest(&meta, self.buf.clone()).await?;
147 self.buf.clear();
148 self.window = None;
149 Ok(())
150 }
151
152 fn should_fire(&self) -> bool {
153 let Some(window) = &self.window else {
154 return false;
155 };
156 self.buf.len() >= self.policy.max_records || window.opened_at.elapsed() >= self.policy.every
157 }
158}
159
160impl<R, S> Sink<R> for Buffered<R, S>
161where
162 R: Clone + Send + 'static,
163 S: Sink<R>,
164{
165 type Error = S::Error;
166
167 async fn ingest(&mut self, meta: &WindowMeta, records: Vec<R>) -> Result<(), Self::Error> {
168 if self.window.is_none() {
169 self.window = Some(Window {
170 pipeline: meta.pipeline.clone(),
171 start: meta.start,
172 opened_at: Instant::now(),
173 });
174 }
175 self.buf.extend(records);
176 if self.should_fire() {
177 self.drain().await?;
178 }
179 Ok(())
180 }
181
182 async fn flush(&mut self) -> Result<(), Self::Error> {
183 self.drain().await?;
184 self.inner.flush().await
185 }
186}
187
188pub struct Tee<A, B>(pub A, pub B);
193
194#[derive(Debug, thiserror::Error)]
196pub enum TeeError<A, B>
197where
198 A: error::Error + Send + Sync + 'static,
199 B: error::Error + Send + Sync + 'static,
200{
201 #[error("tee: first sink failed: {0}")]
202 First(#[source] A),
203 #[error("tee: second sink failed: {0}")]
204 Second(#[source] B),
205 #[error("tee: both sinks failed: first: {first}; second: {second}")]
206 Both { first: A, second: B },
207}
208
209impl<R, A, B> Sink<R> for Tee<A, B>
210where
211 R: Clone + Send + 'static,
212 A: Sink<R>,
213 B: Sink<R>,
214{
215 type Error = TeeError<A::Error, B::Error>;
216
217 async fn ingest(&mut self, meta: &WindowMeta, records: Vec<R>) -> Result<(), Self::Error> {
218 let first = self.0.ingest(meta, records.clone()).await;
219 let second = self.1.ingest(meta, records).await;
220 match (first, second) {
221 (Ok(()), Ok(())) => Ok(()),
222 (Err(a), Ok(())) => Err(TeeError::First(a)),
223 (Ok(()), Err(b)) => Err(TeeError::Second(b)),
224 (Err(a), Err(b)) => Err(TeeError::Both {
225 first: a,
226 second: b,
227 }),
228 }
229 }
230
231 async fn flush(&mut self) -> Result<(), Self::Error> {
232 let first = self.0.flush().await;
233 let second = self.1.flush().await;
234 match (first, second) {
235 (Ok(()), Ok(())) => Ok(()),
236 (Err(a), Ok(())) => Err(TeeError::First(a)),
237 (Ok(()), Err(b)) => Err(TeeError::Second(b)),
238 (Err(a), Err(b)) => Err(TeeError::Both {
239 first: a,
240 second: b,
241 }),
242 }
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::test_util::{SharedSink, meta};
250 use tokio::time;
251
252 #[tokio::test]
253 async fn buffered_holds_until_max_records() {
254 let inner = SharedSink::new();
255 let mut sink = inner
256 .clone()
257 .buffered(FlushPolicy::new(Duration::from_secs(3600), 3));
258
259 sink.ingest(&meta("p"), vec![1, 2]).await.unwrap();
260 assert!(inner.batches().is_empty());
261
262 sink.ingest(&meta("p"), vec![3]).await.unwrap();
263 let batches = inner.batches();
264 assert_eq!(batches.len(), 1);
265 assert_eq!(batches[0].1, vec![1, 2, 3]);
266 }
267
268 #[tokio::test(start_paused = true)]
269 async fn buffered_fires_on_elapsed_window() {
270 let inner = SharedSink::new();
271 let mut sink = inner
272 .clone()
273 .buffered(FlushPolicy::every(Duration::from_secs(300)));
274
275 sink.ingest(&meta("p"), vec![1]).await.unwrap();
276 assert!(inner.batches().is_empty());
277
278 time::advance(Duration::from_secs(301)).await;
279 sink.ingest(&meta("p"), vec![2]).await.unwrap();
280
281 let batches = inner.batches();
282 assert_eq!(batches.len(), 1);
283 assert_eq!(batches[0].1, vec![1, 2]);
284 }
285
286 #[tokio::test]
287 async fn buffered_retains_records_across_failing_downstream() {
288 let inner = SharedSink::new();
289 let mut sink = inner
290 .clone()
291 .buffered(FlushPolicy::new(Duration::from_secs(3600), 2));
292
293 inner.set_fail(true);
294 assert!(sink.ingest(&meta("p"), vec![1, 2]).await.is_err());
295 assert!(inner.batches().is_empty());
296
297 inner.set_fail(false);
298 sink.ingest(&meta("p"), vec![3]).await.unwrap();
299 let batches = inner.batches();
300 assert_eq!(batches.len(), 1);
301 assert_eq!(batches[0].1, vec![1, 2, 3]);
302 }
303
304 #[tokio::test]
305 async fn tee_fans_out_to_both_branches() {
306 let a = SharedSink::new();
307 let b = SharedSink::new();
308 let mut sink = a.clone().tee(b.clone());
309
310 sink.ingest(&meta("p"), vec![1, 2]).await.unwrap();
311 assert_eq!(a.batches()[0].1, vec![1, 2]);
312 assert_eq!(b.batches()[0].1, vec![1, 2]);
313 }
314
315 #[tokio::test]
316 async fn tee_reports_failing_branch_but_feeds_the_other() {
317 let a = SharedSink::new();
318 let b = SharedSink::new();
319 a.set_fail(true);
320 let mut sink = a.clone().tee(b.clone());
321
322 let err = sink.ingest(&meta("p"), vec![1]).await.unwrap_err();
323 assert!(matches!(err, TeeError::First(_)));
324 assert_eq!(b.batches().len(), 1);
325 }
326
327 #[tokio::test]
328 async fn flush_drains_the_whole_stack() {
329 let bottom = SharedSink::new();
330 let mut sink = bottom
331 .clone()
332 .buffered(FlushPolicy::hourly())
333 .buffered(FlushPolicy::hourly());
334
335 sink.ingest(&meta("p"), vec![1, 2, 3]).await.unwrap();
336 assert!(bottom.batches().is_empty());
337
338 sink.flush().await.unwrap();
339 assert_eq!(bottom.batches()[0].1, vec![1, 2, 3]);
340 assert!(bottom.flushed());
341 }
342}