Skip to main content

vector/sinks/util/
batch.rs

1use std::{marker::PhantomData, num::NonZeroUsize, time::Duration};
2
3use derivative::Derivative;
4use serde_with::serde_as;
5use snafu::Snafu;
6use vector_lib::{
7    configurable::configurable_component, json_size::JsonSize, stream::BatcherSettings,
8};
9
10use super::EncodedEvent;
11use crate::{event::EventFinalizers, internal_events::LargeEventDroppedError};
12
13// * Provide sensible sink default 10 MB with 1s timeout. Don't allow chaining builder methods on
14//   that.
15
16#[derive(Debug, Snafu, PartialEq, Eq)]
17pub enum BatchError {
18    #[snafu(display("This sink does not allow setting `max_bytes`"))]
19    BytesNotAllowed,
20    #[snafu(display("`max_bytes` must be greater than zero"))]
21    InvalidMaxBytes,
22    #[snafu(display("`max_events` must be greater than zero"))]
23    InvalidMaxEvents,
24    #[snafu(display("`timeout_secs` must be greater than zero"))]
25    InvalidTimeout,
26    #[snafu(display("provided `max_bytes` exceeds the maximum limit of {}", limit))]
27    MaxBytesExceeded { limit: usize },
28    #[snafu(display("provided `max_events` exceeds the maximum limit of {}", limit))]
29    MaxEventsExceeded { limit: usize },
30}
31
32pub trait SinkBatchSettings {
33    const MAX_EVENTS: Option<usize>;
34    const MAX_BYTES: Option<usize>;
35    const TIMEOUT_SECS: f64;
36}
37
38/// Reasonable default batch settings for sinks with timeliness concerns, limited by event count.
39#[derive(Clone, Copy, Debug, Default)]
40pub struct RealtimeEventBasedDefaultBatchSettings;
41
42impl SinkBatchSettings for RealtimeEventBasedDefaultBatchSettings {
43    const MAX_EVENTS: Option<usize> = Some(1000);
44    const MAX_BYTES: Option<usize> = None;
45    const TIMEOUT_SECS: f64 = 1.0;
46}
47
48/// Reasonable default batch settings for sinks with timeliness concerns, limited by byte size.
49#[derive(Clone, Copy, Debug, Default)]
50pub struct RealtimeSizeBasedDefaultBatchSettings;
51
52impl SinkBatchSettings for RealtimeSizeBasedDefaultBatchSettings {
53    const MAX_EVENTS: Option<usize> = None;
54    const MAX_BYTES: Option<usize> = Some(10_000_000);
55    const TIMEOUT_SECS: f64 = 1.0;
56}
57
58/// Reasonable default batch settings for sinks focused on shipping fewer-but-larger batches,
59/// limited by byte size.
60#[derive(Clone, Copy, Debug, Default)]
61pub struct BulkSizeBasedDefaultBatchSettings;
62
63impl SinkBatchSettings for BulkSizeBasedDefaultBatchSettings {
64    const MAX_EVENTS: Option<usize> = None;
65    const MAX_BYTES: Option<usize> = Some(10_000_000);
66    const TIMEOUT_SECS: f64 = 300.0;
67}
68
69/// "Default" batch settings when a sink handles batch settings entirely on its own.
70///
71/// This has very few usages, but can be notably seen in the Kafka sink, where the values are used
72/// to configure `librdkafka` itself rather than being passed as `BatchSettings`/`BatcherSettings`
73/// to components in the sink itself.
74#[derive(Clone, Copy, Debug, Default)]
75pub struct NoDefaultsBatchSettings;
76
77impl SinkBatchSettings for NoDefaultsBatchSettings {
78    const MAX_EVENTS: Option<usize> = None;
79    const MAX_BYTES: Option<usize> = None;
80    const TIMEOUT_SECS: f64 = 1.0;
81}
82
83#[derive(Clone, Copy, Debug, Default)]
84pub struct Merged;
85
86#[derive(Clone, Copy, Debug, Default)]
87pub struct Unmerged;
88
89/// Event batching behavior.
90// NOTE: the default values are extracted from the consts in `D`. This generates correct defaults
91// in automatic cue docs generation. Implementations of `SinkBatchSettings` should not specify
92// defaults, since that is satisfied here.
93#[serde_as]
94#[configurable_component]
95#[derive(Clone, Copy, Debug, Default)]
96pub struct BatchConfig<D: SinkBatchSettings + Clone, S = Unmerged>
97where
98    S: Clone,
99{
100    /// The maximum size of a batch that is processed by a sink.
101    ///
102    /// This is based on the uncompressed size of the batched events, before they are
103    /// serialized or compressed.
104    #[serde(default = "default_max_bytes::<D>")]
105    #[configurable(metadata(docs::type_unit = "bytes"))]
106    pub max_bytes: Option<usize>,
107
108    /// The maximum size of a batch before it is flushed.
109    #[serde(default = "default_max_events::<D>")]
110    #[configurable(metadata(docs::type_unit = "events"))]
111    pub max_events: Option<usize>,
112
113    /// The maximum age of a batch before it is flushed.
114    #[serde(default = "default_timeout::<D>")]
115    #[configurable(metadata(docs::type_unit = "seconds"))]
116    #[configurable(metadata(docs::human_name = "Timeout"))]
117    pub timeout_secs: Option<f64>,
118
119    #[serde(skip)]
120    _d: PhantomData<D>,
121    #[serde(skip)]
122    _s: PhantomData<S>,
123}
124
125const fn default_max_bytes<D: SinkBatchSettings>() -> Option<usize> {
126    D::MAX_BYTES
127}
128
129const fn default_max_events<D: SinkBatchSettings>() -> Option<usize> {
130    D::MAX_EVENTS
131}
132
133const fn default_timeout<D: SinkBatchSettings>() -> Option<f64> {
134    Some(D::TIMEOUT_SECS)
135}
136
137impl<D: SinkBatchSettings + Clone> BatchConfig<D, Unmerged> {
138    pub fn validate(self) -> Result<BatchConfig<D, Merged>, BatchError> {
139        let config = BatchConfig {
140            max_bytes: self.max_bytes.or(D::MAX_BYTES),
141            max_events: self.max_events.or(D::MAX_EVENTS),
142            timeout_secs: self.timeout_secs.or(Some(D::TIMEOUT_SECS)),
143            _d: PhantomData,
144            _s: PhantomData,
145        };
146
147        match (config.max_bytes, config.max_events, config.timeout_secs) {
148            // TODO: what logic do we want to check that we have the minimum number of settings?
149            // for example, we always assert that timeout_secs from D is greater than zero, but
150            // technically we could end up with max bytes or max events being none, since we just
151            // chain options... but asserting that they're set isn't really doable either, because
152            // you dont always set both of those fields, etc..
153            (Some(0), _, _) => Err(BatchError::InvalidMaxBytes),
154            (_, Some(0), _) => Err(BatchError::InvalidMaxEvents),
155            (_, _, Some(timeout)) if timeout <= 0.0 => Err(BatchError::InvalidTimeout),
156
157            _ => Ok(config),
158        }
159    }
160
161    pub fn into_batch_settings<T: Batch>(self) -> Result<BatchSettings<T>, BatchError> {
162        let config = self.validate()?;
163        config.into_batch_settings()
164    }
165
166    /// Converts these settings into [`BatcherSettings`].
167    ///
168    /// `BatcherSettings` is effectively the `vector_core` spiritual successor of
169    /// [`BatchSettings<B>`].  Once all sinks are rewritten in the new stream-based style and we can
170    /// eschew customized batch buffer types, we can de-genericify `BatchSettings` and move it into
171    /// `vector_core`, and use that instead of `BatcherSettings`.
172    pub fn into_batcher_settings(self) -> Result<BatcherSettings, BatchError> {
173        let config = self.validate()?;
174        config.into_batcher_settings()
175    }
176}
177
178impl<D: SinkBatchSettings + Clone> BatchConfig<D, Merged> {
179    pub const fn validate(self) -> Result<BatchConfig<D, Merged>, BatchError> {
180        Ok(self)
181    }
182
183    pub const fn disallow_max_bytes(self) -> Result<Self, BatchError> {
184        // Sinks that used `max_size` for an event count cannot count
185        // bytes, so err if `max_bytes` is set.
186        match self.max_bytes {
187            Some(_) => Err(BatchError::BytesNotAllowed),
188            None => Ok(self),
189        }
190    }
191
192    pub const fn limit_max_bytes(self, limit: usize) -> Result<Self, BatchError> {
193        match self.max_bytes {
194            Some(n) if n > limit => Err(BatchError::MaxBytesExceeded { limit }),
195            _ => Ok(self),
196        }
197    }
198
199    pub const fn limit_max_events(self, limit: usize) -> Result<Self, BatchError> {
200        match self.max_events {
201            Some(n) if n > limit => Err(BatchError::MaxEventsExceeded { limit }),
202            _ => Ok(self),
203        }
204    }
205
206    pub fn into_batch_settings<T: Batch>(self) -> Result<BatchSettings<T>, BatchError> {
207        let adjusted = T::get_settings_defaults(self)?;
208
209        // This is unfortunate since we technically have already made sure this isn't possible in
210        // `validate`, but alas.
211        let timeout_secs = adjusted.timeout_secs.ok_or(BatchError::InvalidTimeout)?;
212
213        Ok(BatchSettings {
214            size: BatchSize {
215                bytes: adjusted.max_bytes.unwrap_or(usize::MAX),
216                events: adjusted.max_events.unwrap_or(usize::MAX),
217                _type_marker: PhantomData,
218            },
219            timeout: Duration::from_secs_f64(timeout_secs),
220        })
221    }
222
223    /// Converts these settings into [`BatcherSettings`].
224    ///
225    /// `BatcherSettings` is effectively the `vector_core` spiritual successor of
226    /// [`BatchSettings<B>`].  Once all sinks are rewritten in the new stream-based style and we can
227    /// eschew customized batch buffer types, we can de-genericify `BatchSettings` and move it into
228    /// `vector_core`, and use that instead of `BatcherSettings`.
229    pub fn into_batcher_settings(self) -> Result<BatcherSettings, BatchError> {
230        let max_bytes = self
231            .max_bytes
232            .and_then(NonZeroUsize::new)
233            .or_else(|| NonZeroUsize::new(usize::MAX))
234            .expect("`max_bytes` should already be validated");
235
236        let max_events = self
237            .max_events
238            .and_then(NonZeroUsize::new)
239            .or_else(|| NonZeroUsize::new(usize::MAX))
240            .expect("`max_bytes` should already be validated");
241
242        // This is unfortunate since we technically have already made sure this isn't possible in
243        // `validate`, but alas.
244        let timeout_secs = self.timeout_secs.ok_or(BatchError::InvalidTimeout)?;
245
246        Ok(BatcherSettings::new(
247            Duration::from_secs_f64(timeout_secs),
248            max_bytes,
249            max_events,
250        ))
251    }
252}
253
254// Going from a merged to unmerged configuration is fine, because we know it already had to have
255// been validated/limited.
256impl<D1, D2> From<BatchConfig<D1, Merged>> for BatchConfig<D2, Unmerged>
257where
258    D1: SinkBatchSettings + Clone,
259    D2: SinkBatchSettings + Clone,
260{
261    fn from(config: BatchConfig<D1, Merged>) -> Self {
262        BatchConfig {
263            max_bytes: config.max_bytes,
264            max_events: config.max_events,
265            timeout_secs: config.timeout_secs,
266            _d: PhantomData,
267            _s: PhantomData,
268        }
269    }
270}
271
272#[derive(Debug, Derivative)]
273#[derivative(Clone(bound = ""))]
274#[derivative(Copy(bound = ""))]
275pub struct BatchSize<B> {
276    pub bytes: usize,
277    pub events: usize,
278    // This type marker is used to drive type inference, which allows us
279    // to call the right Batch::get_settings_defaults without explicitly
280    // naming the type in BatchSettings::parse_config.
281    _type_marker: PhantomData<B>,
282}
283
284impl<B> BatchSize<B> {
285    pub const fn const_default() -> Self {
286        BatchSize {
287            bytes: usize::MAX,
288            events: usize::MAX,
289            _type_marker: PhantomData,
290        }
291    }
292}
293
294impl<B> Default for BatchSize<B> {
295    fn default() -> Self {
296        BatchSize::const_default()
297    }
298}
299
300#[derive(Debug, Derivative)]
301#[derivative(Clone(bound = ""))]
302#[derivative(Copy(bound = ""))]
303pub struct BatchSettings<B> {
304    pub size: BatchSize<B>,
305    pub timeout: Duration,
306}
307
308impl<B> Default for BatchSettings<B> {
309    fn default() -> Self {
310        BatchSettings {
311            size: BatchSize {
312                bytes: 10_000_000,
313                events: usize::MAX,
314                _type_marker: PhantomData,
315            },
316            timeout: Duration::from_secs(1),
317        }
318    }
319}
320
321pub(super) fn err_event_too_large<T>(length: usize, max_length: usize) -> PushResult<T> {
322    emit!(LargeEventDroppedError { length, max_length });
323    PushResult::Ok(false)
324}
325
326/// This enum provides the result of a push operation, indicating if the
327/// event was added and the fullness state of the buffer.
328#[must_use]
329#[derive(Debug, Eq, PartialEq)]
330pub enum PushResult<T> {
331    /// Event was added, with an indicator if the buffer is now full
332    Ok(bool),
333    /// Event could not be added because it would overflow the
334    /// buffer. Since push takes ownership of the event, it must be
335    /// returned here.
336    Overflow(T),
337}
338
339pub trait Batch: Sized {
340    type Input;
341    type Output;
342
343    /// Turn the batch configuration into an actualized set of settings,
344    /// and deal with the proper behavior of `max_size` and if
345    /// `max_bytes` may be set. This is in the trait to ensure all batch
346    /// buffers implement it.
347    fn get_settings_defaults<D: SinkBatchSettings + Clone>(
348        config: BatchConfig<D, Merged>,
349    ) -> Result<BatchConfig<D, Merged>, BatchError> {
350        Ok(config)
351    }
352
353    fn push(&mut self, item: Self::Input) -> PushResult<Self::Input>;
354    fn is_empty(&self) -> bool;
355    fn fresh(&self) -> Self;
356    fn finish(self) -> Self::Output;
357    fn num_items(&self) -> usize;
358}
359
360#[derive(Debug)]
361pub struct EncodedBatch<I> {
362    pub items: I,
363    pub finalizers: EventFinalizers,
364    pub count: usize,
365    pub byte_size: usize,
366    pub json_byte_size: JsonSize,
367}
368
369/// This is a batch construct that stores an set of event finalizers alongside the batch itself.
370#[derive(Clone, Debug)]
371pub struct FinalizersBatch<B> {
372    inner: B,
373    finalizers: EventFinalizers,
374    // The count of items inserted into this batch is distinct from the
375    // number of items recorded by the inner batch, as that inner count
376    // could be smaller due to aggregated items (ie metrics).
377    count: usize,
378    byte_size: usize,
379    json_byte_size: JsonSize,
380}
381
382impl<B: Batch> From<B> for FinalizersBatch<B> {
383    fn from(inner: B) -> Self {
384        Self {
385            inner,
386            finalizers: Default::default(),
387            count: 0,
388            byte_size: 0,
389            json_byte_size: JsonSize::zero(),
390        }
391    }
392}
393
394impl<B: Batch> Batch for FinalizersBatch<B> {
395    type Input = EncodedEvent<B::Input>;
396    type Output = EncodedBatch<B::Output>;
397
398    fn get_settings_defaults<D: SinkBatchSettings + Clone>(
399        config: BatchConfig<D, Merged>,
400    ) -> Result<BatchConfig<D, Merged>, BatchError> {
401        B::get_settings_defaults(config)
402    }
403
404    fn push(&mut self, item: Self::Input) -> PushResult<Self::Input> {
405        let EncodedEvent {
406            item,
407            finalizers,
408            byte_size,
409            json_byte_size,
410        } = item;
411        match self.inner.push(item) {
412            PushResult::Ok(full) => {
413                self.finalizers.merge(finalizers);
414                self.count += 1;
415                self.byte_size += byte_size;
416                self.json_byte_size += json_byte_size;
417                PushResult::Ok(full)
418            }
419            PushResult::Overflow(item) => PushResult::Overflow(EncodedEvent {
420                item,
421                finalizers,
422                byte_size,
423                json_byte_size,
424            }),
425        }
426    }
427
428    fn is_empty(&self) -> bool {
429        self.inner.is_empty()
430    }
431
432    fn fresh(&self) -> Self {
433        Self {
434            inner: self.inner.fresh(),
435            finalizers: Default::default(),
436            count: 0,
437            byte_size: 0,
438            json_byte_size: JsonSize::zero(),
439        }
440    }
441
442    fn finish(self) -> Self::Output {
443        EncodedBatch {
444            items: self.inner.finish(),
445            finalizers: self.finalizers,
446            count: self.count,
447            byte_size: self.byte_size,
448            json_byte_size: self.json_byte_size,
449        }
450    }
451
452    fn num_items(&self) -> usize {
453        self.inner.num_items()
454    }
455}
456
457#[derive(Clone, Debug)]
458pub struct StatefulBatch<B> {
459    inner: B,
460    was_full: bool,
461}
462
463impl<B: Batch> From<B> for StatefulBatch<B> {
464    fn from(inner: B) -> Self {
465        Self {
466            inner,
467            was_full: false,
468        }
469    }
470}
471
472impl<B> StatefulBatch<B> {
473    pub const fn was_full(&self) -> bool {
474        self.was_full
475    }
476
477    #[allow(clippy::missing_const_for_fn)] // const cannot run destructor
478    pub fn into_inner(self) -> B {
479        self.inner
480    }
481}
482
483impl<B: Batch> Batch for StatefulBatch<B> {
484    type Input = B::Input;
485    type Output = B::Output;
486
487    fn get_settings_defaults<D: SinkBatchSettings + Clone>(
488        config: BatchConfig<D, Merged>,
489    ) -> Result<BatchConfig<D, Merged>, BatchError> {
490        B::get_settings_defaults(config)
491    }
492
493    fn push(&mut self, item: Self::Input) -> PushResult<Self::Input> {
494        if self.was_full {
495            PushResult::Overflow(item)
496        } else {
497            let result = self.inner.push(item);
498            self.was_full =
499                matches!(result, PushResult::Overflow(_)) || matches!(result, PushResult::Ok(true));
500            result
501        }
502    }
503
504    fn is_empty(&self) -> bool {
505        !self.was_full && self.inner.is_empty()
506    }
507
508    fn fresh(&self) -> Self {
509        Self {
510            inner: self.inner.fresh(),
511            was_full: false,
512        }
513    }
514
515    fn finish(self) -> Self::Output {
516        self.inner.finish()
517    }
518
519    fn num_items(&self) -> usize {
520        self.inner.num_items()
521    }
522}