Skip to main content

relay_server/services/buffer/envelope_buffer/
mod.rs

1use std::cmp::Ordering;
2use std::collections::BTreeSet;
3use std::convert::Infallible;
4use std::error::Error;
5use std::mem;
6use std::time::Duration;
7
8use chrono::{DateTime, Utc};
9use hashbrown::HashSet;
10use relay_base_schema::project::ProjectKey;
11use relay_config::ConfigSnapshot;
12use tokio::time::{Instant, timeout};
13
14use crate::envelope::Envelope;
15use crate::envelope::Item;
16use crate::services::buffer::common::ProjectKeyPair;
17use crate::services::buffer::envelope_stack::EnvelopeStack;
18use crate::services::buffer::envelope_stack::sqlite::SqliteEnvelopeStackError;
19use crate::services::buffer::envelope_store::sqlite::SqliteEnvelopeStoreError;
20use crate::services::buffer::stack_provider::memory::MemoryStackProvider;
21use crate::services::buffer::stack_provider::sqlite::SqliteStackProvider;
22use crate::services::buffer::stack_provider::{StackCreationType, StackProvider};
23use crate::statsd::{RelayDistributions, RelayGauges, RelayTimers};
24use crate::utils::MemoryChecker;
25
26/// Polymorphic envelope buffering interface.
27///
28/// The underlying buffer can either be disk-based or memory-based,
29/// depending on the given configuration.
30///
31/// NOTE: This is implemented as an enum because a trait object with async methods would not be
32/// object safe.
33#[derive(Debug)]
34#[allow(private_interfaces)]
35pub enum PolymorphicEnvelopeBuffer {
36    /// An enveloper buffer that uses in-memory envelopes stacks.
37    InMemory(EnvelopeBuffer<MemoryStackProvider>),
38    /// An enveloper buffer that uses sqlite envelopes stacks.
39    Sqlite(EnvelopeBuffer<SqliteStackProvider>),
40}
41
42impl PolymorphicEnvelopeBuffer {
43    /// Returns true if the implementation stores all envelopes in RAM.
44    pub fn is_memory(&self) -> bool {
45        match self {
46            Self::InMemory(_) => true,
47            Self::Sqlite(_) => false,
48        }
49    }
50
51    /// Creates either a memory-based or a disk-based envelope buffer,
52    /// depending on the given configuration.
53    pub async fn from_config(
54        partition_id: u8,
55        config: &ConfigSnapshot,
56        memory_checker: MemoryChecker,
57    ) -> Result<Self, EnvelopeBufferError> {
58        let buffer = if config.spool_envelopes_path(partition_id).is_some() {
59            relay_log::trace!("PolymorphicEnvelopeBuffer: initializing sqlite envelope buffer");
60            let buffer = EnvelopeBuffer::<SqliteStackProvider>::new(partition_id, config).await?;
61            Self::Sqlite(buffer)
62        } else {
63            relay_log::trace!("PolymorphicEnvelopeBuffer: initializing memory envelope buffer");
64            let buffer = EnvelopeBuffer::<MemoryStackProvider>::new(partition_id, memory_checker);
65            Self::InMemory(buffer)
66        };
67
68        Ok(buffer)
69    }
70
71    /// Initializes the envelope buffer.
72    pub async fn initialize(&mut self) {
73        match self {
74            PolymorphicEnvelopeBuffer::InMemory(buffer) => buffer.initialize().await,
75            PolymorphicEnvelopeBuffer::Sqlite(buffer) => buffer.initialize().await,
76        }
77    }
78
79    /// Adds an envelope to the buffer.
80    pub async fn push(&mut self, envelope: Box<Envelope>) -> Result<(), EnvelopeBufferError> {
81        relay_statsd::metric!(
82            distribution(RelayDistributions::BufferEnvelopeBodySize) =
83                envelope.items().map(Item::len).sum::<usize>() as u64,
84            partition_id = self.partition_tag()
85        );
86
87        relay_statsd::metric!(
88            timer(RelayTimers::BufferPush),
89            partition_id = self.partition_tag(),
90            {
91                match self {
92                    Self::Sqlite(buffer) => buffer.push(envelope).await,
93                    Self::InMemory(buffer) => buffer.push(envelope).await,
94                }
95            }
96        )
97    }
98
99    /// Returns a reference to the next-in-line envelope.
100    pub async fn peek(&mut self) -> Result<Peek, EnvelopeBufferError> {
101        relay_statsd::metric!(
102            timer(RelayTimers::BufferPeek),
103            partition_id = self.partition_tag(),
104            {
105                match self {
106                    Self::Sqlite(buffer) => buffer.peek().await,
107                    Self::InMemory(buffer) => buffer.peek().await,
108                }
109            }
110        )
111    }
112
113    /// Pops the next-in-line envelope.
114    pub async fn pop(&mut self) -> Result<Option<Box<Envelope>>, EnvelopeBufferError> {
115        relay_statsd::metric!(
116            timer(RelayTimers::BufferPop),
117            partition_id = self.partition_tag(),
118            {
119                match self {
120                    Self::Sqlite(buffer) => buffer.pop().await,
121                    Self::InMemory(buffer) => buffer.pop().await,
122                }
123            }
124        )
125    }
126
127    /// Marks a project as ready or not ready.
128    ///
129    /// The buffer re-prioritizes its envelopes based on this information.
130    /// Returns `true` if at least one priority was changed.
131    pub fn mark_ready(&mut self, project: &ProjectKey, is_ready: bool) -> bool {
132        relay_log::trace!(
133            project_key = project.as_str(),
134            "buffer marked {}",
135            if is_ready { "ready" } else { "not ready" }
136        );
137        match self {
138            Self::Sqlite(buffer) => buffer.mark_ready(project, is_ready),
139            Self::InMemory(buffer) => buffer.mark_ready(project, is_ready),
140        }
141    }
142
143    /// Marks a stack as seen.
144    ///
145    /// Non-ready stacks are deprioritized when they are marked as seen, such that
146    /// the next call to `.peek()` will look at a different stack. This prevents
147    /// head-of-line blocking.
148    pub fn mark_seen(&mut self, project_key_pair: &ProjectKeyPair, next_fetch: Duration) {
149        match self {
150            Self::Sqlite(buffer) => buffer.mark_seen(project_key_pair, next_fetch),
151            Self::InMemory(buffer) => buffer.mark_seen(project_key_pair, next_fetch),
152        }
153    }
154
155    /// Returns `true` whether the buffer has capacity to accept new [`Envelope`]s.
156    pub fn has_capacity(&self) -> bool {
157        match self {
158            Self::Sqlite(buffer) => buffer.has_capacity(),
159            Self::InMemory(buffer) => buffer.has_capacity(),
160        }
161    }
162
163    /// Returns the total number of envelopes that have been spooled since the startup. It does
164    /// not include the count that existed in a persistent spooler before.
165    pub fn item_count(&self) -> u64 {
166        match self {
167            Self::Sqlite(buffer) => buffer.tracked_count,
168            Self::InMemory(buffer) => buffer.tracked_count,
169        }
170    }
171
172    /// Returns the total number of bytes that the spooler storage uses or `None` if the number
173    /// cannot be reliably determined.
174    pub fn total_size(&self) -> Option<u64> {
175        match self {
176            Self::Sqlite(buffer) => buffer.stack_provider.total_size(),
177            Self::InMemory(buffer) => buffer.stack_provider.total_size(),
178        }
179    }
180
181    /// Shuts down the [`PolymorphicEnvelopeBuffer`].
182    pub async fn shutdown(&mut self) -> bool {
183        // Currently, we want to flush the buffer only for disk, since the in memory implementation
184        // tries to not do anything and pop as many elements as possible within the shutdown
185        // timeout.
186        match self {
187            Self::Sqlite(buffer) if !buffer.stack_provider.ephemeral() => {
188                buffer.flush().await;
189                true
190            }
191            _ => {
192                relay_log::trace!("shutdown procedure not needed");
193                false
194            }
195        }
196    }
197
198    /// Returns the partition tag for this [`PolymorphicEnvelopeBuffer`].
199    fn partition_tag(&self) -> &str {
200        match self {
201            PolymorphicEnvelopeBuffer::InMemory(buffer) => &buffer.partition_tag,
202            PolymorphicEnvelopeBuffer::Sqlite(buffer) => &buffer.partition_tag,
203        }
204    }
205}
206
207/// Error that occurs while interacting with the envelope buffer.
208#[derive(Debug, thiserror::Error)]
209pub enum EnvelopeBufferError {
210    #[error("sqlite")]
211    SqliteStore(#[from] SqliteEnvelopeStoreError),
212
213    #[error("sqlite")]
214    SqliteStack(#[from] SqliteEnvelopeStackError),
215
216    #[error("failed to push envelope to the buffer")]
217    PushFailed,
218}
219
220impl From<Infallible> for EnvelopeBufferError {
221    fn from(value: Infallible) -> Self {
222        match value {}
223    }
224}
225
226/// An envelope buffer that holds an individual stack for each project/sampling project combination.
227///
228/// Envelope stacks are organized in a priority queue, and are re-prioritized every time an envelope
229/// is pushed, popped, or when a project becomes ready.
230#[derive(Debug)]
231struct EnvelopeBuffer<P: StackProvider> {
232    /// The central priority queue.
233    priority_queue: priority_queue::PriorityQueue<QueueItem<ProjectKeyPair, P::Stack>, Priority>,
234    /// A lookup table to find all stacks involving a project.
235    stacks_by_project: hashbrown::HashMap<ProjectKey, BTreeSet<ProjectKeyPair>>,
236    /// A provider of stacks that provides utilities to create stacks, check their capacity...
237    ///
238    /// This indirection is needed because different stack implementations might need different
239    /// initialization (e.g. a database connection).
240    stack_provider: P,
241    /// The total count of envelopes that the buffer is working with.
242    ///
243    /// Note that this count is not meant to be perfectly accurate since the initialization of the
244    /// count might not succeed if it takes more than a set timeout. For example, if we load the
245    /// count of all envelopes from disk, and it takes more than the time we set, we will mark the
246    /// initial count as 0 and just count incoming and outgoing envelopes from the buffer.
247    total_count: i64,
248    /// The total count of envelopes that the buffer is working with ignoring envelopes that
249    /// were previously stored on disk.
250    ///
251    /// On startup this will always be 0 and will only count incoming envelopes. If a reliable
252    /// count of currently buffered envelopes is required, prefer this over `total_count`
253    tracked_count: u64,
254    /// Whether the count initialization succeeded or not.
255    ///
256    /// This boolean is just used for tagging the metric that tracks the total count of envelopes
257    /// in the buffer.
258    total_count_initialized: bool,
259    /// The tag value of this partition which is used for reporting purposes.
260    partition_tag: String,
261}
262
263impl EnvelopeBuffer<MemoryStackProvider> {
264    /// Creates an empty memory-based buffer.
265    pub fn new(partition_id: u8, memory_checker: MemoryChecker) -> Self {
266        Self {
267            stacks_by_project: Default::default(),
268            priority_queue: Default::default(),
269            stack_provider: MemoryStackProvider::new(memory_checker),
270            total_count: 0,
271            tracked_count: 0,
272            total_count_initialized: false,
273            partition_tag: partition_id.to_string(),
274        }
275    }
276}
277
278#[allow(dead_code)]
279impl EnvelopeBuffer<SqliteStackProvider> {
280    /// Creates an empty sqlite-based buffer.
281    pub async fn new(
282        partition_id: u8,
283        config: &ConfigSnapshot,
284    ) -> Result<Self, EnvelopeBufferError> {
285        Ok(Self {
286            stacks_by_project: Default::default(),
287            priority_queue: Default::default(),
288            stack_provider: SqliteStackProvider::new(partition_id, config).await?,
289            total_count: 0,
290            tracked_count: 0,
291            total_count_initialized: false,
292            partition_tag: partition_id.to_string(),
293        })
294    }
295}
296
297impl<P: StackProvider> EnvelopeBuffer<P>
298where
299    EnvelopeBufferError: From<<P::Stack as EnvelopeStack>::Error>,
300{
301    /// Initializes the [`EnvelopeBuffer`] given the initialization state from the
302    /// [`StackProvider`].
303    pub async fn initialize(&mut self) {
304        relay_statsd::metric!(
305            timer(RelayTimers::BufferInitialization),
306            partition_id = &self.partition_tag,
307            {
308                let initialization_state = self.stack_provider.initialize().await;
309                self.load_stacks(initialization_state.project_key_pairs)
310                    .await;
311                self.load_store_total_count().await;
312            }
313        );
314    }
315
316    /// Pushes an envelope to the appropriate envelope stack and re-prioritizes the stack.
317    ///
318    /// If the envelope stack does not exist, a new stack is pushed to the priority queue.
319    /// The priority of the stack is updated with the envelope's received_at time.
320    pub async fn push(&mut self, envelope: Box<Envelope>) -> Result<(), EnvelopeBufferError> {
321        let received_at = envelope.received_at();
322
323        let project_key_pair = ProjectKeyPair::from_envelope(&envelope);
324        if let Some((
325            QueueItem {
326                key: _,
327                value: stack,
328            },
329            _,
330        )) = self.priority_queue.get_mut(&project_key_pair)
331        {
332            stack.push(envelope).await?;
333        } else {
334            // Since we have initialization code that creates all the necessary stacks, we assume
335            // that any new stack that is added during the envelope buffer's lifecycle, is recreated.
336            self.push_stack(
337                StackCreationType::New,
338                ProjectKeyPair::from_envelope(&envelope),
339                Some(envelope),
340            )
341            .await?;
342        }
343        self.priority_queue
344            .change_priority_by(&project_key_pair, |prio| {
345                prio.received_at = received_at;
346            });
347
348        self.total_count += 1;
349        self.tracked_count += 1;
350        self.track_total_count();
351
352        Ok(())
353    }
354
355    /// Returns a reference to the next-in-line envelope, if one exists.
356    pub async fn peek(&mut self) -> Result<Peek, EnvelopeBufferError> {
357        let Some((
358            QueueItem {
359                key: project_key_pair,
360                value: stack,
361            },
362            Priority {
363                readiness,
364                next_project_fetch,
365                ..
366            },
367        )) = self.priority_queue.peek_mut()
368        else {
369            return Ok(Peek::Empty);
370        };
371
372        let ready = readiness.ready();
373
374        Ok(match (stack.peek().await?, ready) {
375            (None, _) => Peek::Empty,
376            (Some(last_received_at), true) => Peek::Ready {
377                project_key_pair: *project_key_pair,
378                last_received_at,
379            },
380            (Some(last_received_at), false) => Peek::NotReady {
381                project_key_pair: *project_key_pair,
382                next_project_fetch: *next_project_fetch,
383                last_received_at,
384            },
385        })
386    }
387
388    /// Returns the next-in-line envelope, if one exists.
389    ///
390    /// The priority of the envelope's stack is updated with the next envelope's received_at
391    /// time. If the stack is empty after popping, it is removed from the priority queue.
392    pub async fn pop(&mut self) -> Result<Option<Box<Envelope>>, EnvelopeBufferError> {
393        let Some((QueueItem { key, value: stack }, _)) = self.priority_queue.peek_mut() else {
394            return Ok(None);
395        };
396        let project_key_pair = *key;
397        let envelope = stack.pop().await?.expect("found an empty stack");
398
399        let last_received_at = stack.peek().await?;
400
401        match last_received_at {
402            None => {
403                self.pop_stack(project_key_pair);
404            }
405            Some(last_received_at) => {
406                self.priority_queue
407                    .change_priority_by(&project_key_pair, |prio| {
408                        prio.received_at = last_received_at;
409                    });
410            }
411        }
412
413        // We are fine with the count going negative, since it represents that more data was popped,
414        // than it was initially counted, meaning that we had a wrong total count from
415        // initialization.
416        self.total_count -= 1;
417        self.tracked_count = self.tracked_count.saturating_sub(1);
418        self.track_total_count();
419
420        Ok(Some(envelope))
421    }
422
423    /// Re-prioritizes all stacks that involve the given project key by setting it to "ready".
424    ///
425    /// Returns `true` if at least one priority was changed.
426    pub fn mark_ready(&mut self, project: &ProjectKey, is_ready: bool) -> bool {
427        let mut changed = false;
428        if let Some(project_key_pairs) = self.stacks_by_project.get(project) {
429            for project_key_pair in project_key_pairs {
430                self.priority_queue
431                    .change_priority_by(project_key_pair, |stack| {
432                        let mut found = false;
433                        for (subkey, readiness) in [
434                            (
435                                project_key_pair.own_key,
436                                &mut stack.readiness.own_project_ready,
437                            ),
438                            (
439                                project_key_pair.sampling_key,
440                                &mut stack.readiness.sampling_project_ready,
441                            ),
442                        ] {
443                            if subkey == *project {
444                                found = true;
445                                if *readiness != is_ready {
446                                    changed = true;
447                                    *readiness = is_ready;
448                                }
449                            }
450                        }
451                        debug_assert!(found);
452                    });
453            }
454        }
455
456        changed
457    }
458
459    /// Marks a stack as seen.
460    ///
461    /// Non-ready stacks are deprioritized when they are marked as seen, such that
462    /// the next call to `.peek()` will look at a different stack. This prevents
463    /// head-of-line blocking.
464    pub fn mark_seen(&mut self, project_key_pair: &ProjectKeyPair, next_fetch: Duration) {
465        self.priority_queue
466            .change_priority_by(project_key_pair, |stack| {
467                // We use the next project fetch to debounce project fetching and avoid head of
468                // line blocking of non-ready stacks.
469                stack.next_project_fetch = Instant::now() + next_fetch;
470            });
471    }
472
473    /// Returns `true` if the underlying storage has the capacity to store more envelopes.
474    pub fn has_capacity(&self) -> bool {
475        self.stack_provider.has_store_capacity()
476    }
477
478    /// Flushes the envelope buffer.
479    pub async fn flush(&mut self) {
480        let priority_queue = mem::take(&mut self.priority_queue);
481        self.stack_provider
482            .flush(priority_queue.into_iter().map(|(q, _)| q.value))
483            .await;
484    }
485
486    /// Pushes a new [`EnvelopeStack`] with the given [`Envelope`] inserted.
487    async fn push_stack(
488        &mut self,
489        stack_creation_type: StackCreationType,
490        project_key_pair: ProjectKeyPair,
491        envelope: Option<Box<Envelope>>,
492    ) -> Result<(), EnvelopeBufferError> {
493        let received_at = envelope.as_ref().map_or(Utc::now(), |e| e.received_at());
494
495        let mut stack = self
496            .stack_provider
497            .create_stack(stack_creation_type, project_key_pair);
498        if let Some(envelope) = envelope {
499            stack.push(envelope).await?;
500        }
501
502        let previous_entry = self.priority_queue.push(
503            QueueItem {
504                key: project_key_pair,
505                value: stack,
506            },
507            Priority::new(received_at),
508        );
509        debug_assert!(previous_entry.is_none());
510        for project_key in project_key_pair.iter() {
511            self.stacks_by_project
512                .entry(project_key)
513                .or_default()
514                .insert(project_key_pair);
515        }
516        relay_statsd::metric!(
517            gauge(RelayGauges::BufferStackCount) = self.priority_queue.len() as u64,
518            partition_id = &self.partition_tag
519        );
520
521        Ok(())
522    }
523
524    /// Pops an [`EnvelopeStack`] with the supplied [`EnvelopeBufferError`].
525    fn pop_stack(&mut self, project_key_pair: ProjectKeyPair) {
526        for project_key in project_key_pair.iter() {
527            self.stacks_by_project
528                .get_mut(&project_key)
529                .expect("project_key is missing from lookup")
530                .remove(&project_key_pair);
531        }
532        self.priority_queue.remove(&project_key_pair);
533
534        relay_statsd::metric!(
535            gauge(RelayGauges::BufferStackCount) = self.priority_queue.len() as u64,
536            partition_id = &self.partition_tag
537        );
538    }
539
540    /// Creates all the [`EnvelopeStack`]s with no data given a set of [`ProjectKeyPair`].
541    async fn load_stacks(&mut self, project_key_pairs: HashSet<ProjectKeyPair>) {
542        for project_key_pair in project_key_pairs {
543            self.push_stack(StackCreationType::Initialization, project_key_pair, None)
544                .await
545                .expect("Pushing an empty stack raised an error");
546        }
547    }
548
549    /// Loads the total count from the store if it takes less than a specified duration.
550    ///
551    /// The total count returned by the store is related to the count of elements that the buffer
552    /// will process, besides the count of elements that will be added and removed during its
553    /// lifecycle
554    async fn load_store_total_count(&mut self) {
555        let total_count = timeout(Duration::from_secs(1), async {
556            self.stack_provider.store_total_count().await
557        })
558        .await;
559        match total_count {
560            Ok(total_count) => {
561                self.total_count = total_count as i64;
562                self.total_count_initialized = true;
563            }
564            Err(error) => {
565                self.total_count_initialized = false;
566                relay_log::error!(
567                    error = &error as &dyn Error,
568                    "failed to load the total envelope count of the store",
569                );
570            }
571        };
572        self.track_total_count();
573    }
574
575    /// Emits a metric to track the total count of envelopes that are in the envelope buffer.
576    fn track_total_count(&self) {
577        let total_count = self.total_count as f64;
578        let initialized = match self.total_count_initialized {
579            true => "true",
580            false => "false",
581        };
582        relay_statsd::metric!(
583            gauge(RelayGauges::BufferEnvelopesCount) = total_count,
584            initialized = initialized,
585            stack_type = self.stack_provider.stack_type(),
586            partition_id = &self.partition_tag
587        );
588    }
589}
590
591/// Contains the state of the first element in the buffer.
592pub enum Peek {
593    Empty,
594    Ready {
595        project_key_pair: ProjectKeyPair,
596        last_received_at: DateTime<Utc>,
597    },
598    NotReady {
599        project_key_pair: ProjectKeyPair,
600        next_project_fetch: Instant,
601        last_received_at: DateTime<Utc>,
602    },
603}
604
605impl Peek {
606    pub fn last_received_at(&self) -> Option<DateTime<Utc>> {
607        match self {
608            Self::Empty => None,
609            Self::Ready {
610                last_received_at, ..
611            }
612            | Self::NotReady {
613                last_received_at, ..
614            } => Some(*last_received_at),
615        }
616    }
617}
618
619#[derive(Debug)]
620struct QueueItem<K, V> {
621    key: K,
622    value: V,
623}
624
625impl<K, V> std::borrow::Borrow<K> for QueueItem<K, V> {
626    fn borrow(&self) -> &K {
627        &self.key
628    }
629}
630
631impl<K: std::hash::Hash, V> std::hash::Hash for QueueItem<K, V> {
632    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
633        self.key.hash(state);
634    }
635}
636
637impl<K: PartialEq, V> PartialEq for QueueItem<K, V> {
638    fn eq(&self, other: &Self) -> bool {
639        self.key == other.key
640    }
641}
642
643impl<K: PartialEq, V> Eq for QueueItem<K, V> {}
644
645#[derive(Debug, Clone)]
646struct Priority {
647    readiness: Readiness,
648    received_at: DateTime<Utc>,
649    next_project_fetch: Instant,
650}
651
652impl Priority {
653    fn new(received_at: DateTime<Utc>) -> Self {
654        Self {
655            readiness: Readiness::new(),
656            received_at,
657            next_project_fetch: Instant::now(),
658        }
659    }
660}
661
662impl Ord for Priority {
663    fn cmp(&self, other: &Self) -> Ordering {
664        match (self.readiness.ready(), other.readiness.ready()) {
665            // Assuming that two priorities differ only w.r.t. the `last_peek`, we want to prioritize
666            // stacks that were the least recently peeked. The rationale behind this is that we want
667            // to keep cycling through different stacks while peeking.
668            (true, true) => self.received_at.cmp(&other.received_at),
669            (true, false) => Ordering::Greater,
670            (false, true) => Ordering::Less,
671            // For non-ready stacks, we invert the priority, such that projects that are not
672            // ready and did not receive envelopes recently can be evicted.
673            (false, false) => self
674                .next_project_fetch
675                .cmp(&other.next_project_fetch)
676                .reverse()
677                .then(self.received_at.cmp(&other.received_at).reverse()),
678        }
679    }
680}
681
682impl PartialOrd for Priority {
683    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
684        Some(self.cmp(other))
685    }
686}
687
688impl PartialEq for Priority {
689    fn eq(&self, other: &Self) -> bool {
690        self.cmp(other).is_eq()
691    }
692}
693
694impl Eq for Priority {}
695
696#[derive(Debug, Clone, Copy)]
697struct Readiness {
698    own_project_ready: bool,
699    sampling_project_ready: bool,
700}
701
702impl Readiness {
703    fn new() -> Self {
704        // Optimistically set ready state to true.
705        // The large majority of stack creations are re-creations after a stack was emptied.
706        Self {
707            own_project_ready: true,
708            sampling_project_ready: true,
709        }
710    }
711
712    fn ready(&self) -> bool {
713        self.own_project_ready && self.sampling_project_ready
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use relay_base_schema::project::ProjectId;
720    use relay_common::Dsn;
721    use relay_config::Config;
722    use relay_event_schema::protocol::EventId;
723    use relay_sampling::DynamicSamplingContext;
724    use std::str::FromStr;
725    use std::sync::Arc;
726    use uuid::Uuid;
727
728    use crate::SqliteEnvelopeStore;
729    use crate::envelope::{Item, ItemType};
730    use crate::extractors::RequestMeta;
731    use crate::services::buffer::common::ProjectKeyPair;
732    use crate::services::buffer::envelope_store::sqlite::DatabaseEnvelope;
733    use crate::services::buffer::testutils::utils::mock_envelopes;
734    use crate::utils::MemoryStat;
735
736    use super::*;
737
738    impl Peek {
739        fn is_empty(&self) -> bool {
740            matches!(self, Peek::Empty)
741        }
742    }
743
744    fn new_envelope(
745        own_key: ProjectKey,
746        sampling_key: Option<ProjectKey>,
747        event_id: Option<EventId>,
748    ) -> Box<Envelope> {
749        let mut envelope = Envelope::from_request(
750            None,
751            RequestMeta::new(Dsn::from_str(&format!("http://{own_key}@localhost/1")).unwrap()),
752        );
753        if let Some(sampling_key) = sampling_key {
754            envelope.set_dsc(DynamicSamplingContext {
755                public_key: sampling_key,
756                project_id: Some(ProjectId::new(42)),
757                trace_id: "67e5504410b1426f9247bb680e5fe0c8".parse().unwrap(),
758                release: None,
759                user: Default::default(),
760                replay_id: None,
761                environment: None,
762                transaction: None,
763                sample_rate: None,
764                sampled: None,
765                other: Default::default(),
766            });
767            envelope.add_item(Item::new(ItemType::Transaction));
768        }
769        if let Some(event_id) = event_id {
770            envelope.set_event_id(event_id);
771        }
772        envelope
773    }
774
775    fn mock_config(path: &str) -> Arc<Config> {
776        Config::from_json_value(serde_json::json!({
777            "spool": {
778                "envelopes": {
779                    "path": path
780                }
781            }
782        }))
783        .unwrap()
784        .into()
785    }
786
787    fn mock_memory_checker() -> MemoryChecker {
788        MemoryChecker::new(MemoryStat::default(), mock_config("my/db/path").clone())
789    }
790
791    async fn peek_received_at(buffer: &mut EnvelopeBuffer<MemoryStackProvider>) -> DateTime<Utc> {
792        buffer.peek().await.unwrap().last_received_at().unwrap()
793    }
794
795    #[tokio::test]
796    async fn test_insert_pop() {
797        let mut buffer = EnvelopeBuffer::<MemoryStackProvider>::new(0, mock_memory_checker());
798
799        let project_key1 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fed").unwrap();
800        let project_key2 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fee").unwrap();
801        let project_key3 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fef").unwrap();
802
803        assert!(buffer.pop().await.unwrap().is_none());
804        assert!(buffer.peek().await.unwrap().is_empty());
805
806        let envelope1 = new_envelope(project_key1, None, None);
807        let time1 = envelope1.meta().received_at();
808        buffer.push(envelope1).await.unwrap();
809
810        let envelope2 = new_envelope(project_key2, None, None);
811        let time2 = envelope2.meta().received_at();
812        buffer.push(envelope2).await.unwrap();
813
814        // Both projects are ready, so project 2 is on top (has the newest envelopes):
815        assert_eq!(peek_received_at(&mut buffer).await, time2);
816
817        buffer.mark_ready(&project_key1, false);
818        buffer.mark_ready(&project_key2, false);
819
820        // Both projects are not ready, so project 1 is on top (has the oldest envelopes):
821        assert_eq!(peek_received_at(&mut buffer).await, time1);
822
823        let envelope3 = new_envelope(project_key3, None, None);
824        let time3 = envelope3.meta().received_at();
825        buffer.push(envelope3).await.unwrap();
826        buffer.mark_ready(&project_key3, false);
827
828        // All projects are not ready, so project 1 is on top (has the oldest envelopes):
829        assert_eq!(peek_received_at(&mut buffer).await, time1);
830
831        // After marking a project ready, it goes to the top:
832        buffer.mark_ready(&project_key3, true);
833        assert_eq!(peek_received_at(&mut buffer).await, time3);
834        assert_eq!(
835            buffer.pop().await.unwrap().unwrap().meta().public_key(),
836            project_key3
837        );
838
839        // After popping, project 1 is on top again:
840        assert_eq!(peek_received_at(&mut buffer).await, time1);
841
842        // Mark project 1 as ready (still on top):
843        buffer.mark_ready(&project_key1, true);
844        assert_eq!(peek_received_at(&mut buffer).await, time1);
845
846        // Mark project 2 as ready as well (now on top because most recent):
847        buffer.mark_ready(&project_key2, true);
848        assert_eq!(peek_received_at(&mut buffer).await, time2);
849        assert_eq!(
850            buffer.pop().await.unwrap().unwrap().meta().public_key(),
851            project_key2
852        );
853
854        // Pop last element:
855        assert_eq!(
856            buffer.pop().await.unwrap().unwrap().meta().public_key(),
857            project_key1
858        );
859        assert!(buffer.pop().await.unwrap().is_none());
860        assert!(buffer.peek().await.unwrap().is_empty());
861    }
862
863    #[tokio::test]
864    async fn test_project_internal_order() {
865        let mut buffer = EnvelopeBuffer::<MemoryStackProvider>::new(0, mock_memory_checker());
866
867        let project_key = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fed").unwrap();
868
869        let envelope1 = new_envelope(project_key, None, None);
870        let time1 = envelope1.meta().received_at();
871        let envelope2 = new_envelope(project_key, None, None);
872        let time2 = envelope2.meta().received_at();
873
874        assert!(time2 > time1);
875
876        buffer.push(envelope1).await.unwrap();
877        buffer.push(envelope2).await.unwrap();
878
879        assert_eq!(
880            buffer.pop().await.unwrap().unwrap().meta().received_at(),
881            time2
882        );
883        assert_eq!(
884            buffer.pop().await.unwrap().unwrap().meta().received_at(),
885            time1
886        );
887        assert!(buffer.pop().await.unwrap().is_none());
888    }
889
890    #[tokio::test]
891    async fn test_sampling_projects() {
892        let mut buffer = EnvelopeBuffer::<MemoryStackProvider>::new(0, mock_memory_checker());
893
894        let project_key1 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fed").unwrap();
895        let project_key2 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fef").unwrap();
896
897        let envelope1 = new_envelope(project_key1, None, None);
898        let time1 = envelope1.received_at();
899        buffer.push(envelope1).await.unwrap();
900
901        let envelope2 = new_envelope(project_key2, None, None);
902        let time2 = envelope2.received_at();
903        buffer.push(envelope2).await.unwrap();
904
905        let envelope3 = new_envelope(project_key1, Some(project_key2), None);
906        let time3 = envelope3.meta().received_at();
907        buffer.push(envelope3).await.unwrap();
908
909        buffer.mark_ready(&project_key1, false);
910        buffer.mark_ready(&project_key2, false);
911
912        // Nothing is ready, instant1 is on top:
913        assert_eq!(
914            buffer.peek().await.unwrap().last_received_at().unwrap(),
915            time1
916        );
917
918        // Mark project 2 ready, gets on top:
919        buffer.mark_ready(&project_key2, true);
920        assert_eq!(
921            buffer.peek().await.unwrap().last_received_at().unwrap(),
922            time2
923        );
924
925        // Revert
926        buffer.mark_ready(&project_key2, false);
927        assert_eq!(
928            buffer.peek().await.unwrap().last_received_at().unwrap(),
929            time1
930        );
931
932        // Project 1 ready:
933        buffer.mark_ready(&project_key1, true);
934        assert_eq!(
935            buffer.peek().await.unwrap().last_received_at().unwrap(),
936            time1
937        );
938
939        // when both projects are ready, event no 3 ends up on top:
940        buffer.mark_ready(&project_key2, true);
941        assert_eq!(
942            buffer.pop().await.unwrap().unwrap().meta().received_at(),
943            time3
944        );
945        assert_eq!(
946            buffer.peek().await.unwrap().last_received_at().unwrap(),
947            time2
948        );
949
950        buffer.mark_ready(&project_key2, false);
951        assert_eq!(buffer.pop().await.unwrap().unwrap().received_at(), time1);
952        assert_eq!(buffer.pop().await.unwrap().unwrap().received_at(), time2);
953
954        assert!(buffer.pop().await.unwrap().is_none());
955    }
956
957    #[tokio::test]
958    async fn test_project_keys_distinct() {
959        let project_key1 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fed").unwrap();
960        let project_key2 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fef").unwrap();
961
962        let project_key_pair1 = ProjectKeyPair::new(project_key1, project_key2);
963        let project_key_pair2 = ProjectKeyPair::new(project_key2, project_key1);
964
965        assert_ne!(project_key_pair1, project_key_pair2);
966
967        let mut buffer = EnvelopeBuffer::<MemoryStackProvider>::new(0, mock_memory_checker());
968        buffer
969            .push(new_envelope(project_key1, Some(project_key2), None))
970            .await
971            .unwrap();
972        buffer
973            .push(new_envelope(project_key2, Some(project_key1), None))
974            .await
975            .unwrap();
976        assert_eq!(buffer.priority_queue.len(), 2);
977    }
978
979    #[test]
980    fn test_total_order() {
981        let p1 = Priority {
982            readiness: Readiness {
983                own_project_ready: true,
984                sampling_project_ready: true,
985            },
986            received_at: Utc::now(),
987            next_project_fetch: Instant::now(),
988        };
989        let mut p2 = p1.clone();
990        p2.next_project_fetch += Duration::from_millis(1);
991
992        // Last peek does not matter because project is ready:
993        assert_eq!(p1.cmp(&p2), Ordering::Equal);
994        assert_eq!(p1, p2);
995    }
996
997    #[tokio::test]
998    async fn test_last_peek_internal_order() {
999        let mut buffer = EnvelopeBuffer::<MemoryStackProvider>::new(0, mock_memory_checker());
1000
1001        let project_key_1 = ProjectKey::parse("a94ae32be2584e0bbd7a4cbb95971fed").unwrap();
1002        let event_id_1 = EventId::new();
1003        let envelope1 = new_envelope(project_key_1, None, Some(event_id_1));
1004        let time1 = envelope1.received_at();
1005
1006        let project_key_2 = ProjectKey::parse("b56ae32be2584e0bbd7a4cbb95971fed").unwrap();
1007        let event_id_2 = EventId::new();
1008        let envelope2 = new_envelope(project_key_2, None, Some(event_id_2));
1009        let time2 = envelope2.received_at();
1010
1011        buffer.push(envelope1).await.unwrap();
1012        buffer.push(envelope2).await.unwrap();
1013
1014        buffer.mark_ready(&project_key_1, false);
1015        buffer.mark_ready(&project_key_2, false);
1016
1017        // event_id_1 is first element:
1018        let Peek::NotReady {
1019            last_received_at, ..
1020        } = buffer.peek().await.unwrap()
1021        else {
1022            panic!();
1023        };
1024        assert_eq!(last_received_at, time1);
1025
1026        // Second peek returns same element:
1027        let Peek::NotReady {
1028            last_received_at,
1029            project_key_pair,
1030            ..
1031        } = buffer.peek().await.unwrap()
1032        else {
1033            panic!();
1034        };
1035        assert_eq!(last_received_at, time1);
1036        assert_ne!(last_received_at, time2);
1037
1038        buffer.mark_seen(&project_key_pair, Duration::ZERO);
1039
1040        // After mark_seen, event 2 is on top:
1041        let Peek::NotReady {
1042            last_received_at, ..
1043        } = buffer.peek().await.unwrap()
1044        else {
1045            panic!();
1046        };
1047        assert_eq!(last_received_at, time2);
1048        assert_ne!(last_received_at, time1);
1049
1050        let Peek::NotReady {
1051            last_received_at,
1052            project_key_pair,
1053            ..
1054        } = buffer.peek().await.unwrap()
1055        else {
1056            panic!();
1057        };
1058        assert_eq!(last_received_at, time2);
1059        assert_ne!(last_received_at, time1);
1060
1061        buffer.mark_seen(&project_key_pair, Duration::ZERO);
1062
1063        // After another mark_seen, cycle back to event 1:
1064        let Peek::NotReady {
1065            last_received_at, ..
1066        } = buffer.peek().await.unwrap()
1067        else {
1068            panic!();
1069        };
1070        assert_eq!(last_received_at, time1);
1071        assert_ne!(last_received_at, time2);
1072    }
1073
1074    #[tokio::test]
1075    async fn test_initialize_buffer() {
1076        let path = std::env::temp_dir()
1077            .join(Uuid::new_v4().to_string())
1078            .into_os_string()
1079            .into_string()
1080            .unwrap();
1081        let config = mock_config(&path);
1082        let current_config = config.current();
1083        let mut store = SqliteEnvelopeStore::prepare(0, &current_config)
1084            .await
1085            .unwrap();
1086        let mut buffer = EnvelopeBuffer::<SqliteStackProvider>::new(0, &current_config)
1087            .await
1088            .unwrap();
1089
1090        // We write 5 envelopes to disk so that we can check if they are loaded. These envelopes
1091        // belong to the same project keys, so they belong to the same envelope stack.
1092        let envelopes = mock_envelopes(10);
1093        assert!(
1094            store
1095                .insert_batch(
1096                    envelopes
1097                        .into_iter()
1098                        .map(|e| DatabaseEnvelope::try_from(e.as_ref()).unwrap())
1099                        .collect::<Vec<_>>()
1100                        .try_into()
1101                        .unwrap()
1102                )
1103                .await
1104                .is_ok()
1105        );
1106
1107        // We assume that the buffer is empty.
1108        assert!(buffer.priority_queue.is_empty());
1109        assert!(buffer.stacks_by_project.is_empty());
1110
1111        buffer.initialize().await;
1112
1113        // We assume that we loaded only 1 envelope stack, because of the project keys combinations
1114        // of the envelopes we inserted above.
1115        assert_eq!(buffer.priority_queue.len(), 1);
1116        // We expect to have an entry per project key, since we have 1 pair, the total entries
1117        // should be 2.
1118        assert_eq!(buffer.stacks_by_project.len(), 2);
1119    }
1120}