1use std::collections::{BTreeMap, VecDeque};
8use std::fs::{self, File, OpenOptions};
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Condvar, Mutex, RwLock};
12use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
13
14use reddb_file::{ReplicationSlot, ReplicationSlotInvalidationCause};
15use tracing::warn;
16
17mod slots;
18use slots::{
19 load_replication_slot_catalog, load_replication_slots, persist_replication_slot_catalog,
20 persist_replication_slots,
21};
22
23fn term_from_payload(payload: &[u8]) -> u64 {
24 crate::replication::cdc::ChangeRecord::decode(payload)
25 .map(|record| record.term)
26 .unwrap_or(crate::replication::DEFAULT_REPLICATION_TERM)
27}
28
29fn authority_from_payload(payload: &[u8]) -> (u64, Option<u64>) {
30 crate::replication::cdc::ChangeRecord::decode(payload)
31 .map(|record| (record.term, record.ownership_epoch))
32 .unwrap_or((crate::replication::DEFAULT_REPLICATION_TERM, None))
33}
34
35pub struct WalBuffer {
43 records: RwLock<VecDeque<(u64, Arc<[u8]>)>>,
45 current_lsn: RwLock<u64>,
47}
48
49impl WalBuffer {
50 pub fn new(max_size: usize) -> Self {
51 Self {
52 records: RwLock::new(VecDeque::with_capacity(max_size)),
53 current_lsn: RwLock::new(0),
54 }
55 }
56
57 pub fn append(&self, lsn: u64, data: Vec<u8>) {
59 let mut records = self.records.write().unwrap_or_else(|e| e.into_inner());
60 records.push_back((lsn, Arc::from(data.into_boxed_slice())));
61
62 let mut current = self.current_lsn.write().unwrap_or_else(|e| e.into_inner());
63 *current = (*current).max(lsn);
64 }
65
66 pub fn read_since(&self, since_lsn: u64, max_count: usize) -> Vec<(u64, Vec<u8>)> {
72 self.read_since_shared(since_lsn, max_count)
73 .into_iter()
74 .map(|(lsn, data)| (lsn, data.to_vec()))
75 .collect()
76 }
77
78 pub fn read_since_shared(&self, since_lsn: u64, max_count: usize) -> Vec<(u64, Arc<[u8]>)> {
83 let records = self.records.read().unwrap_or_else(|e| e.into_inner());
84 records
85 .iter()
86 .filter(|(lsn, _)| *lsn > since_lsn)
87 .take(max_count)
88 .cloned()
89 .collect()
90 }
91
92 pub fn read_since_shared_bounded(
96 &self,
97 since_lsn: u64,
98 max_count: usize,
99 max_payload_bytes: usize,
100 ) -> Vec<(u64, Arc<[u8]>)> {
101 let records = self.records.read().unwrap_or_else(|e| e.into_inner());
102 let mut payload_bytes = 0usize;
103 records
104 .iter()
105 .filter(|(lsn, _)| *lsn > since_lsn)
106 .take(max_count)
107 .take_while(|(_, data)| {
108 let next_payload_bytes = payload_bytes.saturating_add(data.len());
109 let include = payload_bytes == 0 || next_payload_bytes <= max_payload_bytes;
110 if include {
111 payload_bytes = next_payload_bytes;
112 }
113 include
114 })
115 .cloned()
116 .collect()
117 }
118
119 pub fn current_lsn(&self) -> u64 {
121 *self.current_lsn.read().unwrap_or_else(|e| e.into_inner())
122 }
123
124 pub fn set_current_lsn(&self, lsn: u64) {
125 let mut current = self.current_lsn.write().unwrap_or_else(|e| e.into_inner());
126 *current = (*current).max(lsn);
127 }
128
129 pub fn prune_through(&self, upto_lsn: u64) {
130 let mut records = self.records.write().unwrap_or_else(|e| e.into_inner());
131 while records
132 .front()
133 .map(|(lsn, _)| *lsn <= upto_lsn)
134 .unwrap_or(false)
135 {
136 records.pop_front();
137 }
138 }
139
140 pub fn oldest_lsn(&self) -> Option<u64> {
142 let records = self.records.read().unwrap_or_else(|e| e.into_inner());
143 records.front().map(|(lsn, _)| *lsn)
144 }
145}
146
147fn logical_wal_entry_term(entry: &reddb_file::LogicalWalEntry) -> u64 {
148 if entry.term == 0 {
149 term_from_payload(&entry.data)
150 } else {
151 entry.term
152 }
153}
154
155fn logical_wal_data_with_framing_term(entry: &reddb_file::LogicalWalEntry) -> Vec<u8> {
156 let term = logical_wal_entry_term(entry);
157 match crate::replication::cdc::ChangeRecord::decode(&entry.data) {
158 Ok(mut record)
159 if record.term != term || record.ownership_epoch != entry.ownership_epoch =>
160 {
161 record.term = term;
162 if entry.ownership_epoch.is_some() {
163 record.ownership_epoch = entry.ownership_epoch;
164 }
165 record.encode()
166 }
167 _ => entry.data.clone(),
168 }
169}
170
171#[derive(Debug, Default)]
179struct LogicalWalSpoolState {
180 current_lsn: u64,
181 seek_index: Vec<(u64, u64)>,
185 write_offset: u64,
189 record_count: u64,
192}
193
194impl LogicalWalSpoolState {
195 fn note_record(&mut self, ordinal: u64, lsn: u64, offset: u64) {
199 if ordinal.is_multiple_of(reddb_file::LOGICAL_WAL_SEEK_INDEX_INTERVAL) {
200 if self.seek_index.last().map(|(l, _)| *l) != Some(lsn) {
204 self.seek_index.push((lsn, offset));
205 }
206 }
207 }
208
209 fn seek_floor_offset(&self, since_lsn: u64) -> u64 {
214 match self
215 .seek_index
216 .binary_search_by(|(lsn, _)| lsn.cmp(&since_lsn))
217 {
218 Ok(idx) => self.seek_index[idx].1,
219 Err(0) => 0,
220 Err(idx) => self.seek_index[idx - 1].1,
221 }
222 }
223}
224
225pub struct LogicalWalSpool {
229 path: PathBuf,
230 state: Mutex<LogicalWalSpoolState>,
232}
233
234impl LogicalWalSpool {
235 pub fn path_for(data_path: &Path) -> PathBuf {
236 reddb_file::layout::logical_wal_path(data_path)
237 }
238
239 pub fn open(data_path: &Path) -> io::Result<Self> {
240 let path = Self::path_for(data_path);
241 if let Some(parent) = path.parent() {
242 fs::create_dir_all(parent)?;
243 }
244 if !path.exists() {
245 File::create(&path)?;
246 }
247 let entries = reddb_file::read_and_repair_logical_wal_entries(&path)?;
252 let current_lsn = entries.last().map(|entry| entry.lsn).unwrap_or(0);
253 let (seek_index, write_offset, record_count) =
256 reddb_file::build_logical_wal_seek_index(&path)?;
257 Ok(Self {
258 path,
259 state: Mutex::new(LogicalWalSpoolState {
260 current_lsn,
261 seek_index,
262 write_offset,
263 record_count,
264 }),
265 })
266 }
267
268 pub fn append(&self, lsn: u64, data: &[u8]) -> io::Result<()> {
269 let timestamp_ms = SystemTime::now()
270 .duration_since(UNIX_EPOCH)
271 .map(|d| d.as_millis() as u64)
272 .unwrap_or(0);
273 self.append_with_timestamp(lsn, timestamp_ms, data)
274 }
275
276 pub fn append_with_timestamp(
280 &self,
281 lsn: u64,
282 timestamp_ms: u64,
283 data: &[u8],
284 ) -> io::Result<()> {
285 let (term, ownership_epoch) = authority_from_payload(data);
286 self.append_with_term_epoch_and_timestamp(term, ownership_epoch, lsn, timestamp_ms, data)
287 }
288
289 pub fn append_with_term_and_timestamp(
290 &self,
291 term: u64,
292 lsn: u64,
293 timestamp_ms: u64,
294 data: &[u8],
295 ) -> io::Result<()> {
296 let (_, ownership_epoch) = authority_from_payload(data);
297 self.append_with_term_epoch_and_timestamp(term, ownership_epoch, lsn, timestamp_ms, data)
298 }
299
300 pub fn append_with_term_epoch_and_timestamp(
301 &self,
302 term: u64,
303 ownership_epoch: Option<u64>,
304 lsn: u64,
305 timestamp_ms: u64,
306 data: &[u8],
307 ) -> io::Result<()> {
308 let frame =
312 reddb_file::encode_logical_wal_v3(term, ownership_epoch, lsn, timestamp_ms, data)?;
313 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
314 let mut file = OpenOptions::new()
315 .create(true)
316 .append(true)
317 .open(&self.path)?;
318
319 file.write_all(&frame)?;
320 file.sync_all()?;
325
326 state.current_lsn = state.current_lsn.max(lsn);
327 let record_start = state.write_offset;
331 let ordinal = state.record_count;
332 state.note_record(ordinal, lsn, record_start);
333 state.write_offset = record_start + frame.len() as u64;
334 state.record_count = ordinal + 1;
335 Ok(())
336 }
337
338 pub fn read_since(&self, since_lsn: u64, max_count: usize) -> io::Result<Vec<(u64, Vec<u8>)>> {
339 let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
345 let start_offset = state.seek_floor_offset(since_lsn);
346 let entries = reddb_file::read_logical_wal_entries_from(&self.path, start_offset)?;
347 drop(state);
348 Ok(entries
349 .into_iter()
350 .filter(|entry| entry.lsn > since_lsn)
351 .take(max_count)
352 .map(|entry| (entry.lsn, logical_wal_data_with_framing_term(&entry)))
353 .collect())
354 }
355
356 pub fn read_since_bounded(
357 &self,
358 since_lsn: u64,
359 max_count: usize,
360 max_payload_bytes: usize,
361 ) -> io::Result<Vec<(u64, Vec<u8>)>> {
362 let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
363 let start_offset = state.seek_floor_offset(since_lsn);
364 let entries = reddb_file::read_logical_wal_entries_from_bounded(
365 &self.path,
366 start_offset,
367 since_lsn,
368 max_count,
369 max_payload_bytes,
370 )?;
371 drop(state);
372 Ok(entries
373 .into_iter()
374 .map(|entry| (entry.lsn, logical_wal_data_with_framing_term(&entry)))
375 .collect())
376 }
377
378 #[cfg(test)]
382 fn seek_floor_offset(&self, since_lsn: u64) -> u64 {
383 self.state
384 .lock()
385 .unwrap_or_else(|e| e.into_inner())
386 .seek_floor_offset(since_lsn)
387 }
388
389 pub fn current_lsn(&self) -> u64 {
390 self.state
391 .lock()
392 .unwrap_or_else(|e| e.into_inner())
393 .current_lsn
394 }
395
396 pub(crate) fn restore_lsn_floor(&self, lsn: u64) {
399 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
400 state.current_lsn = state.current_lsn.max(lsn);
401 }
402
403 pub fn oldest_lsn(&self) -> io::Result<Option<u64>> {
404 let _state = self.state.lock().unwrap_or_else(|e| e.into_inner());
405 Ok(reddb_file::read_and_repair_logical_wal_entries(&self.path)?
406 .into_iter()
407 .next()
408 .map(|entry| entry.lsn))
409 }
410
411 pub fn prune_through(&self, upto_lsn: u64) -> io::Result<()> {
412 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
413 let previous_lsn = state.current_lsn;
414 let mut retained: Vec<_> = reddb_file::read_and_repair_logical_wal_entries(&self.path)?
415 .into_iter()
416 .filter(|entry| entry.lsn > upto_lsn)
417 .collect();
418 for entry in &mut retained {
419 entry.term = logical_wal_entry_term(entry);
420 }
421 let temp_path = reddb_file::layout::logical_wal_temp_path(&self.path);
422 for entry in &mut retained {
423 let timestamp_ms = if entry.timestamp_ms > 0 {
431 entry.timestamp_ms
432 } else {
433 SystemTime::now()
434 .duration_since(UNIX_EPOCH)
435 .map(|d| d.as_millis() as u64)
436 .unwrap_or(0)
437 };
438 entry.timestamp_ms = timestamp_ms;
439 }
440 let current_lsn =
441 reddb_file::rewrite_logical_wal_entries(&self.path, &temp_path, &retained)?;
442
443 let (seek_index, write_offset, record_count) =
446 reddb_file::build_logical_wal_seek_index(&self.path)?;
447 state.current_lsn = previous_lsn.max(current_lsn).max(upto_lsn);
448 state.seek_index = seek_index;
449 state.write_offset = write_offset;
450 state.record_count = record_count;
451 Ok(())
452 }
453}
454
455#[derive(Debug, Clone)]
461pub struct ReplicaState {
462 pub id: String,
463 pub last_acked_lsn: u64,
464 pub last_sent_lsn: u64,
465 pub last_durable_lsn: u64,
466 pub apply_error_count: u64,
467 pub divergence_count: u64,
468 pub connected_at_unix_ms: u128,
469 pub last_seen_at_unix_ms: u128,
470 pub region: Option<String>,
475 pub rebootstrapping: bool,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub struct ReplicationProgress {
487 pub lag_lsn: u64,
488 pub safe_replay_lsn: u64,
489}
490
491impl ReplicationProgress {
492 pub fn from_replicas(replicas: &[ReplicaState]) -> Option<Self> {
493 let max_sent_lsn = replicas.iter().map(|replica| replica.last_sent_lsn).max()?;
494 let min_acked_lsn = replicas
495 .iter()
496 .map(|replica| replica.last_acked_lsn)
497 .min()?;
498 let safe_replay_lsn = replicas
499 .iter()
500 .map(|replica| replica.last_durable_lsn)
501 .min()?;
502
503 Some(Self {
504 lag_lsn: max_sent_lsn.saturating_sub(min_acked_lsn),
505 safe_replay_lsn,
506 })
507 }
508}
509
510pub struct PrimaryReplication {
512 pub wal_buffer: Arc<WalBuffer>,
513 pub logical_wal_spool: Option<Arc<LogicalWalSpool>>,
514 pub replicas: RwLock<Vec<ReplicaState>>,
515 pub(crate) routing_endpoints: RwLock<BTreeMap<String, reddb_wire::routing::RoutingEndpoints>>,
516 wal_appended: (Mutex<u64>, Condvar),
517 metadata_store: Option<Arc<crate::storage::unified::UnifiedStore>>,
518 metadata_error: Mutex<Option<String>>,
519 slot_path: Option<PathBuf>,
520 slot_catalog_path: Option<PathBuf>,
521 primary_replica_file_plan: Option<reddb_file::PrimaryReplicaFilePlan>,
522 primary_replica_wal_lock: Mutex<()>,
523 slots: RwLock<BTreeMap<String, ReplicationSlot>>,
524 slot_timeline: reddb_file::TimelineId,
525 slot_retention_max_lag_lsn: u64,
526 slot_idle_timeout_ms: u64,
527 pub commit_waiter: Arc<crate::replication::commit_waiter::CommitWaiter>,
531 topology_epoch: std::sync::atomic::AtomicU64,
538 partial_resync_count: std::sync::atomic::AtomicU64,
544 full_resync_count: std::sync::atomic::AtomicU64,
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum ResumeMode {
556 PartialResync { resume_lsn: u64 },
560 FullRebootstrap {
563 cause: ReplicationSlotInvalidationCause,
564 },
565}
566
567const REBOOTSTRAP_PROOF_TOKEN_BYTES_MAX: usize = 1024;
568
569#[derive(Debug)]
570struct ReplicaRebootstrapProof {
571 replica_id: String,
572 timeline: u64,
573 checkpoint_lsn: u64,
574 token: String,
575}
576
577impl ReplicaRebootstrapProof {
578 const MAGIC: &'static [u8; 5] = b"RBRP1";
579
580 fn encode(&self) -> io::Result<Vec<u8>> {
581 let replica_id_bytes = self.replica_id.as_bytes();
582 let token_bytes = self.token.as_bytes();
583 let replica_id_len = u16::try_from(replica_id_bytes.len()).map_err(|_| {
584 io::Error::new(io::ErrorKind::InvalidInput, "replica identity is too long")
585 })?;
586 let token_len = u16::try_from(token_bytes.len()).map_err(|_| {
587 io::Error::new(io::ErrorKind::InvalidInput, "snapshot token is too long")
588 })?;
589 let mut bytes = Vec::with_capacity(25 + replica_id_bytes.len() + token_bytes.len());
590 bytes.extend_from_slice(Self::MAGIC);
591 bytes.extend_from_slice(&self.timeline.to_le_bytes());
592 bytes.extend_from_slice(&self.checkpoint_lsn.to_le_bytes());
593 bytes.extend_from_slice(&replica_id_len.to_le_bytes());
594 bytes.extend_from_slice(&token_len.to_le_bytes());
595 bytes.extend_from_slice(replica_id_bytes);
596 bytes.extend_from_slice(token_bytes);
597 Ok(bytes)
598 }
599
600 fn decode(bytes: &[u8]) -> io::Result<Self> {
601 if bytes.len() < 25 || &bytes[..5] != Self::MAGIC {
602 return Err(io::Error::new(
603 io::ErrorKind::InvalidData,
604 "invalid replica rebootstrap proof header",
605 ));
606 }
607 let timeline = u64::from_le_bytes(
608 bytes[5..13]
609 .try_into()
610 .expect("invariant: proof timeline slice has eight bytes"),
611 );
612 let checkpoint_lsn = u64::from_le_bytes(
613 bytes[13..21]
614 .try_into()
615 .expect("invariant: proof checkpoint slice has eight bytes"),
616 );
617 let replica_id_len = usize::from(u16::from_le_bytes(
618 bytes[21..23]
619 .try_into()
620 .expect("invariant: proof identity length slice has two bytes"),
621 ));
622 let token_len = usize::from(u16::from_le_bytes(
623 bytes[23..25]
624 .try_into()
625 .expect("invariant: proof token length slice has two bytes"),
626 ));
627 let expected_len = 25usize
628 .checked_add(replica_id_len)
629 .and_then(|len| len.checked_add(token_len))
630 .ok_or_else(|| {
631 io::Error::new(
632 io::ErrorKind::InvalidData,
633 "rebootstrap proof length overflow",
634 )
635 })?;
636 if bytes.len() != expected_len {
637 return Err(io::Error::new(
638 io::ErrorKind::InvalidData,
639 "invalid replica rebootstrap proof length",
640 ));
641 }
642 let replica_id_end = 25 + replica_id_len;
643 let replica_id = std::str::from_utf8(&bytes[25..replica_id_end])
644 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
645 .to_string();
646 let token = std::str::from_utf8(&bytes[replica_id_end..expected_len])
647 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
648 .to_string();
649 Ok(Self {
650 replica_id,
651 timeline,
652 checkpoint_lsn,
653 token,
654 })
655 }
656}
657
658fn replica_rebootstrap_proof_key(replica_id: &str) -> String {
659 format!("replication.rebootstrap.proof.{}", hex::encode(replica_id))
660}
661
662impl PrimaryReplication {
663 pub fn slot_path_for(data_path: &Path) -> PathBuf {
664 reddb_file::layout::legacy_logical_slots_path(data_path)
665 }
666
667 pub fn primary_replica_root_for(data_path: &Path) -> PathBuf {
668 reddb_file::layout::primary_replica_root(data_path)
669 }
670
671 pub fn slot_catalog_path_for(data_path: &Path) -> PathBuf {
672 Self::primary_replica_file_plan_for(data_path).slots_path()
673 }
674
675 fn primary_replica_file_plan_for(data_path: &Path) -> reddb_file::PrimaryReplicaFilePlan {
676 let root = Self::primary_replica_root_for(data_path);
677 let timeline =
678 Self::primary_replica_current_timeline_for_root(&root).unwrap_or_else(|err| {
679 warn!(
680 target: "reddb::replication::primary",
681 error = %err,
682 "failed to read primary-replica timeline history; using initial timeline"
683 );
684 reddb_file::TimelineId::initial()
685 });
686 reddb_file::PrimaryReplicaFilePlan::new(root, timeline)
687 }
688
689 fn primary_replica_current_timeline_for_root(
690 root: &Path,
691 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
692 let path = reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
693 .timeline_history_path();
694 match reddb_file::TimelineHistory::read_from_path(&path) {
695 Ok(history) => Ok(history
696 .current()
697 .unwrap_or_else(reddb_file::TimelineId::initial)),
698 Err(reddb_file::RdbFileError::Io(err))
699 if err.kind() == std::io::ErrorKind::NotFound =>
700 {
701 Ok(reddb_file::TimelineId::initial())
702 }
703 Err(err) => Err(err),
704 }
705 }
706
707 pub fn new(data_path: Option<&Path>) -> Self {
708 Self::new_with_config(data_path, &crate::replication::ReplicationConfig::primary())
709 }
710
711 pub fn new_with_config(
712 data_path: Option<&Path>,
713 config: &crate::replication::ReplicationConfig,
714 ) -> Self {
715 let now_ms = crate::utils::now_unix_millis() as u128;
716 let slot_path = data_path.map(Self::slot_path_for);
717 let slot_catalog_path = data_path.map(Self::slot_catalog_path_for);
718 let primary_replica_file_plan = data_path.map(Self::primary_replica_file_plan_for);
719 let mut slots = load_replication_slot_catalog(slot_catalog_path.as_deref(), now_ms);
720 slots.extend(load_replication_slots(slot_path.as_deref(), now_ms));
721 let logical_wal_spool = data_path
722 .and_then(|path| LogicalWalSpool::open(path).ok())
723 .map(Arc::new);
724 let current_lsn = logical_wal_spool
725 .as_ref()
726 .map(|spool| spool.current_lsn())
727 .unwrap_or(0);
728 Self {
729 wal_buffer: Arc::new(WalBuffer::new(100_000)),
730 logical_wal_spool,
731 replicas: RwLock::new(Vec::new()),
732 routing_endpoints: RwLock::new(BTreeMap::new()),
733 wal_appended: (Mutex::new(current_lsn), Condvar::new()),
734 metadata_store: None,
735 metadata_error: Mutex::new(None),
736 slot_path,
737 slot_catalog_path,
738 primary_replica_file_plan,
739 primary_replica_wal_lock: Mutex::new(()),
740 slots: RwLock::new(slots),
741 slot_timeline: reddb_file::TimelineId::initial(),
744 slot_retention_max_lag_lsn: config.slot_retention_max_lag_lsn,
745 slot_idle_timeout_ms: config.slot_idle_timeout_ms,
746 commit_waiter: Arc::new(crate::replication::commit_waiter::CommitWaiter::new()),
747 topology_epoch: std::sync::atomic::AtomicU64::new(0),
748 partial_resync_count: std::sync::atomic::AtomicU64::new(0),
749 full_resync_count: std::sync::atomic::AtomicU64::new(0),
750 }
751 }
752
753 pub(crate) fn new_with_store(
754 data_path: Option<&Path>,
755 config: &crate::replication::ReplicationConfig,
756 store: Arc<crate::storage::unified::UnifiedStore>,
757 ) -> io::Result<Self> {
758 let mut primary = Self::new_with_config(data_path, config);
759 if let Some(path) = data_path.filter(|_| primary.logical_wal_spool.is_none()) {
760 primary.logical_wal_spool = Some(Arc::new(LogicalWalSpool::open(path)?));
763 }
764 let stored_catalog = super::metadata::read(&store, super::metadata::SLOTS)?;
765 if let Some(bytes) = &stored_catalog {
766 let catalog =
767 reddb_file::ReplicationSlotCatalog::decode(&bytes).map_err(io::Error::other)?;
768 primary.slots = RwLock::new(
769 catalog
770 .slots
771 .into_iter()
772 .map(|slot| (slot.replica_id.clone(), slot))
773 .collect(),
774 );
775 } else {
776 let mut valid_source = false;
779 let mut import_error = None;
780 for path in [&primary.slot_catalog_path, &primary.slot_path]
781 .into_iter()
782 .flatten()
783 {
784 if path.try_exists()? {
785 let result = if primary.slot_catalog_path.as_ref() == Some(path) {
786 reddb_file::ReplicationSlotCatalog::read_from_path(path)
787 } else {
788 reddb_file::ReplicationSlotCatalog::read_legacy_json_from_path(
789 path,
790 crate::utils::now_unix_millis() as u128,
791 )
792 };
793 match result {
794 Ok(_) => valid_source = true,
795 Err(error) => import_error = Some(error),
796 }
797 }
798 }
799 if !valid_source {
800 if let Some(error) = import_error {
801 return Err(io::Error::other(error));
802 }
803 }
804 }
805 {
806 let now_ms = crate::utils::now_unix_millis();
807 let history = match &primary.primary_replica_file_plan {
808 Some(plan) => super::metadata::timeline(&store, &plan.timeline_history_path())?,
809 None => reddb_file::TimelineHistory::new(now_ms),
810 };
811 let mut slots = primary
812 .slots
813 .write()
814 .unwrap_or_else(|error| error.into_inner());
815 primary.slot_timeline =
816 slots::align_slots_with_timeline(&mut slots, &history, u128::from(now_ms))?;
817 let bytes = slots::slot_catalog_from_map(&slots, primary.slot_timeline)
818 .and_then(|catalog| catalog.encode())
819 .map_err(io::Error::other)?;
820 if stored_catalog.as_deref() != Some(bytes.as_slice()) {
821 super::metadata::write(&store, super::metadata::SLOTS, &bytes)?;
822 }
823 }
824 for path in [&primary.slot_catalog_path, &primary.slot_path]
825 .into_iter()
826 .flatten()
827 {
828 super::metadata::retire_legacy(path)?;
829 }
830 primary.metadata_store = Some(store);
831 Ok(primary)
832 }
833
834 fn current_timeline(
835 &self,
836 root: &Path,
837 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
838 if let Some(store) = &self.metadata_store {
839 let path =
840 reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
841 .timeline_history_path();
842 return super::metadata::timeline(store, &path)
843 .map(|history| {
844 history
845 .current()
846 .unwrap_or_else(reddb_file::TimelineId::initial)
847 })
848 .map_err(reddb_file::RdbFileError::Io);
849 }
850 Self::primary_replica_current_timeline_for_root(root)
851 }
852
853 pub fn append_logical_record(&self, lsn: u64, encoded: Vec<u8>) {
854 if let Err(error) = self.try_append_logical_record(lsn, encoded) {
855 warn!(%error, lsn, "failed to publish logical replication record");
856 }
857 }
858
859 pub(crate) fn try_append_logical_record(&self, lsn: u64, encoded: Vec<u8>) -> io::Result<()> {
863 let _guard = self
864 .primary_replica_wal_lock
865 .lock()
866 .unwrap_or_else(|error| error.into_inner());
867 if let Some(plan) = &self.primary_replica_file_plan {
868 let timeline = self
869 .current_timeline(&plan.root)
870 .map_err(io::Error::other)?;
871 let plan = reddb_file::PrimaryReplicaFilePlan::new(plan.root.clone(), timeline);
872 plan.append_wal_record(lsn, &encoded)
873 .map_err(io::Error::other)?;
874 }
875 if let Some(spool) = &self.logical_wal_spool {
876 if spool.current_lsn() < lsn {
877 spool.append(lsn, &encoded)?;
878 }
879 }
880 if self.wal_buffer.current_lsn() < lsn {
881 self.wal_buffer.append(lsn, encoded);
882 }
883 let (lock, cvar) = &self.wal_appended;
884 let mut latest = lock.lock().unwrap_or_else(|error| error.into_inner());
885 *latest = (*latest).max(lsn);
886 cvar.notify_all();
887 Ok(())
888 }
889
890 pub fn wait_for_logical_lsn_after(&self, since_lsn: u64, timeout: Duration) -> bool {
891 if self.current_logical_lsn() > since_lsn {
892 return true;
893 }
894 let deadline = Instant::now() + timeout;
895 let (lock, cvar) = &self.wal_appended;
896 let mut latest = lock.lock().unwrap_or_else(|e| e.into_inner());
897 while *latest <= since_lsn {
898 let now = Instant::now();
899 if now >= deadline {
900 return false;
901 }
902 let remaining = deadline.saturating_duration_since(now);
903 let (guard, result) = cvar
904 .wait_timeout(latest, remaining)
905 .unwrap_or_else(|e| e.into_inner());
906 latest = guard;
907 if result.timed_out() && *latest <= since_lsn {
908 return false;
909 }
910 }
911 true
912 }
913
914 pub fn register_replica(&self, id: String) -> u64 {
915 self.register_replica_with_region(id, None)
916 }
917
918 pub fn register_replica_with_region(&self, id: String, region: Option<String>) -> u64 {
935 self.register_replica_with_region_at(id, region, self.current_logical_lsn())
936 }
937
938 fn register_replica_with_region_at(
939 &self,
940 id: String,
941 region: Option<String>,
942 initial_lsn: u64,
943 ) -> u64 {
944 let now_ms = crate::utils::now_unix_millis() as u128;
945 let resume_lsn = self.ensure_slot(&id, initial_lsn);
946 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
947 if let Some(existing) = replicas.iter_mut().find(|r| r.id == id) {
948 existing.last_seen_at_unix_ms = now_ms;
949 if region.is_some() {
950 existing.region = region;
951 }
952 return resume_lsn;
953 }
954 replicas.push(ReplicaState {
955 id,
956 last_acked_lsn: resume_lsn,
957 last_sent_lsn: resume_lsn,
958 last_durable_lsn: resume_lsn,
959 apply_error_count: 0,
960 divergence_count: 0,
961 connected_at_unix_ms: now_ms,
962 last_seen_at_unix_ms: now_ms,
963 region,
964 rebootstrapping: false,
965 });
966 drop(replicas);
967 self.bump_topology_epoch();
968 resume_lsn
969 }
970
971 pub fn set_replica_rebootstrapping(&self, id: &str, rebootstrapping: bool) -> bool {
986 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
987 let Some(state) = replicas.iter_mut().find(|r| r.id == id) else {
988 return false;
989 };
990 if state.rebootstrapping == rebootstrapping {
991 return true;
992 }
993 state.rebootstrapping = rebootstrapping;
994 drop(replicas);
995 self.bump_topology_epoch();
996 true
997 }
998
999 pub fn ensure_replica_registered(&self, id: &str, requested_since_lsn: u64) -> bool {
1010 let already = self
1011 .replicas
1012 .read()
1013 .unwrap_or_else(|e| e.into_inner())
1014 .iter()
1015 .any(|r| r.id == id);
1016 if already {
1017 return false;
1018 }
1019 let initial_lsn = requested_since_lsn.min(self.current_logical_lsn());
1020 self.register_replica_with_region_at(id.to_string(), None, initial_lsn);
1021 true
1022 }
1023
1024 pub fn unregister_replica(&self, id: &str) -> bool {
1028 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
1029 let before = replicas.len();
1030 replicas.retain(|r| r.id != id);
1031 let removed = replicas.len() != before;
1032 drop(replicas);
1033 if removed {
1034 self.routing_endpoints
1035 .write()
1036 .expect("replica routing endpoint map lock must not be poisoned")
1037 .remove(id);
1038 self.commit_waiter.drop_replica(id);
1039 self.bump_topology_epoch();
1040 }
1041 removed
1042 }
1043
1044 pub fn topology_epoch(&self) -> u64 {
1047 self.topology_epoch
1048 .load(std::sync::atomic::Ordering::Relaxed)
1049 }
1050
1051 pub fn bump_topology_epoch(&self) {
1059 self.topology_epoch
1060 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1061 }
1062
1063 pub fn ack_replica(&self, id: &str, lsn: u64) {
1064 let now_ms = crate::utils::now_unix_millis() as u128;
1065 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
1066 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
1067 r.last_acked_lsn = r.last_acked_lsn.max(lsn);
1068 r.last_durable_lsn = r.last_durable_lsn.max(lsn);
1069 r.last_seen_at_unix_ms = now_ms;
1070 }
1071 drop(replicas);
1072 self.commit_waiter.record_replica_ack(id, lsn);
1073 }
1074
1075 pub fn ack_replica_lsn(&self, id: &str, applied_lsn: u64, durable_lsn: u64) {
1081 self.ack_replica_lsn_with_observability(id, applied_lsn, durable_lsn, 0, 0);
1082 }
1083
1084 pub fn ack_replica_lsn_with_observability(
1085 &self,
1086 id: &str,
1087 applied_lsn: u64,
1088 durable_lsn: u64,
1089 apply_error_count: u64,
1090 divergence_count: u64,
1091 ) {
1092 let now_ms = crate::utils::now_unix_millis() as u128;
1093 self.advance_slot(id, applied_lsn, durable_lsn, now_ms);
1094 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
1095 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
1096 r.last_acked_lsn = r.last_acked_lsn.max(applied_lsn);
1097 r.last_durable_lsn = r.last_durable_lsn.max(durable_lsn);
1098 r.apply_error_count = r.apply_error_count.max(apply_error_count);
1099 r.divergence_count = r.divergence_count.max(divergence_count);
1100 r.last_seen_at_unix_ms = now_ms;
1101 }
1102 drop(replicas);
1106 self.commit_waiter.record_replica_ack(id, durable_lsn);
1107 }
1108
1109 pub fn note_replica_pull(&self, id: &str, last_sent_lsn: u64) {
1114 let now_ms = crate::utils::now_unix_millis() as u128;
1115 self.touch_slot(id, now_ms);
1116 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
1117 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
1118 r.last_sent_lsn = r.last_sent_lsn.max(last_sent_lsn);
1119 r.last_seen_at_unix_ms = now_ms;
1120 }
1121 }
1122
1123 pub fn replica_snapshots(&self) -> Vec<ReplicaState> {
1127 self.replicas
1128 .read()
1129 .unwrap_or_else(|e| e.into_inner())
1130 .clone()
1131 }
1132
1133 pub fn replication_progress(&self) -> Option<ReplicationProgress> {
1134 let replicas = self.replicas.read().unwrap_or_else(|e| e.into_inner());
1135 ReplicationProgress::from_replicas(&replicas)
1136 }
1137
1138 pub fn slot_snapshots(&self) -> Vec<ReplicationSlot> {
1139 self.slots
1140 .read()
1141 .unwrap_or_else(|e| e.into_inner())
1142 .values()
1143 .cloned()
1144 .collect()
1145 }
1146
1147 pub fn retention_floor_lsn(&self) -> Option<u64> {
1148 let slots = self.slots.read().unwrap_or_else(|e| e.into_inner());
1151 if self.ensure_slot_metadata_durable().is_err() {
1152 return Some(0);
1153 }
1154 slots
1155 .values()
1156 .filter(|slot| slot.invalidation_reason.is_none())
1157 .map(|slot| slot.restart_lsn)
1158 .min()
1159 }
1160
1161 pub fn prune_retained_wal_through(&self, archived_lsn: u64) -> io::Result<u64> {
1162 self.enforce_retention_limits(crate::utils::now_unix_millis() as u128);
1163 let prune_lsn = self
1164 .retention_floor_lsn()
1165 .map(|floor| floor.min(archived_lsn))
1166 .unwrap_or(archived_lsn);
1167 if prune_lsn > 0 {
1168 if let Some(spool) = &self.logical_wal_spool {
1169 spool.prune_through(prune_lsn)?;
1170 }
1171 self.wal_buffer.prune_through(prune_lsn);
1172 }
1173 Ok(prune_lsn)
1174 }
1175
1176 pub fn replica_count(&self) -> usize {
1177 self.replicas
1178 .read()
1179 .unwrap_or_else(|e| e.into_inner())
1180 .len()
1181 }
1182
1183 pub fn current_logical_lsn(&self) -> u64 {
1187 self.logical_wal_spool
1188 .as_ref()
1189 .map(|spool| spool.current_lsn())
1190 .unwrap_or_else(|| self.wal_buffer.current_lsn())
1191 }
1192
1193 fn ensure_slot(&self, id: &str, initial_lsn: u64) -> u64 {
1194 let now_ms = crate::utils::now_unix_millis() as u128;
1195 let _publication = self
1196 .metadata_store
1197 .as_ref()
1198 .map(|store| store.vault_publication_lock.lock());
1199 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1200 if let Some(slot) = slots.get_mut(id) {
1201 slot.last_seen_at_unix_ms = now_ms;
1202 let restart_lsn = slot.restart_lsn;
1203 self.persist_slots_locked(&slots);
1204 return restart_lsn;
1205 }
1206 let mut slot = ReplicationSlot::new(id.to_string(), self.slot_timeline, initial_lsn);
1207 slot.last_seen_at_unix_ms = now_ms;
1208 slots.insert(id.to_string(), slot);
1209 let restart_lsn = initial_lsn;
1210 self.persist_slots_locked(&slots);
1211 restart_lsn
1212 }
1213
1214 fn advance_slot(&self, id: &str, confirmed_lsn: u64, restart_lsn: u64, now_ms: u128) {
1215 let _publication = self
1216 .metadata_store
1217 .as_ref()
1218 .map(|store| store.vault_publication_lock.lock());
1219 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1220 let slot = slots.entry(id.to_string()).or_insert_with(|| {
1221 let mut slot = ReplicationSlot::new(id.to_string(), self.slot_timeline, 0);
1222 slot.last_seen_at_unix_ms = now_ms;
1223 slot
1224 });
1225 if slot.invalidation_reason.is_some() {
1226 return;
1227 }
1228 slot.confirmed_write_lsn = slot.confirmed_lsn().max(confirmed_lsn).max(restart_lsn);
1229 slot.restart_lsn = slot.restart_lsn.max(restart_lsn);
1230 slot.confirmed_flush_lsn = slot.confirmed_flush_lsn.max(slot.restart_lsn);
1231 slot.confirmed_apply_lsn = slot.confirmed_apply_lsn.max(slot.restart_lsn);
1232 slot.last_seen_at_unix_ms = now_ms;
1233 self.persist_slots_locked(&slots);
1234 }
1235
1236 pub fn touch_slot(&self, id: &str, now_ms: u128) {
1237 let _publication = self
1238 .metadata_store
1239 .as_ref()
1240 .map(|store| store.vault_publication_lock.lock());
1241 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1242 let mut changed = false;
1243 if let Some(slot) = slots.get_mut(id) {
1244 if slot.invalidation_reason.is_none() {
1245 slot.last_seen_at_unix_ms = now_ms;
1246 changed = true;
1247 }
1248 }
1249 if changed {
1250 self.persist_slots_locked(&slots);
1251 }
1252 }
1253
1254 pub fn enforce_retention_limits(
1255 &self,
1256 now_ms: u128,
1257 ) -> Vec<(String, ReplicationSlotInvalidationCause)> {
1258 let current_lsn = self.current_logical_lsn();
1259 let mut invalidated = Vec::new();
1260 let _publication = self
1261 .metadata_store
1262 .as_ref()
1263 .map(|store| store.vault_publication_lock.lock());
1264 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1265 for slot in slots.values_mut() {
1266 if slot.invalidation_reason.is_some() {
1267 continue;
1268 }
1269 let reason = if self.slot_retention_max_lag_lsn > 0
1270 && current_lsn.saturating_sub(slot.restart_lsn) > self.slot_retention_max_lag_lsn
1271 {
1272 Some(ReplicationSlotInvalidationCause::Horizon)
1273 } else if self.slot_idle_timeout_ms > 0
1274 && now_ms.saturating_sub(slot.last_seen_at_unix_ms)
1275 > u128::from(self.slot_idle_timeout_ms)
1276 {
1277 Some(ReplicationSlotInvalidationCause::IdleTimeout)
1278 } else {
1279 None
1280 };
1281 if let Some(reason) = reason {
1282 slot.invalidation_reason = Some(reason);
1283 slot.invalidated_at_unix_ms = Some(now_ms);
1284 invalidated.push((slot.replica_id.clone(), reason));
1285 }
1286 }
1287 if !invalidated.is_empty() {
1288 self.persist_slots_locked(&slots);
1289 }
1290 invalidated
1291 }
1292
1293 pub fn slot_rebootstrap_reason(
1294 &self,
1295 id: &str,
1296 requested_since_lsn: u64,
1297 oldest_available_lsn: Option<u64>,
1298 ) -> Option<ReplicationSlotInvalidationCause> {
1299 let now_ms = crate::utils::now_unix_millis() as u128;
1300 let _publication = self
1301 .metadata_store
1302 .as_ref()
1303 .map(|store| store.vault_publication_lock.lock());
1304 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1305 let slot = slots.get_mut(id)?;
1306 if let Some(reason) = slot.invalidation_reason {
1307 return Some(reason);
1308 }
1309 let slot_floor = slot.restart_lsn.max(requested_since_lsn);
1310 if oldest_available_lsn
1311 .map(|oldest| oldest > slot_floor.saturating_add(1))
1312 .unwrap_or(false)
1313 {
1314 slot.invalidation_reason = Some(ReplicationSlotInvalidationCause::WalRemoved);
1315 slot.invalidated_at_unix_ms = Some(now_ms);
1316 self.persist_slots_locked(&slots);
1317 return Some(ReplicationSlotInvalidationCause::WalRemoved);
1318 }
1319 None
1320 }
1321
1322 pub fn record_replica_rebootstrap_snapshot(
1323 &self,
1324 replica_id: &str,
1325 token: &str,
1326 timeline: reddb_file::TimelineId,
1327 checkpoint_lsn: u64,
1328 resumed_transfer: bool,
1329 ) -> io::Result<bool> {
1330 if token.is_empty() || token.len() > REBOOTSTRAP_PROOF_TOKEN_BYTES_MAX {
1331 return Err(io::Error::new(
1332 io::ErrorKind::InvalidInput,
1333 "invalid replica rebootstrap snapshot token",
1334 ));
1335 }
1336 if timeline != self.slot_timeline || checkpoint_lsn > self.current_logical_lsn() {
1337 return Err(io::Error::new(
1338 io::ErrorKind::InvalidInput,
1339 "replica rebootstrap snapshot is outside the primary frontier",
1340 ));
1341 }
1342 let _publication = self
1343 .metadata_store
1344 .as_ref()
1345 .map(|store| store.vault_publication_lock.lock());
1346 let slots = self.slots.read().unwrap_or_else(|error| error.into_inner());
1347 let Some(slot) = slots.get(replica_id) else {
1348 return Err(io::Error::new(
1349 io::ErrorKind::NotFound,
1350 "replica rebootstrap slot is not registered",
1351 ));
1352 };
1353 if slot.invalidation_reason.is_none() {
1354 return Ok(false);
1355 }
1356 let store = self.metadata_store.as_ref().ok_or_else(|| {
1357 io::Error::new(
1358 io::ErrorKind::Unsupported,
1359 "replica rebootstrap proof requires native metadata",
1360 )
1361 })?;
1362 let key = replica_rebootstrap_proof_key(replica_id);
1363 let proof = ReplicaRebootstrapProof {
1364 replica_id: replica_id.to_string(),
1365 timeline: timeline.0,
1366 checkpoint_lsn,
1367 token: token.to_string(),
1368 };
1369 if resumed_transfer {
1370 let bytes = super::metadata::read(store, &key)?
1371 .filter(|bytes| !bytes.is_empty())
1372 .ok_or_else(|| {
1373 io::Error::new(
1374 io::ErrorKind::InvalidData,
1375 "replica rebootstrap snapshot token is no longer active",
1376 )
1377 })?;
1378 let recorded = ReplicaRebootstrapProof::decode(&bytes)?;
1379 if recorded.replica_id != proof.replica_id
1380 || recorded.timeline != proof.timeline
1381 || recorded.checkpoint_lsn != proof.checkpoint_lsn
1382 || !crate::crypto::constant_time_eq(
1383 recorded.token.as_bytes(),
1384 proof.token.as_bytes(),
1385 )
1386 {
1387 return Err(io::Error::new(
1388 io::ErrorKind::PermissionDenied,
1389 "replica rebootstrap snapshot token does not match the issued snapshot",
1390 ));
1391 }
1392 return Ok(true);
1393 }
1394 let bytes = proof.encode()?;
1395 super::metadata::write(store, &key, &bytes)?;
1396 Ok(true)
1397 }
1398
1399 pub fn accept_replica_rebootstrap_snapshot(
1400 &self,
1401 replica_id: &str,
1402 token: Option<&str>,
1403 installed_lsn: u64,
1404 ) -> io::Result<bool> {
1405 let Some(token) = token else {
1406 return Ok(false);
1407 };
1408 if token.is_empty() || token.len() > REBOOTSTRAP_PROOF_TOKEN_BYTES_MAX {
1409 return Err(io::Error::new(
1410 io::ErrorKind::InvalidInput,
1411 "invalid installed replica snapshot token",
1412 ));
1413 }
1414 let current_lsn = self.current_logical_lsn();
1415 let _publication = self
1416 .metadata_store
1417 .as_ref()
1418 .map(|store| store.vault_publication_lock.lock());
1419 let mut slots = self
1420 .slots
1421 .write()
1422 .unwrap_or_else(|error| error.into_inner());
1423 let Some(slot) = slots.get(replica_id) else {
1424 return Err(io::Error::new(
1425 io::ErrorKind::NotFound,
1426 "installed replica snapshot has no registered slot",
1427 ));
1428 };
1429 if slot.invalidation_reason.is_none() {
1430 return Ok(false);
1431 }
1432 let store = self.metadata_store.as_ref().ok_or_else(|| {
1433 io::Error::new(
1434 io::ErrorKind::Unsupported,
1435 "installed replica snapshot proof requires native metadata",
1436 )
1437 })?;
1438 let key = replica_rebootstrap_proof_key(replica_id);
1439 let Some(bytes) = super::metadata::read(store, &key)?.filter(|bytes| !bytes.is_empty())
1440 else {
1441 return Ok(false);
1445 };
1446 let proof = ReplicaRebootstrapProof::decode(&bytes)?;
1447 if proof.replica_id != replica_id
1448 || proof.timeline != self.slot_timeline.0
1449 || slot.timeline != self.slot_timeline
1450 || proof.checkpoint_lsn != installed_lsn
1451 || installed_lsn > current_lsn
1452 || !crate::crypto::constant_time_eq(proof.token.as_bytes(), token.as_bytes())
1453 {
1454 return Err(io::Error::new(
1455 io::ErrorKind::PermissionDenied,
1456 "installed replica snapshot does not match the issued checkpoint",
1457 ));
1458 }
1459
1460 super::metadata::write(store, &key, &[])?;
1464 let mut repaired = slots.clone();
1465 let repaired_slot = repaired
1466 .get_mut(replica_id)
1467 .expect("invariant: repaired slot was cloned from the registered slot map");
1468 repaired_slot.timeline = self.slot_timeline;
1469 repaired_slot.restart_lsn = installed_lsn;
1470 repaired_slot.confirmed_write_lsn = installed_lsn;
1471 repaired_slot.confirmed_flush_lsn = installed_lsn;
1472 repaired_slot.confirmed_apply_lsn = installed_lsn;
1473 repaired_slot.active = true;
1474 repaired_slot.last_seen_at_unix_ms = crate::utils::now_unix_millis() as u128;
1475 repaired_slot.invalidation_reason = None;
1476 repaired_slot.invalidated_at_unix_ms = None;
1477 let bytes = slots::slot_catalog_from_map(&repaired, self.slot_timeline)
1478 .and_then(|catalog| catalog.encode())
1479 .map_err(io::Error::other)?;
1480 super::metadata::write(store, super::metadata::SLOTS, &bytes)?;
1481 *slots = repaired;
1482 drop(slots);
1483 self.set_replica_rebootstrapping(replica_id, false);
1484 Ok(true)
1485 }
1486
1487 pub fn plan_replica_resume(
1496 &self,
1497 id: &str,
1498 requested_since_lsn: u64,
1499 oldest_available_lsn: Option<u64>,
1500 ) -> ResumeMode {
1501 if let Some(cause) =
1502 self.slot_rebootstrap_reason(id, requested_since_lsn, oldest_available_lsn)
1503 {
1504 self.full_resync_count
1505 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1506 return ResumeMode::FullRebootstrap { cause };
1507 }
1508 let resume_lsn = self
1509 .slot_snapshots()
1510 .into_iter()
1511 .find(|slot| slot.replica_id == id)
1512 .map(|slot| requested_since_lsn.max(slot.restart_lsn))
1513 .unwrap_or(requested_since_lsn);
1514 self.partial_resync_count
1515 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1516 ResumeMode::PartialResync { resume_lsn }
1517 }
1518
1519 pub fn partial_resync_count(&self) -> u64 {
1522 self.partial_resync_count
1523 .load(std::sync::atomic::Ordering::Relaxed)
1524 }
1525
1526 pub fn full_resync_count(&self) -> u64 {
1530 self.full_resync_count
1531 .load(std::sync::atomic::Ordering::Relaxed)
1532 }
1533
1534 fn persist_slots_locked(&self, slots: &BTreeMap<String, ReplicationSlot>) {
1535 if let Some(store) = &self.metadata_store {
1536 let result = slots::slot_catalog_from_map(slots, self.slot_timeline)
1537 .and_then(|catalog| catalog.encode())
1538 .map_err(io::Error::other)
1539 .and_then(|bytes| super::metadata::write(store, super::metadata::SLOTS, &bytes));
1540 let mut last_error = self
1541 .metadata_error
1542 .lock()
1543 .unwrap_or_else(|error| error.into_inner());
1544 if let Err(error) = result {
1545 *last_error = Some(error.to_string());
1546 warn!(target: "reddb::replication::slots", %error,
1547 "failed to persist replication slots inside database");
1548 } else {
1549 *last_error = None;
1550 }
1551 return;
1552 }
1553 if let Err(err) =
1554 persist_replication_slots(self.slot_path.as_deref(), slots, self.slot_timeline)
1555 {
1556 warn!(
1557 target: "reddb::replication::slots",
1558 error = %err,
1559 "failed to persist replication slots"
1560 );
1561 }
1562 if let Err(err) = persist_replication_slot_catalog(
1563 self.slot_catalog_path.as_deref(),
1564 slots,
1565 self.slot_timeline,
1566 ) {
1567 warn!(
1568 target: "reddb::replication::slots",
1569 error = %err,
1570 "failed to persist binary replication slot catalog"
1571 );
1572 }
1573 }
1574
1575 pub(crate) fn ensure_slot_metadata_durable(&self) -> io::Result<()> {
1576 match self
1577 .metadata_error
1578 .lock()
1579 .unwrap_or_else(|error| error.into_inner())
1580 .as_ref()
1581 {
1582 Some(error) => Err(io::Error::other(error.clone())),
1583 None => Ok(()),
1584 }
1585 }
1586}
1587
1588#[cfg(test)]
1589mod tests;