Skip to main content

liminal/durability/channel/
presence.rs

1//! Authoritative idempotency-key lookup reads a channel's partitions page by page,
2//! without changing admission state and without holding the channel's append lock.
3use std::sync::Arc;
4
5use super::{AppendResolution, DurableChannel, MessageEnvelope};
6use crate::durability::head_checkpoint::READ_BATCH_SIZE;
7use crate::durability::{DurabilityError, DurableStore};
8
9/// The streams a key lookup reads, taken from a channel so the lookup can run
10/// after the channel's append lock is released.
11#[derive(Debug)]
12pub struct KeyLookup {
13    stream_keys: Vec<String>,
14    store: Arc<dyn DurableStore>,
15}
16
17impl DurableChannel {
18    /// The streams that answer for this channel's idempotency keys.
19    ///
20    /// A key is looked up only once its pending append is settled, and a held
21    /// key is never appended again, so rows appended while the lookup reads
22    /// cannot carry it: the lookup needs no lock.
23    #[must_use]
24    pub fn key_lookup(&self) -> KeyLookup {
25        KeyLookup {
26            stream_keys: (0..self.partition_count())
27                .map(|partition| self.stream_key_for(partition))
28                .collect(),
29            store: Arc::clone(&self.store),
30        }
31    }
32
33    /// Find the original durable position of a producer's key across
34    /// partitions, reading each partition from its position in `from`.
35    ///
36    /// # Errors
37    /// Propagates authoritative read and envelope decode errors. A failed read
38    /// never establishes absence.
39    pub async fn find_idempotency_key(
40        &self,
41        key: &str,
42        from: &[u64],
43    ) -> Result<AppendResolution, DurabilityError> {
44        self.key_lookup().find(key, from).await
45    }
46}
47
48impl KeyLookup {
49    /// Find the original durable position of a producer's key, reading each
50    /// partition from its position in `from` in pages of the recovery read
51    /// size.
52    ///
53    /// `from` is where each partition stood when the key was claimed, which
54    /// is before the key's row could be appended, so no row before it can
55    /// carry the key. A partition `from` does not name is read from its first
56    /// row.
57    ///
58    /// # Errors
59    /// Propagates authoritative read and envelope decode errors. A failed read
60    /// never establishes absence.
61    pub async fn find(&self, key: &str, from: &[u64]) -> Result<AppendResolution, DurabilityError> {
62        for (partition, stream_key) in self.stream_keys.iter().enumerate() {
63            let mut sequence = from.get(partition).copied().unwrap_or(0);
64            loop {
65                let page = self
66                    .store
67                    .read_from(stream_key, sequence, READ_BATCH_SIZE)
68                    .await?;
69                for row in &page {
70                    if row.sequence != sequence {
71                        return Err(DurabilityError::ConfigError(format!(
72                            "stream {stream_key} read at {sequence} returned sequence {}",
73                            row.sequence
74                        )));
75                    }
76                    let envelope = MessageEnvelope::deserialize(&row.payload)?;
77                    if envelope.idempotency_key.as_deref() == Some(key) {
78                        return Ok(AppendResolution::Committed {
79                            position: row.sequence,
80                        });
81                    }
82                    sequence = sequence.checked_add(1).ok_or_else(|| {
83                        DurabilityError::ConfigError(format!(
84                            "stream {stream_key} sequence overflow during key lookup"
85                        ))
86                    })?;
87                }
88                if page.len() < READ_BATCH_SIZE {
89                    break;
90                }
91            }
92        }
93        Ok(AppendResolution::Absent)
94    }
95}