1use std::{
2 collections::BTreeMap,
3 fs,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6};
7
8use serde::{Deserialize, Serialize};
9use tokio::sync::mpsc;
10
11use crate::{CdcError, CdcOffset, CdcResult, PgLsn};
12
13pub(crate) enum FeedbackCommand {
14 Checkpoint(CdcOffset),
15 Stop,
16 DropSlot {
17 force: bool,
18 reply: tokio::sync::oneshot::Sender<CdcResult<()>>,
19 },
20}
21
22pub trait CdcCheckpointStore: Send + Sync + 'static {
24 fn load(&self, slot: &str) -> CdcResult<Option<CdcOffset>>;
25 fn store(&self, offset: &CdcOffset) -> CdcResult<()>;
26}
27
28#[derive(Debug, Clone)]
33pub struct FileCheckpointStore {
34 path: Arc<PathBuf>,
35}
36
37impl FileCheckpointStore {
38 #[must_use]
39 pub fn new(path: impl Into<PathBuf>) -> Self {
40 Self {
41 path: Arc::new(path.into()),
42 }
43 }
44
45 fn slot_path(&self, slot: &str) -> PathBuf {
46 if self.path.extension().is_some() {
47 return (*self.path).clone();
48 }
49 self.path.join(format!("{slot}.json"))
50 }
51}
52
53impl CdcCheckpointStore for FileCheckpointStore {
54 fn load(&self, slot: &str) -> CdcResult<Option<CdcOffset>> {
55 let path = self.slot_path(slot);
56 if !path.exists() {
57 return Ok(None);
58 }
59 let bytes = fs::read(&path)?;
60 let record: CheckpointRecord = serde_json::from_slice(&bytes)?;
61 Ok(Some(record.offset))
62 }
63
64 fn store(&self, offset: &CdcOffset) -> CdcResult<()> {
65 let path = self.slot_path(&offset.slot);
66 if let Some(parent) = path.parent() {
67 fs::create_dir_all(parent)?;
68 }
69 let tmp = path.with_extension("json.tmp");
70 let bytes = serde_json::to_vec_pretty(&CheckpointRecord {
71 offset: offset.clone(),
72 })?;
73 fs::write(&tmp, bytes)?;
74 fs::rename(tmp, path)?;
75 Ok(())
76 }
77}
78
79#[derive(Debug, Serialize, Deserialize)]
80struct CheckpointRecord {
81 offset: CdcOffset,
82}
83
84#[derive(Debug, Default)]
86pub struct MemoryCheckpointStore {
87 offsets: Mutex<BTreeMap<String, CdcOffset>>,
88}
89
90impl MemoryCheckpointStore {
91 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95}
96
97impl CdcCheckpointStore for MemoryCheckpointStore {
98 fn load(&self, slot: &str) -> CdcResult<Option<CdcOffset>> {
99 Ok(self
100 .offsets
101 .lock()
102 .expect("checkpoint store poisoned")
103 .get(slot)
104 .cloned())
105 }
106
107 fn store(&self, offset: &CdcOffset) -> CdcResult<()> {
108 self.offsets
109 .lock()
110 .expect("checkpoint store poisoned")
111 .insert(offset.slot.clone(), offset.clone());
112 Ok(())
113 }
114}
115
116#[derive(Clone)]
118pub struct CdcCheckpointHandle {
119 slot: String,
120 store: Option<Arc<dyn CdcCheckpointStore>>,
121 commands: mpsc::UnboundedSender<FeedbackCommand>,
122}
123
124impl CdcCheckpointHandle {
125 pub(crate) fn new(
126 slot: String,
127 store: Option<Arc<dyn CdcCheckpointStore>>,
128 commands: mpsc::UnboundedSender<FeedbackCommand>,
129 ) -> Self {
130 Self {
131 slot,
132 store,
133 commands,
134 }
135 }
136
137 pub fn checkpoint(&self, offset: CdcOffset) -> CdcResult<()> {
142 if offset.slot != self.slot {
143 return Err(CdcError::Checkpoint(format!(
144 "checkpoint for slot {:?} cannot be applied to slot {:?}",
145 offset.slot, self.slot
146 )));
147 }
148 if let Some(store) = &self.store {
149 store.store(&offset)?;
150 }
151 self.commands
152 .send(FeedbackCommand::Checkpoint(offset))
153 .map_err(|_| CdcError::ChannelClosed)
154 }
155
156 pub async fn checkpoint_async(&self, offset: CdcOffset) -> CdcResult<()> {
157 self.checkpoint(offset)
158 }
159}
160
161#[derive(Debug)]
162pub(crate) struct FeedbackCoordinator {
163 slot: String,
164 watermark: PgLsn,
165 pending: BTreeMap<PgLsn, PendingTx>,
166}
167
168#[derive(Debug)]
169struct PendingTx {
170 seen: Vec<bool>,
171 seen_count: usize,
172}
173
174impl FeedbackCoordinator {
175 pub(crate) fn new(slot: String, watermark: PgLsn) -> Self {
176 Self {
177 slot,
178 watermark,
179 pending: BTreeMap::new(),
180 }
181 }
182
183 pub(crate) fn register_tx(&mut self, tx_end_lsn: PgLsn, event_count: u32) -> CdcResult<()> {
184 if event_count == 0 || tx_end_lsn <= self.watermark {
185 return Ok(());
186 }
187 let event_count = usize::try_from(event_count).map_err(|_| {
188 CdcError::Checkpoint("transaction event count does not fit usize".into())
189 })?;
190 self.pending
191 .entry(tx_end_lsn)
192 .or_insert_with(|| PendingTx::new(event_count));
193 Ok(())
194 }
195
196 pub(crate) fn checkpoint(&mut self, offset: &CdcOffset) -> CdcResult<Option<PgLsn>> {
197 if offset.slot != self.slot || offset.tx_end_lsn <= self.watermark {
198 return Ok(None);
199 }
200 self.register_tx(offset.tx_end_lsn, offset.event_count)?;
201 let Some(tx) = self.pending.get_mut(&offset.tx_end_lsn) else {
202 return Ok(None);
203 };
204 tx.mark(offset.event_index)?;
205 Ok(self.advance_watermark())
206 }
207
208 pub(crate) fn note_empty_tx(&mut self, tx_end_lsn: PgLsn) -> CdcResult<Option<PgLsn>> {
216 if tx_end_lsn <= self.watermark {
217 return Ok(None);
218 }
219 self.pending
220 .entry(tx_end_lsn)
221 .or_insert_with(|| PendingTx::new(0));
222 Ok(self.advance_watermark())
223 }
224
225 fn advance_watermark(&mut self) -> Option<PgLsn> {
228 let mut advanced = None;
229 while let Some((lsn, tx)) = self.pending.first_key_value() {
230 if !tx.is_complete() {
231 break;
232 }
233 let lsn = *lsn;
234 self.watermark = lsn;
235 advanced = Some(lsn);
236 self.pending.remove(&lsn);
237 }
238 advanced
239 }
240
241 #[cfg(test)]
242 #[must_use]
243 pub(crate) fn watermark(&self) -> PgLsn {
244 self.watermark
245 }
246}
247
248impl PendingTx {
249 fn new(event_count: usize) -> Self {
250 Self {
251 seen: vec![false; event_count],
252 seen_count: 0,
253 }
254 }
255
256 fn mark(&mut self, event_index: u32) -> CdcResult<()> {
257 let event_index = usize::try_from(event_index)
258 .map_err(|_| CdcError::Checkpoint("event index does not fit usize".into()))?;
259 let Some(seen) = self.seen.get_mut(event_index) else {
260 return Err(CdcError::Checkpoint(format!(
261 "event index {event_index} outside transaction event count {}",
262 self.seen.len()
263 )));
264 };
265 if !*seen {
266 *seen = true;
267 self.seen_count += 1;
268 }
269 Ok(())
270 }
271
272 fn is_complete(&self) -> bool {
273 self.seen_count == self.seen.len()
274 }
275}
276
277#[allow(dead_code)]
278fn _assert_path_send_sync(_: &Path) {}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn offset(slot: &str, tx: u64, index: u32, count: u32) -> CdcOffset {
285 CdcOffset {
286 slot: slot.to_owned(),
287 tx_end_lsn: PgLsn::from_u64(tx),
288 commit_lsn: PgLsn::from_u64(tx - 1),
289 xid: 7,
290 event_index: index,
291 event_count: count,
292 }
293 }
294
295 #[test]
296 fn feedback_waits_for_complete_contiguous_transactions() {
297 let mut feedback = FeedbackCoordinator::new("slot".to_owned(), PgLsn::ZERO);
298 feedback
299 .register_tx(PgLsn::from_u64(10), 2)
300 .expect("register tx 10");
301 feedback
302 .register_tx(PgLsn::from_u64(20), 1)
303 .expect("register tx 20");
304
305 assert_eq!(
306 feedback.checkpoint(&offset("slot", 20, 0, 1)).unwrap(),
307 None
308 );
309 assert_eq!(
310 feedback.checkpoint(&offset("slot", 10, 0, 2)).unwrap(),
311 None
312 );
313 assert_eq!(
314 feedback.checkpoint(&offset("slot", 10, 1, 2)).unwrap(),
315 Some(PgLsn::from_u64(20))
316 );
317 assert_eq!(feedback.watermark(), PgLsn::from_u64(20));
318 }
319
320 #[test]
321 fn empty_transaction_does_not_advance_past_unconfirmed_earlier_transaction() {
322 let mut feedback = FeedbackCoordinator::new("slot".to_owned(), PgLsn::ZERO);
323 feedback
326 .register_tx(PgLsn::from_u64(10), 2)
327 .expect("register tx 10");
328
329 assert_eq!(feedback.note_empty_tx(PgLsn::from_u64(20)).unwrap(), None);
333 assert_eq!(feedback.watermark(), PgLsn::ZERO);
334
335 assert_eq!(
338 feedback.checkpoint(&offset("slot", 10, 0, 2)).unwrap(),
339 None
340 );
341 assert_eq!(
342 feedback.checkpoint(&offset("slot", 10, 1, 2)).unwrap(),
343 Some(PgLsn::from_u64(20))
344 );
345 assert_eq!(feedback.watermark(), PgLsn::from_u64(20));
346 }
347
348 #[test]
349 fn empty_transaction_advances_when_no_earlier_transaction_pending() {
350 let mut feedback = FeedbackCoordinator::new("slot".to_owned(), PgLsn::ZERO);
351 assert_eq!(
354 feedback.note_empty_tx(PgLsn::from_u64(30)).unwrap(),
355 Some(PgLsn::from_u64(30))
356 );
357 assert_eq!(feedback.watermark(), PgLsn::from_u64(30));
358 assert_eq!(feedback.note_empty_tx(PgLsn::from_u64(30)).unwrap(), None);
360 assert_eq!(feedback.watermark(), PgLsn::from_u64(30));
361 }
362}