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 let mut advanced = None;
206 while let Some((lsn, tx)) = self.pending.first_key_value() {
207 if !tx.is_complete() {
208 break;
209 }
210 let lsn = *lsn;
211 self.watermark = lsn;
212 advanced = Some(lsn);
213 self.pending.remove(&lsn);
214 }
215 Ok(advanced)
216 }
217
218 #[cfg(test)]
219 #[must_use]
220 pub(crate) fn watermark(&self) -> PgLsn {
221 self.watermark
222 }
223}
224
225impl PendingTx {
226 fn new(event_count: usize) -> Self {
227 Self {
228 seen: vec![false; event_count],
229 seen_count: 0,
230 }
231 }
232
233 fn mark(&mut self, event_index: u32) -> CdcResult<()> {
234 let event_index = usize::try_from(event_index)
235 .map_err(|_| CdcError::Checkpoint("event index does not fit usize".into()))?;
236 let Some(seen) = self.seen.get_mut(event_index) else {
237 return Err(CdcError::Checkpoint(format!(
238 "event index {event_index} outside transaction event count {}",
239 self.seen.len()
240 )));
241 };
242 if !*seen {
243 *seen = true;
244 self.seen_count += 1;
245 }
246 Ok(())
247 }
248
249 fn is_complete(&self) -> bool {
250 self.seen_count == self.seen.len()
251 }
252}
253
254#[allow(dead_code)]
255fn _assert_path_send_sync(_: &Path) {}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 fn offset(slot: &str, tx: u64, index: u32, count: u32) -> CdcOffset {
262 CdcOffset {
263 slot: slot.to_owned(),
264 tx_end_lsn: PgLsn::from_u64(tx),
265 commit_lsn: PgLsn::from_u64(tx - 1),
266 xid: 7,
267 event_index: index,
268 event_count: count,
269 }
270 }
271
272 #[test]
273 fn feedback_waits_for_complete_contiguous_transactions() {
274 let mut feedback = FeedbackCoordinator::new("slot".to_owned(), PgLsn::ZERO);
275 feedback
276 .register_tx(PgLsn::from_u64(10), 2)
277 .expect("register tx 10");
278 feedback
279 .register_tx(PgLsn::from_u64(20), 1)
280 .expect("register tx 20");
281
282 assert_eq!(
283 feedback.checkpoint(&offset("slot", 20, 0, 1)).unwrap(),
284 None
285 );
286 assert_eq!(
287 feedback.checkpoint(&offset("slot", 10, 0, 2)).unwrap(),
288 None
289 );
290 assert_eq!(
291 feedback.checkpoint(&offset("slot", 10, 1, 2)).unwrap(),
292 Some(PgLsn::from_u64(20))
293 );
294 assert_eq!(feedback.watermark(), PgLsn::from_u64(20));
295 }
296}