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 current_lsn(&self) -> u64 {
94 *self.current_lsn.read().unwrap_or_else(|e| e.into_inner())
95 }
96
97 pub fn set_current_lsn(&self, lsn: u64) {
98 let mut current = self.current_lsn.write().unwrap_or_else(|e| e.into_inner());
99 *current = (*current).max(lsn);
100 }
101
102 pub fn prune_through(&self, upto_lsn: u64) {
103 let mut records = self.records.write().unwrap_or_else(|e| e.into_inner());
104 while records
105 .front()
106 .map(|(lsn, _)| *lsn <= upto_lsn)
107 .unwrap_or(false)
108 {
109 records.pop_front();
110 }
111 }
112
113 pub fn oldest_lsn(&self) -> Option<u64> {
115 let records = self.records.read().unwrap_or_else(|e| e.into_inner());
116 records.front().map(|(lsn, _)| *lsn)
117 }
118}
119
120fn logical_wal_entry_term(entry: &reddb_file::LogicalWalEntry) -> u64 {
121 if entry.term == 0 {
122 term_from_payload(&entry.data)
123 } else {
124 entry.term
125 }
126}
127
128fn logical_wal_data_with_framing_term(entry: &reddb_file::LogicalWalEntry) -> Vec<u8> {
129 let term = logical_wal_entry_term(entry);
130 match crate::replication::cdc::ChangeRecord::decode(&entry.data) {
131 Ok(mut record)
132 if record.term != term || record.ownership_epoch != entry.ownership_epoch =>
133 {
134 record.term = term;
135 if entry.ownership_epoch.is_some() {
136 record.ownership_epoch = entry.ownership_epoch;
137 }
138 record.encode()
139 }
140 _ => entry.data.clone(),
141 }
142}
143
144#[derive(Debug, Default)]
152struct LogicalWalSpoolState {
153 current_lsn: u64,
154 seek_index: Vec<(u64, u64)>,
158 write_offset: u64,
162 record_count: u64,
165}
166
167impl LogicalWalSpoolState {
168 fn note_record(&mut self, ordinal: u64, lsn: u64, offset: u64) {
172 if ordinal.is_multiple_of(reddb_file::LOGICAL_WAL_SEEK_INDEX_INTERVAL) {
173 if self.seek_index.last().map(|(l, _)| *l) != Some(lsn) {
177 self.seek_index.push((lsn, offset));
178 }
179 }
180 }
181
182 fn seek_floor_offset(&self, since_lsn: u64) -> u64 {
187 match self
188 .seek_index
189 .binary_search_by(|(lsn, _)| lsn.cmp(&since_lsn))
190 {
191 Ok(idx) => self.seek_index[idx].1,
192 Err(0) => 0,
193 Err(idx) => self.seek_index[idx - 1].1,
194 }
195 }
196}
197
198pub struct LogicalWalSpool {
202 path: PathBuf,
203 state: Mutex<LogicalWalSpoolState>,
204}
205
206impl LogicalWalSpool {
207 pub fn path_for(data_path: &Path) -> PathBuf {
208 reddb_file::layout::logical_wal_path(data_path)
209 }
210
211 pub fn open(data_path: &Path) -> io::Result<Self> {
212 let path = Self::path_for(data_path);
213 if let Some(parent) = path.parent() {
214 fs::create_dir_all(parent)?;
215 }
216 if !path.exists() {
217 File::create(&path)?;
218 }
219 let entries = reddb_file::read_and_repair_logical_wal_entries(&path)?;
224 let current_lsn = entries.last().map(|entry| entry.lsn).unwrap_or(0);
225 let (seek_index, write_offset, record_count) =
228 reddb_file::build_logical_wal_seek_index(&path)?;
229 Ok(Self {
230 path,
231 state: Mutex::new(LogicalWalSpoolState {
232 current_lsn,
233 seek_index,
234 write_offset,
235 record_count,
236 }),
237 })
238 }
239
240 pub fn append(&self, lsn: u64, data: &[u8]) -> io::Result<()> {
241 let timestamp_ms = SystemTime::now()
242 .duration_since(UNIX_EPOCH)
243 .map(|d| d.as_millis() as u64)
244 .unwrap_or(0);
245 self.append_with_timestamp(lsn, timestamp_ms, data)
246 }
247
248 pub fn append_with_timestamp(
252 &self,
253 lsn: u64,
254 timestamp_ms: u64,
255 data: &[u8],
256 ) -> io::Result<()> {
257 let (term, ownership_epoch) = authority_from_payload(data);
258 self.append_with_term_epoch_and_timestamp(term, ownership_epoch, lsn, timestamp_ms, data)
259 }
260
261 pub fn append_with_term_and_timestamp(
262 &self,
263 term: u64,
264 lsn: u64,
265 timestamp_ms: u64,
266 data: &[u8],
267 ) -> io::Result<()> {
268 let (_, ownership_epoch) = authority_from_payload(data);
269 self.append_with_term_epoch_and_timestamp(term, ownership_epoch, lsn, timestamp_ms, data)
270 }
271
272 pub fn append_with_term_epoch_and_timestamp(
273 &self,
274 term: u64,
275 ownership_epoch: Option<u64>,
276 lsn: u64,
277 timestamp_ms: u64,
278 data: &[u8],
279 ) -> io::Result<()> {
280 let mut file = OpenOptions::new()
281 .create(true)
282 .append(true)
283 .open(&self.path)?;
284 let frame =
294 reddb_file::encode_logical_wal_v3(term, ownership_epoch, lsn, timestamp_ms, data)?;
295
296 file.write_all(&frame)?;
297 file.sync_all()?;
302
303 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
304 state.current_lsn = state.current_lsn.max(lsn);
305 let record_start = state.write_offset;
309 let ordinal = state.record_count;
310 state.note_record(ordinal, lsn, record_start);
311 state.write_offset = record_start + frame.len() as u64;
312 state.record_count = ordinal + 1;
313 Ok(())
314 }
315
316 pub fn read_since(&self, since_lsn: u64, max_count: usize) -> io::Result<Vec<(u64, Vec<u8>)>> {
317 let start_offset = {
323 let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
324 state.seek_floor_offset(since_lsn)
325 };
326 let entries = reddb_file::read_logical_wal_entries_from(&self.path, start_offset)?;
327 Ok(entries
328 .into_iter()
329 .filter(|entry| entry.lsn > since_lsn)
330 .take(max_count)
331 .map(|entry| (entry.lsn, logical_wal_data_with_framing_term(&entry)))
332 .collect())
333 }
334
335 #[cfg(test)]
339 fn seek_floor_offset(&self, since_lsn: u64) -> u64 {
340 self.state
341 .lock()
342 .unwrap_or_else(|e| e.into_inner())
343 .seek_floor_offset(since_lsn)
344 }
345
346 pub fn current_lsn(&self) -> u64 {
347 self.state
348 .lock()
349 .unwrap_or_else(|e| e.into_inner())
350 .current_lsn
351 }
352
353 pub fn oldest_lsn(&self) -> io::Result<Option<u64>> {
354 Ok(reddb_file::read_and_repair_logical_wal_entries(&self.path)?
355 .into_iter()
356 .next()
357 .map(|entry| entry.lsn))
358 }
359
360 pub fn prune_through(&self, upto_lsn: u64) -> io::Result<()> {
361 let previous_lsn = self.current_lsn();
362 let mut retained: Vec<_> = reddb_file::read_and_repair_logical_wal_entries(&self.path)?
363 .into_iter()
364 .filter(|entry| entry.lsn > upto_lsn)
365 .collect();
366 for entry in &mut retained {
367 entry.term = logical_wal_entry_term(entry);
368 }
369 let temp_path = reddb_file::layout::logical_wal_temp_path(&self.path);
370 for entry in &mut retained {
371 let timestamp_ms = if entry.timestamp_ms > 0 {
379 entry.timestamp_ms
380 } else {
381 SystemTime::now()
382 .duration_since(UNIX_EPOCH)
383 .map(|d| d.as_millis() as u64)
384 .unwrap_or(0)
385 };
386 entry.timestamp_ms = timestamp_ms;
387 }
388 let current_lsn =
389 reddb_file::rewrite_logical_wal_entries(&self.path, &temp_path, &retained)?;
390
391 let (seek_index, write_offset, record_count) =
394 reddb_file::build_logical_wal_seek_index(&self.path)?;
395 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
396 state.current_lsn = previous_lsn.max(current_lsn).max(upto_lsn);
397 state.seek_index = seek_index;
398 state.write_offset = write_offset;
399 state.record_count = record_count;
400 Ok(())
401 }
402}
403
404#[derive(Debug, Clone)]
410pub struct ReplicaState {
411 pub id: String,
412 pub last_acked_lsn: u64,
413 pub last_sent_lsn: u64,
414 pub last_durable_lsn: u64,
415 pub apply_error_count: u64,
416 pub divergence_count: u64,
417 pub connected_at_unix_ms: u128,
418 pub last_seen_at_unix_ms: u128,
419 pub region: Option<String>,
424 pub rebootstrapping: bool,
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435pub struct ReplicationProgress {
436 pub lag_lsn: u64,
437 pub safe_replay_lsn: u64,
438}
439
440impl ReplicationProgress {
441 pub fn from_replicas(replicas: &[ReplicaState]) -> Option<Self> {
442 let max_sent_lsn = replicas.iter().map(|replica| replica.last_sent_lsn).max()?;
443 let min_acked_lsn = replicas
444 .iter()
445 .map(|replica| replica.last_acked_lsn)
446 .min()?;
447 let safe_replay_lsn = replicas
448 .iter()
449 .map(|replica| replica.last_durable_lsn)
450 .min()?;
451
452 Some(Self {
453 lag_lsn: max_sent_lsn.saturating_sub(min_acked_lsn),
454 safe_replay_lsn,
455 })
456 }
457}
458
459pub struct PrimaryReplication {
461 pub wal_buffer: Arc<WalBuffer>,
462 pub logical_wal_spool: Option<Arc<LogicalWalSpool>>,
463 pub replicas: RwLock<Vec<ReplicaState>>,
464 wal_appended: (Mutex<u64>, Condvar),
465 metadata_store: Option<Arc<crate::storage::unified::UnifiedStore>>,
466 metadata_error: Mutex<Option<String>>,
467 slot_path: Option<PathBuf>,
468 slot_catalog_path: Option<PathBuf>,
469 primary_replica_file_plan: Option<reddb_file::PrimaryReplicaFilePlan>,
470 primary_replica_wal_lock: Mutex<()>,
471 slots: RwLock<BTreeMap<String, ReplicationSlot>>,
472 slot_retention_max_lag_lsn: u64,
473 slot_idle_timeout_ms: u64,
474 pub commit_waiter: Arc<crate::replication::commit_waiter::CommitWaiter>,
478 topology_epoch: std::sync::atomic::AtomicU64,
485 partial_resync_count: std::sync::atomic::AtomicU64,
491 full_resync_count: std::sync::atomic::AtomicU64,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum ResumeMode {
503 PartialResync { resume_lsn: u64 },
507 FullRebootstrap {
510 cause: ReplicationSlotInvalidationCause,
511 },
512}
513
514impl PrimaryReplication {
515 pub fn slot_path_for(data_path: &Path) -> PathBuf {
516 reddb_file::layout::legacy_logical_slots_path(data_path)
517 }
518
519 pub fn primary_replica_root_for(data_path: &Path) -> PathBuf {
520 reddb_file::layout::primary_replica_root(data_path)
521 }
522
523 pub fn slot_catalog_path_for(data_path: &Path) -> PathBuf {
524 Self::primary_replica_file_plan_for(data_path).slots_path()
525 }
526
527 fn primary_replica_file_plan_for(data_path: &Path) -> reddb_file::PrimaryReplicaFilePlan {
528 let root = Self::primary_replica_root_for(data_path);
529 let timeline =
530 Self::primary_replica_current_timeline_for_root(&root).unwrap_or_else(|err| {
531 warn!(
532 target: "reddb::replication::primary",
533 error = %err,
534 "failed to read primary-replica timeline history; using initial timeline"
535 );
536 reddb_file::TimelineId::initial()
537 });
538 reddb_file::PrimaryReplicaFilePlan::new(root, timeline)
539 }
540
541 fn primary_replica_current_timeline_for_root(
542 root: &Path,
543 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
544 let path = reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
545 .timeline_history_path();
546 match reddb_file::TimelineHistory::read_from_path(&path) {
547 Ok(history) => Ok(history
548 .current()
549 .unwrap_or_else(reddb_file::TimelineId::initial)),
550 Err(reddb_file::RdbFileError::Io(err))
551 if err.kind() == std::io::ErrorKind::NotFound =>
552 {
553 Ok(reddb_file::TimelineId::initial())
554 }
555 Err(err) => Err(err),
556 }
557 }
558
559 pub fn new(data_path: Option<&Path>) -> Self {
560 Self::new_with_config(data_path, &crate::replication::ReplicationConfig::primary())
561 }
562
563 pub fn new_with_config(
564 data_path: Option<&Path>,
565 config: &crate::replication::ReplicationConfig,
566 ) -> Self {
567 let now_ms = crate::utils::now_unix_millis() as u128;
568 let slot_path = data_path.map(Self::slot_path_for);
569 let slot_catalog_path = data_path.map(Self::slot_catalog_path_for);
570 let primary_replica_file_plan = data_path.map(Self::primary_replica_file_plan_for);
571 let mut slots = load_replication_slot_catalog(slot_catalog_path.as_deref(), now_ms);
572 slots.extend(load_replication_slots(slot_path.as_deref(), now_ms));
573 let logical_wal_spool = data_path
574 .and_then(|path| LogicalWalSpool::open(path).ok())
575 .map(Arc::new);
576 let current_lsn = logical_wal_spool
577 .as_ref()
578 .map(|spool| spool.current_lsn())
579 .unwrap_or(0);
580 Self {
581 wal_buffer: Arc::new(WalBuffer::new(100_000)),
582 logical_wal_spool,
583 replicas: RwLock::new(Vec::new()),
584 wal_appended: (Mutex::new(current_lsn), Condvar::new()),
585 metadata_store: None,
586 metadata_error: Mutex::new(None),
587 slot_path,
588 slot_catalog_path,
589 primary_replica_file_plan,
590 primary_replica_wal_lock: Mutex::new(()),
591 slots: RwLock::new(slots),
592 slot_retention_max_lag_lsn: config.slot_retention_max_lag_lsn,
593 slot_idle_timeout_ms: config.slot_idle_timeout_ms,
594 commit_waiter: Arc::new(crate::replication::commit_waiter::CommitWaiter::new()),
595 topology_epoch: std::sync::atomic::AtomicU64::new(0),
596 partial_resync_count: std::sync::atomic::AtomicU64::new(0),
597 full_resync_count: std::sync::atomic::AtomicU64::new(0),
598 }
599 }
600
601 pub(crate) fn new_with_store(
602 data_path: Option<&Path>,
603 config: &crate::replication::ReplicationConfig,
604 store: Arc<crate::storage::unified::UnifiedStore>,
605 ) -> io::Result<Self> {
606 let mut primary = Self::new_with_config(data_path, config);
607 if let Some(bytes) = super::metadata::read(&store, super::metadata::SLOTS)? {
608 let catalog =
609 reddb_file::ReplicationSlotCatalog::decode(&bytes).map_err(io::Error::other)?;
610 primary.slots = RwLock::new(
611 catalog
612 .slots
613 .into_iter()
614 .map(|slot| (slot.replica_id.clone(), slot))
615 .collect(),
616 );
617 } else {
618 let mut valid_source = false;
621 let mut import_error = None;
622 for path in [&primary.slot_catalog_path, &primary.slot_path]
623 .into_iter()
624 .flatten()
625 {
626 if path.try_exists()? {
627 let result = if primary.slot_catalog_path.as_ref() == Some(path) {
628 reddb_file::ReplicationSlotCatalog::read_from_path(path)
629 } else {
630 reddb_file::ReplicationSlotCatalog::read_legacy_json_from_path(
631 path,
632 crate::utils::now_unix_millis() as u128,
633 )
634 };
635 match result {
636 Ok(_) => valid_source = true,
637 Err(error) => import_error = Some(error),
638 }
639 }
640 }
641 if !valid_source {
642 if let Some(error) = import_error {
643 return Err(io::Error::other(error));
644 }
645 }
646 let slots = primary
647 .slots
648 .read()
649 .unwrap_or_else(|error| error.into_inner());
650 let bytes = slots::slot_catalog_from_map(&slots)
651 .and_then(|catalog| catalog.encode())
652 .map_err(io::Error::other)?;
653 super::metadata::write(&store, super::metadata::SLOTS, &bytes)?;
654 }
655 for path in [&primary.slot_catalog_path, &primary.slot_path]
656 .into_iter()
657 .flatten()
658 {
659 super::metadata::retire_legacy(path)?;
660 }
661 primary.metadata_store = Some(store);
662 Ok(primary)
663 }
664
665 fn current_timeline(
666 &self,
667 root: &Path,
668 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
669 if let Some(store) = &self.metadata_store {
670 let path =
671 reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
672 .timeline_history_path();
673 return super::metadata::timeline(store, &path)
674 .map(|history| {
675 history
676 .current()
677 .unwrap_or_else(reddb_file::TimelineId::initial)
678 })
679 .map_err(reddb_file::RdbFileError::Io);
680 }
681 Self::primary_replica_current_timeline_for_root(root)
682 }
683
684 pub fn append_logical_record(&self, lsn: u64, encoded: Vec<u8>) {
685 self.wal_buffer.append(lsn, encoded.clone());
686 if let Some(spool) = &self.logical_wal_spool {
687 let _ = spool.append(lsn, &encoded);
688 }
689 if let Some(plan) = &self.primary_replica_file_plan {
690 let _guard = self
691 .primary_replica_wal_lock
692 .lock()
693 .unwrap_or_else(|err| err.into_inner());
694 match self.current_timeline(&plan.root) {
695 Ok(timeline) => {
696 let plan = reddb_file::PrimaryReplicaFilePlan::new(plan.root.clone(), timeline);
697 if let Err(err) = plan.append_wal_record(lsn, &encoded) {
698 warn!(
699 target: "reddb::replication::primary",
700 lsn,
701 error = %err,
702 "failed to append primary-replica WAL segment"
703 );
704 }
705 }
706 Err(err) => {
707 warn!(
708 target: "reddb::replication::primary",
709 lsn,
710 error = %err,
711 "failed to read primary-replica timeline history; skipping WAL append"
712 );
713 }
714 }
715 }
716 let (lock, cvar) = &self.wal_appended;
717 let mut latest = lock.lock().unwrap_or_else(|e| e.into_inner());
718 *latest = (*latest).max(lsn);
719 cvar.notify_all();
720 }
721
722 pub fn wait_for_logical_lsn_after(&self, since_lsn: u64, timeout: Duration) -> bool {
723 if self.current_logical_lsn() > since_lsn {
724 return true;
725 }
726 let deadline = Instant::now() + timeout;
727 let (lock, cvar) = &self.wal_appended;
728 let mut latest = lock.lock().unwrap_or_else(|e| e.into_inner());
729 while *latest <= since_lsn {
730 let now = Instant::now();
731 if now >= deadline {
732 return false;
733 }
734 let remaining = deadline.saturating_duration_since(now);
735 let (guard, result) = cvar
736 .wait_timeout(latest, remaining)
737 .unwrap_or_else(|e| e.into_inner());
738 latest = guard;
739 if result.timed_out() && *latest <= since_lsn {
740 return false;
741 }
742 }
743 true
744 }
745
746 pub fn register_replica(&self, id: String) -> u64 {
747 self.register_replica_with_region(id, None)
748 }
749
750 pub fn register_replica_with_region(&self, id: String, region: Option<String>) -> u64 {
767 self.register_replica_with_region_at(id, region, self.current_logical_lsn())
768 }
769
770 fn register_replica_with_region_at(
771 &self,
772 id: String,
773 region: Option<String>,
774 initial_lsn: u64,
775 ) -> u64 {
776 let now_ms = crate::utils::now_unix_millis() as u128;
777 let resume_lsn = self.ensure_slot(&id, initial_lsn);
778 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
779 if let Some(existing) = replicas.iter_mut().find(|r| r.id == id) {
780 existing.last_seen_at_unix_ms = now_ms;
781 if region.is_some() {
782 existing.region = region;
783 }
784 return resume_lsn;
785 }
786 replicas.push(ReplicaState {
787 id,
788 last_acked_lsn: resume_lsn,
789 last_sent_lsn: resume_lsn,
790 last_durable_lsn: resume_lsn,
791 apply_error_count: 0,
792 divergence_count: 0,
793 connected_at_unix_ms: now_ms,
794 last_seen_at_unix_ms: now_ms,
795 region,
796 rebootstrapping: false,
797 });
798 drop(replicas);
799 self.bump_topology_epoch();
800 resume_lsn
801 }
802
803 pub fn set_replica_rebootstrapping(&self, id: &str, rebootstrapping: bool) -> bool {
818 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
819 let Some(state) = replicas.iter_mut().find(|r| r.id == id) else {
820 return false;
821 };
822 if state.rebootstrapping == rebootstrapping {
823 return true;
824 }
825 state.rebootstrapping = rebootstrapping;
826 drop(replicas);
827 self.bump_topology_epoch();
828 true
829 }
830
831 pub fn ensure_replica_registered(&self, id: &str, requested_since_lsn: u64) -> bool {
842 let already = self
843 .replicas
844 .read()
845 .unwrap_or_else(|e| e.into_inner())
846 .iter()
847 .any(|r| r.id == id);
848 if already {
849 return false;
850 }
851 self.register_replica_with_region_at(id.to_string(), None, requested_since_lsn);
852 true
853 }
854
855 pub fn unregister_replica(&self, id: &str) -> bool {
859 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
860 let before = replicas.len();
861 replicas.retain(|r| r.id != id);
862 let removed = replicas.len() != before;
863 drop(replicas);
864 if removed {
865 self.commit_waiter.drop_replica(id);
866 self.bump_topology_epoch();
867 }
868 removed
869 }
870
871 pub fn topology_epoch(&self) -> u64 {
874 self.topology_epoch
875 .load(std::sync::atomic::Ordering::Relaxed)
876 }
877
878 pub fn bump_topology_epoch(&self) {
886 self.topology_epoch
887 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
888 }
889
890 pub fn ack_replica(&self, id: &str, lsn: u64) {
891 let now_ms = crate::utils::now_unix_millis() as u128;
892 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
893 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
894 r.last_acked_lsn = r.last_acked_lsn.max(lsn);
895 r.last_durable_lsn = r.last_durable_lsn.max(lsn);
896 r.last_seen_at_unix_ms = now_ms;
897 }
898 drop(replicas);
899 self.commit_waiter.record_replica_ack(id, lsn);
900 }
901
902 pub fn ack_replica_lsn(&self, id: &str, applied_lsn: u64, durable_lsn: u64) {
908 self.ack_replica_lsn_with_observability(id, applied_lsn, durable_lsn, 0, 0);
909 }
910
911 pub fn ack_replica_lsn_with_observability(
912 &self,
913 id: &str,
914 applied_lsn: u64,
915 durable_lsn: u64,
916 apply_error_count: u64,
917 divergence_count: u64,
918 ) {
919 let now_ms = crate::utils::now_unix_millis() as u128;
920 self.advance_slot(id, applied_lsn, durable_lsn, now_ms);
921 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
922 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
923 r.last_acked_lsn = r.last_acked_lsn.max(applied_lsn);
924 r.last_durable_lsn = r.last_durable_lsn.max(durable_lsn);
925 r.apply_error_count = r.apply_error_count.max(apply_error_count);
926 r.divergence_count = r.divergence_count.max(divergence_count);
927 r.last_seen_at_unix_ms = now_ms;
928 }
929 drop(replicas);
933 self.commit_waiter.record_replica_ack(id, durable_lsn);
934 }
935
936 pub fn note_replica_pull(&self, id: &str, last_sent_lsn: u64) {
941 let now_ms = crate::utils::now_unix_millis() as u128;
942 self.touch_slot(id, now_ms);
943 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
944 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
945 r.last_sent_lsn = r.last_sent_lsn.max(last_sent_lsn);
946 r.last_seen_at_unix_ms = now_ms;
947 }
948 }
949
950 pub fn replica_snapshots(&self) -> Vec<ReplicaState> {
954 self.replicas
955 .read()
956 .unwrap_or_else(|e| e.into_inner())
957 .clone()
958 }
959
960 pub fn replication_progress(&self) -> Option<ReplicationProgress> {
961 let replicas = self.replicas.read().unwrap_or_else(|e| e.into_inner());
962 ReplicationProgress::from_replicas(&replicas)
963 }
964
965 pub fn slot_snapshots(&self) -> Vec<ReplicationSlot> {
966 self.slots
967 .read()
968 .unwrap_or_else(|e| e.into_inner())
969 .values()
970 .cloned()
971 .collect()
972 }
973
974 pub fn retention_floor_lsn(&self) -> Option<u64> {
975 let slots = self.slots.read().unwrap_or_else(|e| e.into_inner());
978 if self.ensure_slot_metadata_durable().is_err() {
979 return Some(0);
980 }
981 slots
982 .values()
983 .filter(|slot| slot.invalidation_reason.is_none())
984 .map(|slot| slot.restart_lsn)
985 .min()
986 }
987
988 pub fn prune_retained_wal_through(&self, archived_lsn: u64) -> io::Result<u64> {
989 self.enforce_retention_limits(crate::utils::now_unix_millis() as u128);
990 let prune_lsn = self
991 .retention_floor_lsn()
992 .map(|floor| floor.min(archived_lsn))
993 .unwrap_or(archived_lsn);
994 if prune_lsn > 0 {
995 if let Some(spool) = &self.logical_wal_spool {
996 spool.prune_through(prune_lsn)?;
997 }
998 self.wal_buffer.prune_through(prune_lsn);
999 }
1000 Ok(prune_lsn)
1001 }
1002
1003 pub fn replica_count(&self) -> usize {
1004 self.replicas
1005 .read()
1006 .unwrap_or_else(|e| e.into_inner())
1007 .len()
1008 }
1009
1010 pub fn current_logical_lsn(&self) -> u64 {
1014 self.logical_wal_spool
1015 .as_ref()
1016 .map(|spool| spool.current_lsn())
1017 .unwrap_or_else(|| self.wal_buffer.current_lsn())
1018 }
1019
1020 fn ensure_slot(&self, id: &str, initial_lsn: u64) -> u64 {
1021 let now_ms = crate::utils::now_unix_millis() as u128;
1022 let _publication = self
1023 .metadata_store
1024 .as_ref()
1025 .map(|store| store.vault_publication_lock.lock());
1026 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1027 if let Some(slot) = slots.get_mut(id) {
1028 slot.last_seen_at_unix_ms = now_ms;
1029 let restart_lsn = slot.restart_lsn;
1030 self.persist_slots_locked(&slots);
1031 return restart_lsn;
1032 }
1033 let mut slot = ReplicationSlot::new(
1034 id.to_string(),
1035 reddb_file::TimelineId::initial(),
1036 initial_lsn,
1037 );
1038 slot.last_seen_at_unix_ms = now_ms;
1039 slots.insert(id.to_string(), slot);
1040 let restart_lsn = initial_lsn;
1041 self.persist_slots_locked(&slots);
1042 restart_lsn
1043 }
1044
1045 fn advance_slot(&self, id: &str, confirmed_lsn: u64, restart_lsn: u64, now_ms: u128) {
1046 let _publication = self
1047 .metadata_store
1048 .as_ref()
1049 .map(|store| store.vault_publication_lock.lock());
1050 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1051 let slot = slots.entry(id.to_string()).or_insert_with(|| {
1052 let mut slot =
1053 ReplicationSlot::new(id.to_string(), reddb_file::TimelineId::initial(), 0);
1054 slot.last_seen_at_unix_ms = now_ms;
1055 slot
1056 });
1057 if slot.invalidation_reason.is_some() {
1058 return;
1059 }
1060 slot.confirmed_write_lsn = slot.confirmed_lsn().max(confirmed_lsn).max(restart_lsn);
1061 slot.restart_lsn = slot.restart_lsn.max(restart_lsn);
1062 slot.confirmed_flush_lsn = slot.confirmed_flush_lsn.max(slot.restart_lsn);
1063 slot.confirmed_apply_lsn = slot.confirmed_apply_lsn.max(slot.restart_lsn);
1064 slot.last_seen_at_unix_ms = now_ms;
1065 self.persist_slots_locked(&slots);
1066 }
1067
1068 pub fn touch_slot(&self, id: &str, now_ms: u128) {
1069 let _publication = self
1070 .metadata_store
1071 .as_ref()
1072 .map(|store| store.vault_publication_lock.lock());
1073 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1074 let mut changed = false;
1075 if let Some(slot) = slots.get_mut(id) {
1076 if slot.invalidation_reason.is_none() {
1077 slot.last_seen_at_unix_ms = now_ms;
1078 changed = true;
1079 }
1080 }
1081 if changed {
1082 self.persist_slots_locked(&slots);
1083 }
1084 }
1085
1086 pub fn enforce_retention_limits(
1087 &self,
1088 now_ms: u128,
1089 ) -> Vec<(String, ReplicationSlotInvalidationCause)> {
1090 let current_lsn = self.current_logical_lsn();
1091 let mut invalidated = Vec::new();
1092 let _publication = self
1093 .metadata_store
1094 .as_ref()
1095 .map(|store| store.vault_publication_lock.lock());
1096 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1097 for slot in slots.values_mut() {
1098 if slot.invalidation_reason.is_some() {
1099 continue;
1100 }
1101 let reason = if self.slot_retention_max_lag_lsn > 0
1102 && current_lsn.saturating_sub(slot.restart_lsn) > self.slot_retention_max_lag_lsn
1103 {
1104 Some(ReplicationSlotInvalidationCause::Horizon)
1105 } else if self.slot_idle_timeout_ms > 0
1106 && now_ms.saturating_sub(slot.last_seen_at_unix_ms)
1107 > u128::from(self.slot_idle_timeout_ms)
1108 {
1109 Some(ReplicationSlotInvalidationCause::IdleTimeout)
1110 } else {
1111 None
1112 };
1113 if let Some(reason) = reason {
1114 slot.invalidation_reason = Some(reason);
1115 slot.invalidated_at_unix_ms = Some(now_ms);
1116 invalidated.push((slot.replica_id.clone(), reason));
1117 }
1118 }
1119 if !invalidated.is_empty() {
1120 self.persist_slots_locked(&slots);
1121 }
1122 invalidated
1123 }
1124
1125 pub fn slot_rebootstrap_reason(
1126 &self,
1127 id: &str,
1128 requested_since_lsn: u64,
1129 oldest_available_lsn: Option<u64>,
1130 ) -> Option<ReplicationSlotInvalidationCause> {
1131 let now_ms = crate::utils::now_unix_millis() as u128;
1132 let _publication = self
1133 .metadata_store
1134 .as_ref()
1135 .map(|store| store.vault_publication_lock.lock());
1136 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1137 let slot = slots.get_mut(id)?;
1138 if let Some(reason) = slot.invalidation_reason {
1139 return Some(reason);
1140 }
1141 let slot_floor = slot.restart_lsn.max(requested_since_lsn);
1142 if oldest_available_lsn
1143 .map(|oldest| oldest > slot_floor.saturating_add(1))
1144 .unwrap_or(false)
1145 {
1146 slot.invalidation_reason = Some(ReplicationSlotInvalidationCause::WalRemoved);
1147 slot.invalidated_at_unix_ms = Some(now_ms);
1148 self.persist_slots_locked(&slots);
1149 return Some(ReplicationSlotInvalidationCause::WalRemoved);
1150 }
1151 None
1152 }
1153
1154 pub fn plan_replica_resume(
1163 &self,
1164 id: &str,
1165 requested_since_lsn: u64,
1166 oldest_available_lsn: Option<u64>,
1167 ) -> ResumeMode {
1168 if let Some(cause) =
1169 self.slot_rebootstrap_reason(id, requested_since_lsn, oldest_available_lsn)
1170 {
1171 self.full_resync_count
1172 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1173 return ResumeMode::FullRebootstrap { cause };
1174 }
1175 let resume_lsn = self
1176 .slot_snapshots()
1177 .into_iter()
1178 .find(|slot| slot.replica_id == id)
1179 .map(|slot| requested_since_lsn.max(slot.restart_lsn))
1180 .unwrap_or(requested_since_lsn);
1181 self.partial_resync_count
1182 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1183 ResumeMode::PartialResync { resume_lsn }
1184 }
1185
1186 pub fn partial_resync_count(&self) -> u64 {
1189 self.partial_resync_count
1190 .load(std::sync::atomic::Ordering::Relaxed)
1191 }
1192
1193 pub fn full_resync_count(&self) -> u64 {
1197 self.full_resync_count
1198 .load(std::sync::atomic::Ordering::Relaxed)
1199 }
1200
1201 fn persist_slots_locked(&self, slots: &BTreeMap<String, ReplicationSlot>) {
1202 if let Some(store) = &self.metadata_store {
1203 let result = slots::slot_catalog_from_map(slots)
1204 .and_then(|catalog| catalog.encode())
1205 .map_err(io::Error::other)
1206 .and_then(|bytes| super::metadata::write(store, super::metadata::SLOTS, &bytes));
1207 let mut last_error = self
1208 .metadata_error
1209 .lock()
1210 .unwrap_or_else(|error| error.into_inner());
1211 if let Err(error) = result {
1212 *last_error = Some(error.to_string());
1213 warn!(target: "reddb::replication::slots", %error,
1214 "failed to persist replication slots inside database");
1215 } else {
1216 *last_error = None;
1217 }
1218 return;
1219 }
1220 if let Err(err) = persist_replication_slots(self.slot_path.as_deref(), slots) {
1221 warn!(
1222 target: "reddb::replication::slots",
1223 error = %err,
1224 "failed to persist replication slots"
1225 );
1226 }
1227 if let Err(err) = persist_replication_slot_catalog(self.slot_catalog_path.as_deref(), slots)
1228 {
1229 warn!(
1230 target: "reddb::replication::slots",
1231 error = %err,
1232 "failed to persist binary replication slot catalog"
1233 );
1234 }
1235 }
1236
1237 pub(crate) fn ensure_slot_metadata_durable(&self) -> io::Result<()> {
1238 match self
1239 .metadata_error
1240 .lock()
1241 .unwrap_or_else(|error| error.into_inner())
1242 .as_ref()
1243 {
1244 Some(error) => Err(io::Error::other(error.clone())),
1245 None => Ok(()),
1246 }
1247 }
1248}
1249
1250#[cfg(test)]
1251mod tests;