saluki_components/encoders/buffered_incremental/
mod.rs

1use std::time::Duration;
2
3use agent_data_plane_config::defaults::DEFAULT_ENCODER_FLUSH_TIMEOUT;
4use async_trait::async_trait;
5use saluki_common::task::HandleExt as _;
6use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
7use saluki_core::{
8    components::{encoders::*, ComponentContext},
9    data_model::{event::EventType, payload::PayloadType},
10    observability::ComponentMetricsExt,
11};
12use saluki_error::GenericError;
13use saluki_metrics::MetricsBuilder;
14use tokio::{pin, select, time::sleep};
15use tracing::{debug, error};
16
17mod telemetry;
18use self::telemetry::ComponentTelemetry;
19
20/// Buffered incremental encoder.
21///
22/// Wraps an `IncrementalEncoder` and drives it with incoming events, allowing buffering by utilizing a configurable
23/// flush timeout. Payloads are encoded on the global thread pool to avoid affecting latency-sensitive tasks.
24pub struct BufferedIncrementalConfiguration<EB> {
25    /// Flush timeout for pending requests.
26    ///
27    /// When the encoder has written events to the in-flight request payload, but it hasn't yet reached the
28    /// payload size limits that would force the payload to be flushed, the encoder will wait for a period of time
29    /// before flushing the in-flight request payload. This allows for the possibility of other events to be processed
30    /// and written into the request payload, thereby maximizing the payload size and reducing the number of requests
31    /// generated and sent overall.
32    ///
33    /// Defaults to 2 seconds.
34    flush_timeout: Duration,
35
36    encoder_builder: EB,
37}
38
39impl<EB> BufferedIncrementalConfiguration<EB>
40where
41    EB: IncrementalEncoderBuilder,
42{
43    /// Creates a new `BufferedIncrementalConfiguration` from the given incremental encoder builder.
44    pub fn from_encoder_builder(encoder_builder: EB) -> Self {
45        Self {
46            flush_timeout: DEFAULT_ENCODER_FLUSH_TIMEOUT,
47            encoder_builder,
48        }
49    }
50}
51
52#[async_trait]
53impl<EB> EncoderBuilder for BufferedIncrementalConfiguration<EB>
54where
55    EB: IncrementalEncoderBuilder + Sync,
56    EB::Output: Send + 'static,
57{
58    fn input_event_type(&self) -> EventType {
59        self.encoder_builder.input_event_type()
60    }
61
62    fn output_payload_type(&self) -> PayloadType {
63        self.encoder_builder.output_payload_type()
64    }
65
66    async fn build(&self, context: ComponentContext) -> Result<Box<dyn Encoder + Send>, GenericError> {
67        let metrics_builder = MetricsBuilder::from_component_context(&context);
68        let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
69
70        let encoder = self.encoder_builder.build(context).await?;
71
72        let flush_timeout = match self.flush_timeout {
73            // We always give ourselves a minimum flush timeout of 10ms to allow for some very minimal amount of
74            // batching, while still practically flushing things almost immediately.
75            Duration::ZERO => Duration::from_millis(10),
76            dur => dur,
77        };
78
79        Ok(Box::new(BufferedIncremental {
80            encoder,
81            telemetry,
82            flush_timeout,
83        }))
84    }
85}
86
87impl<EB> MemoryBounds for BufferedIncrementalConfiguration<EB>
88where
89    EB: IncrementalEncoderBuilder,
90{
91    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
92        builder
93            .minimum()
94            .with_single_value::<BufferedIncremental<EB::Output>>("component struct");
95
96        self.encoder_builder.specify_bounds(builder);
97    }
98}
99
100pub struct BufferedIncremental<E> {
101    encoder: E,
102    telemetry: ComponentTelemetry,
103    flush_timeout: Duration,
104}
105
106#[async_trait]
107impl<E> Encoder for BufferedIncremental<E>
108where
109    E: IncrementalEncoder + Send + 'static,
110{
111    async fn run(mut self: Box<Self>, context: EncoderContext) -> Result<(), GenericError> {
112        let Self {
113            encoder,
114            telemetry,
115            flush_timeout,
116        } = *self;
117
118        // Spawn our background incremental encoder task.
119        let thread_pool_handle = context.topology_context().global_thread_pool().clone();
120        let runner_name = format!(
121            "{}-incremental-encoder",
122            context.component_context().component_id().replace("_", "-")
123        );
124        let runner = run_incremental_encoder(context, encoder, telemetry, flush_timeout);
125        let runner_handle = thread_pool_handle.spawn_traced_named(runner_name, runner);
126
127        debug!("Buffered Incremental encoder started.");
128
129        // Simply wait for the runner to finish.
130        //
131        // It handles all of the health checking, event consuming, encoding, dispatching, etc.
132        match runner_handle.await {
133            Ok(Ok(())) => debug!("Incremental encoder task stopped."),
134            Ok(Err(e)) => error!(error = %e, "Incremental encoder task failed."),
135            Err(e) => error!(error = %e, "Incremental encoder task panicked."),
136        }
137
138        debug!("Buffered Incremental encoder stopped.");
139
140        Ok(())
141    }
142}
143
144async fn run_incremental_encoder<E>(
145    mut context: EncoderContext, mut encoder: E, telemetry: ComponentTelemetry, flush_timeout: Duration,
146) -> Result<(), GenericError>
147where
148    E: IncrementalEncoder,
149{
150    let mut health = context.take_health_handle();
151
152    health.mark_ready();
153
154    let mut pending_flush = false;
155    let pending_flush_timeout = sleep(flush_timeout);
156    pin!(pending_flush_timeout);
157
158    loop {
159        select! {
160            _ = health.live() => continue,
161            maybe_event_buffer = context.events().next() => {
162                // Break out of our loop when the events channel is closed.
163                let event_buffer = match maybe_event_buffer {
164                    Some(event_buffer) => event_buffer,
165                    None => break,
166                };
167
168                for event in event_buffer {
169                    // Try to process the event.
170                    //
171                    // If we're informed that we need to flush, we'll hold on to this event before triggering a flush and then
172                    // retry processing it after flushing.
173                    let event_to_retry = match encoder.process_event(event).await? {
174                        ProcessResult::Continue => continue,
175                        ProcessResult::FlushRequired(event) => event,
176                    };
177
178                    // Flush the encoder, waiting any payloads it has generated.
179                    encoder.flush(context.dispatcher()).await?;
180
181                    // Now try to process the event again.
182                    //
183                    // If this fails, then we drop the event because it's a logical bug to not be able to encode an event after
184                    // flushing, and we don't want to get stuck in an infinite loop.
185                    match encoder.process_event(event_to_retry).await? {
186                        ProcessResult::Continue => {},
187                        ProcessResult::FlushRequired(_) => {
188                            error!("Failed to process event after flushing.");
189                            telemetry.events_dropped_encoder().increment(1);
190                        },
191                    }
192                }
193
194                debug!("Processed event buffer.");
195
196                // If we're not already pending a flush, we'll start the countdown.
197                if !pending_flush {
198                    pending_flush_timeout.as_mut().reset(tokio::time::Instant::now() + flush_timeout);
199                    pending_flush = true;
200                }
201            },
202            _ = &mut pending_flush_timeout, if pending_flush => {
203                debug!("Flushing encoder of any pending payload(s).");
204
205                pending_flush = false;
206
207                encoder.flush(context.dispatcher()).await?;
208
209                debug!("All pending payloads flushed.");
210            }
211        }
212    }
213
214    // Do a final flush since we may have had a pending payloads before breaking out of the loop.
215    encoder.flush(context.dispatcher()).await?;
216
217    Ok(())
218}