Skip to main content

liminal/durability/
head_checkpoint_stop.rs

1//! Clean-stop checkpoint ownership at the shared store's append boundary.
2use super::{DurabilityError, DurableStore, ScanCursor, ScanPage, StoredEntry};
3use std::collections::BTreeMap;
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::{Arc, Mutex, MutexGuard};
6
7/// Tracks the streams appended through this store for the final shutdown checkpoint.
8#[derive(Debug)]
9pub struct HeadCheckpointStop {
10    inner: Arc<dyn DurableStore>,
11    touched: Mutex<BTreeMap<String, u64>>,
12    stopping: AtomicBool,
13    generation: AtomicU64,
14}
15
16impl HeadCheckpointStop {
17    /// Wraps the shared store before any server stream is constructed.
18    #[must_use]
19    pub fn new(inner: Arc<dyn DurableStore>) -> Self {
20        Self {
21            inner,
22            touched: Mutex::default(),
23            stopping: AtomicBool::new(false),
24            generation: AtomicU64::new(0),
25        }
26    }
27
28    /// Records every head still owed at a clean stop, before the final store flush.
29    ///
30    /// # Errors
31    /// Propagates the store's checkpoint refusal.
32    pub async fn checkpoint_heads(&self) -> Result<(), DurabilityError> {
33        self.stopping.store(true, Ordering::Release);
34        self.advance_touched_heads().await
35    }
36
37    fn touched(&self) -> Result<MutexGuard<'_, BTreeMap<String, u64>>, DurabilityError> {
38        self.touched.lock().map_err(|_| {
39            DurabilityError::ConfigError("clean-stop head tracking lock poisoned".to_owned())
40        })
41    }
42
43    fn note_append(&self, stream: &str) -> Result<(), DurabilityError> {
44        // A receipt stays unique after its stream leaves the pending map, so
45        // another checkpoint cannot clear a later append of the same stream.
46        let generation = self
47            .generation
48            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |previous| {
49                previous.checked_add(1)
50            })
51            .map_err(|_| {
52                DurabilityError::ConfigError(
53                    "clean-stop head tracking generation exhausted".to_owned(),
54                )
55            })?;
56        self.touched()?.insert(stream.to_owned(), generation);
57        Ok(())
58    }
59
60    async fn advance_touched_heads(&self) -> Result<(), DurabilityError> {
61        let touched = self.touched()?.clone();
62        for (stream, generation) in touched {
63            if let Some(next) = self.inner.read_stream_next_seq(&stream).await? {
64                let key = super::head_checkpoint_key(&stream);
65                let recorded = self.inner.read_value(&key).await?.unwrap_or(0);
66                if recorded > next {
67                    return Err(DurabilityError::ConfigError(format!(
68                        "clean-stop head {recorded} exceeds stream {stream} head {next}"
69                    )));
70                }
71                if recorded < next {
72                    self.inner.cas(&key, recorded, next).await?;
73                }
74            }
75            let mut current = self.touched()?;
76            if current.get(&stream) == Some(&generation) {
77                current.remove(&stream);
78            }
79        }
80        Ok(())
81    }
82}
83
84#[async_trait::async_trait]
85impl DurableStore for HeadCheckpointStop {
86    fn reclaim_spine(&self) -> Option<Arc<haematite::EventStore>> {
87        self.inner.reclaim_spine()
88    }
89    fn max_request_bytes(&self) -> Option<usize> {
90        self.inner.max_request_bytes()
91    }
92    fn store_counts(&self) -> Option<super::StoreCountsHandle> {
93        self.inner.store_counts()
94    }
95    async fn truncate_before(
96        &self,
97        stream_key: &str,
98        before_seq: u64,
99    ) -> Result<u64, DurabilityError> {
100        self.inner.truncate_before(stream_key, before_seq).await
101    }
102    async fn scan_routed(
103        &self,
104        route_key: &str,
105        from: &[u8],
106        to: &[u8],
107    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, DurabilityError> {
108        self.inner.scan_routed(route_key, from, to).await
109    }
110    async fn remove_routed_range(
111        &self,
112        route_key: &str,
113        from: &[u8],
114        to: &[u8],
115    ) -> Result<u64, DurabilityError> {
116        self.inner.remove_routed_range(route_key, from, to).await
117    }
118    async fn apply_routed_batch(
119        &self,
120        route_key: &str,
121        items: Vec<crate::durability::RoutedBatchItem>,
122    ) -> Result<(), crate::durability::DurabilityError> {
123        self.inner.apply_routed_batch(route_key, items).await
124    }
125
126    async fn apply_routed_entries(
127        &self,
128        route_key: &str,
129        entries: Vec<crate::durability::RoutedBatchEntry>,
130    ) -> Result<(), crate::durability::DurabilityError> {
131        self.inner.apply_routed_entries(route_key, entries).await
132    }
133
134    async fn get_routed(
135        &self,
136        route_key: &str,
137        key: &[u8],
138    ) -> Result<Option<Vec<u8>>, crate::durability::DurabilityError> {
139        self.inner.get_routed(route_key, key).await
140    }
141
142    async fn append(
143        &self,
144        stream_key: &str,
145        payload: Vec<u8>,
146        expected_seq: u64,
147    ) -> Result<u64, DurabilityError> {
148        self.note_append(stream_key)?;
149        self.inner.append(stream_key, payload, expected_seq).await
150    }
151
152    async fn append_all(
153        &self,
154        stream_key: &str,
155        payloads: Vec<Vec<u8>>,
156        expected_seq: u64,
157    ) -> Result<u64, DurabilityError> {
158        self.note_append(stream_key)?;
159        self.inner
160            .append_all(stream_key, payloads, expected_seq)
161            .await
162    }
163
164    async fn read_from(
165        &self,
166        stream_key: &str,
167        offset: u64,
168        limit: usize,
169    ) -> Result<Vec<StoredEntry>, DurabilityError> {
170        self.inner.read_from(stream_key, offset, limit).await
171    }
172
173    async fn read_at(
174        &self,
175        stream_key: &str,
176        sequence: u64,
177    ) -> Result<Option<StoredEntry>, DurabilityError> {
178        self.inner.read_at(stream_key, sequence).await
179    }
180
181    async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
182        self.inner.cas(key, old_value, new_value).await
183    }
184
185    async fn read_stream_next_seq(&self, stream_key: &str) -> Result<Option<u64>, DurabilityError> {
186        self.inner.read_stream_next_seq(stream_key).await
187    }
188
189    async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
190        self.inner.read_value(key).await
191    }
192
193    async fn scan_page(
194        &self,
195        prefix: &str,
196        cursor: Option<ScanCursor>,
197        limit: usize,
198    ) -> Result<ScanPage, DurabilityError> {
199        self.inner.scan_page(prefix, cursor, limit).await
200    }
201
202    async fn flush(&self) -> Result<(), DurabilityError> {
203        // Final-flush observations can append through this same store. Their
204        // own durable flush must record the newly appended head too.
205        if self.stopping.load(Ordering::Acquire) {
206            self.advance_touched_heads().await?;
207        }
208        self.inner.flush().await
209    }
210}