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 pub(crate) routing_endpoints: RwLock<BTreeMap<String, reddb_wire::routing::RoutingEndpoints>>,
465 wal_appended: (Mutex<u64>, Condvar),
466 metadata_store: Option<Arc<crate::storage::unified::UnifiedStore>>,
467 metadata_error: Mutex<Option<String>>,
468 slot_path: Option<PathBuf>,
469 slot_catalog_path: Option<PathBuf>,
470 primary_replica_file_plan: Option<reddb_file::PrimaryReplicaFilePlan>,
471 primary_replica_wal_lock: Mutex<()>,
472 slots: RwLock<BTreeMap<String, ReplicationSlot>>,
473 slot_retention_max_lag_lsn: u64,
474 slot_idle_timeout_ms: u64,
475 pub commit_waiter: Arc<crate::replication::commit_waiter::CommitWaiter>,
479 topology_epoch: std::sync::atomic::AtomicU64,
486 partial_resync_count: std::sync::atomic::AtomicU64,
492 full_resync_count: std::sync::atomic::AtomicU64,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503pub enum ResumeMode {
504 PartialResync { resume_lsn: u64 },
508 FullRebootstrap {
511 cause: ReplicationSlotInvalidationCause,
512 },
513}
514
515impl PrimaryReplication {
516 pub fn slot_path_for(data_path: &Path) -> PathBuf {
517 reddb_file::layout::legacy_logical_slots_path(data_path)
518 }
519
520 pub fn primary_replica_root_for(data_path: &Path) -> PathBuf {
521 reddb_file::layout::primary_replica_root(data_path)
522 }
523
524 pub fn slot_catalog_path_for(data_path: &Path) -> PathBuf {
525 Self::primary_replica_file_plan_for(data_path).slots_path()
526 }
527
528 fn primary_replica_file_plan_for(data_path: &Path) -> reddb_file::PrimaryReplicaFilePlan {
529 let root = Self::primary_replica_root_for(data_path);
530 let timeline =
531 Self::primary_replica_current_timeline_for_root(&root).unwrap_or_else(|err| {
532 warn!(
533 target: "reddb::replication::primary",
534 error = %err,
535 "failed to read primary-replica timeline history; using initial timeline"
536 );
537 reddb_file::TimelineId::initial()
538 });
539 reddb_file::PrimaryReplicaFilePlan::new(root, timeline)
540 }
541
542 fn primary_replica_current_timeline_for_root(
543 root: &Path,
544 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
545 let path = reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
546 .timeline_history_path();
547 match reddb_file::TimelineHistory::read_from_path(&path) {
548 Ok(history) => Ok(history
549 .current()
550 .unwrap_or_else(reddb_file::TimelineId::initial)),
551 Err(reddb_file::RdbFileError::Io(err))
552 if err.kind() == std::io::ErrorKind::NotFound =>
553 {
554 Ok(reddb_file::TimelineId::initial())
555 }
556 Err(err) => Err(err),
557 }
558 }
559
560 pub fn new(data_path: Option<&Path>) -> Self {
561 Self::new_with_config(data_path, &crate::replication::ReplicationConfig::primary())
562 }
563
564 pub fn new_with_config(
565 data_path: Option<&Path>,
566 config: &crate::replication::ReplicationConfig,
567 ) -> Self {
568 let now_ms = crate::utils::now_unix_millis() as u128;
569 let slot_path = data_path.map(Self::slot_path_for);
570 let slot_catalog_path = data_path.map(Self::slot_catalog_path_for);
571 let primary_replica_file_plan = data_path.map(Self::primary_replica_file_plan_for);
572 let mut slots = load_replication_slot_catalog(slot_catalog_path.as_deref(), now_ms);
573 slots.extend(load_replication_slots(slot_path.as_deref(), now_ms));
574 let logical_wal_spool = data_path
575 .and_then(|path| LogicalWalSpool::open(path).ok())
576 .map(Arc::new);
577 let current_lsn = logical_wal_spool
578 .as_ref()
579 .map(|spool| spool.current_lsn())
580 .unwrap_or(0);
581 Self {
582 wal_buffer: Arc::new(WalBuffer::new(100_000)),
583 logical_wal_spool,
584 replicas: RwLock::new(Vec::new()),
585 routing_endpoints: RwLock::new(BTreeMap::new()),
586 wal_appended: (Mutex::new(current_lsn), Condvar::new()),
587 metadata_store: None,
588 metadata_error: Mutex::new(None),
589 slot_path,
590 slot_catalog_path,
591 primary_replica_file_plan,
592 primary_replica_wal_lock: Mutex::new(()),
593 slots: RwLock::new(slots),
594 slot_retention_max_lag_lsn: config.slot_retention_max_lag_lsn,
595 slot_idle_timeout_ms: config.slot_idle_timeout_ms,
596 commit_waiter: Arc::new(crate::replication::commit_waiter::CommitWaiter::new()),
597 topology_epoch: std::sync::atomic::AtomicU64::new(0),
598 partial_resync_count: std::sync::atomic::AtomicU64::new(0),
599 full_resync_count: std::sync::atomic::AtomicU64::new(0),
600 }
601 }
602
603 pub(crate) fn new_with_store(
604 data_path: Option<&Path>,
605 config: &crate::replication::ReplicationConfig,
606 store: Arc<crate::storage::unified::UnifiedStore>,
607 ) -> io::Result<Self> {
608 let mut primary = Self::new_with_config(data_path, config);
609 if let Some(bytes) = super::metadata::read(&store, super::metadata::SLOTS)? {
610 let catalog =
611 reddb_file::ReplicationSlotCatalog::decode(&bytes).map_err(io::Error::other)?;
612 primary.slots = RwLock::new(
613 catalog
614 .slots
615 .into_iter()
616 .map(|slot| (slot.replica_id.clone(), slot))
617 .collect(),
618 );
619 } else {
620 let mut valid_source = false;
623 let mut import_error = None;
624 for path in [&primary.slot_catalog_path, &primary.slot_path]
625 .into_iter()
626 .flatten()
627 {
628 if path.try_exists()? {
629 let result = if primary.slot_catalog_path.as_ref() == Some(path) {
630 reddb_file::ReplicationSlotCatalog::read_from_path(path)
631 } else {
632 reddb_file::ReplicationSlotCatalog::read_legacy_json_from_path(
633 path,
634 crate::utils::now_unix_millis() as u128,
635 )
636 };
637 match result {
638 Ok(_) => valid_source = true,
639 Err(error) => import_error = Some(error),
640 }
641 }
642 }
643 if !valid_source {
644 if let Some(error) = import_error {
645 return Err(io::Error::other(error));
646 }
647 }
648 let slots = primary
649 .slots
650 .read()
651 .unwrap_or_else(|error| error.into_inner());
652 let bytes = slots::slot_catalog_from_map(&slots)
653 .and_then(|catalog| catalog.encode())
654 .map_err(io::Error::other)?;
655 super::metadata::write(&store, super::metadata::SLOTS, &bytes)?;
656 }
657 for path in [&primary.slot_catalog_path, &primary.slot_path]
658 .into_iter()
659 .flatten()
660 {
661 super::metadata::retire_legacy(path)?;
662 }
663 primary.metadata_store = Some(store);
664 Ok(primary)
665 }
666
667 fn current_timeline(
668 &self,
669 root: &Path,
670 ) -> Result<reddb_file::TimelineId, reddb_file::RdbFileError> {
671 if let Some(store) = &self.metadata_store {
672 let path =
673 reddb_file::PrimaryReplicaFilePlan::new(root, reddb_file::TimelineId::initial())
674 .timeline_history_path();
675 return super::metadata::timeline(store, &path)
676 .map(|history| {
677 history
678 .current()
679 .unwrap_or_else(reddb_file::TimelineId::initial)
680 })
681 .map_err(reddb_file::RdbFileError::Io);
682 }
683 Self::primary_replica_current_timeline_for_root(root)
684 }
685
686 pub fn append_logical_record(&self, lsn: u64, encoded: Vec<u8>) {
687 self.wal_buffer.append(lsn, encoded.clone());
688 if let Some(spool) = &self.logical_wal_spool {
689 let _ = spool.append(lsn, &encoded);
690 }
691 if let Some(plan) = &self.primary_replica_file_plan {
692 let _guard = self
693 .primary_replica_wal_lock
694 .lock()
695 .unwrap_or_else(|err| err.into_inner());
696 match self.current_timeline(&plan.root) {
697 Ok(timeline) => {
698 let plan = reddb_file::PrimaryReplicaFilePlan::new(plan.root.clone(), timeline);
699 if let Err(err) = plan.append_wal_record(lsn, &encoded) {
700 warn!(
701 target: "reddb::replication::primary",
702 lsn,
703 error = %err,
704 "failed to append primary-replica WAL segment"
705 );
706 }
707 }
708 Err(err) => {
709 warn!(
710 target: "reddb::replication::primary",
711 lsn,
712 error = %err,
713 "failed to read primary-replica timeline history; skipping WAL append"
714 );
715 }
716 }
717 }
718 let (lock, cvar) = &self.wal_appended;
719 let mut latest = lock.lock().unwrap_or_else(|e| e.into_inner());
720 *latest = (*latest).max(lsn);
721 cvar.notify_all();
722 }
723
724 pub fn wait_for_logical_lsn_after(&self, since_lsn: u64, timeout: Duration) -> bool {
725 if self.current_logical_lsn() > since_lsn {
726 return true;
727 }
728 let deadline = Instant::now() + timeout;
729 let (lock, cvar) = &self.wal_appended;
730 let mut latest = lock.lock().unwrap_or_else(|e| e.into_inner());
731 while *latest <= since_lsn {
732 let now = Instant::now();
733 if now >= deadline {
734 return false;
735 }
736 let remaining = deadline.saturating_duration_since(now);
737 let (guard, result) = cvar
738 .wait_timeout(latest, remaining)
739 .unwrap_or_else(|e| e.into_inner());
740 latest = guard;
741 if result.timed_out() && *latest <= since_lsn {
742 return false;
743 }
744 }
745 true
746 }
747
748 pub fn register_replica(&self, id: String) -> u64 {
749 self.register_replica_with_region(id, None)
750 }
751
752 pub fn register_replica_with_region(&self, id: String, region: Option<String>) -> u64 {
769 self.register_replica_with_region_at(id, region, self.current_logical_lsn())
770 }
771
772 fn register_replica_with_region_at(
773 &self,
774 id: String,
775 region: Option<String>,
776 initial_lsn: u64,
777 ) -> u64 {
778 let now_ms = crate::utils::now_unix_millis() as u128;
779 let resume_lsn = self.ensure_slot(&id, initial_lsn);
780 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
781 if let Some(existing) = replicas.iter_mut().find(|r| r.id == id) {
782 existing.last_seen_at_unix_ms = now_ms;
783 if region.is_some() {
784 existing.region = region;
785 }
786 return resume_lsn;
787 }
788 replicas.push(ReplicaState {
789 id,
790 last_acked_lsn: resume_lsn,
791 last_sent_lsn: resume_lsn,
792 last_durable_lsn: resume_lsn,
793 apply_error_count: 0,
794 divergence_count: 0,
795 connected_at_unix_ms: now_ms,
796 last_seen_at_unix_ms: now_ms,
797 region,
798 rebootstrapping: false,
799 });
800 drop(replicas);
801 self.bump_topology_epoch();
802 resume_lsn
803 }
804
805 pub fn set_replica_rebootstrapping(&self, id: &str, rebootstrapping: bool) -> bool {
820 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
821 let Some(state) = replicas.iter_mut().find(|r| r.id == id) else {
822 return false;
823 };
824 if state.rebootstrapping == rebootstrapping {
825 return true;
826 }
827 state.rebootstrapping = rebootstrapping;
828 drop(replicas);
829 self.bump_topology_epoch();
830 true
831 }
832
833 pub fn ensure_replica_registered(&self, id: &str, requested_since_lsn: u64) -> bool {
844 let already = self
845 .replicas
846 .read()
847 .unwrap_or_else(|e| e.into_inner())
848 .iter()
849 .any(|r| r.id == id);
850 if already {
851 return false;
852 }
853 self.register_replica_with_region_at(id.to_string(), None, requested_since_lsn);
854 true
855 }
856
857 pub fn unregister_replica(&self, id: &str) -> bool {
861 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
862 let before = replicas.len();
863 replicas.retain(|r| r.id != id);
864 let removed = replicas.len() != before;
865 drop(replicas);
866 if removed {
867 self.routing_endpoints
868 .write()
869 .expect("replica routing endpoint map lock must not be poisoned")
870 .remove(id);
871 self.commit_waiter.drop_replica(id);
872 self.bump_topology_epoch();
873 }
874 removed
875 }
876
877 pub fn topology_epoch(&self) -> u64 {
880 self.topology_epoch
881 .load(std::sync::atomic::Ordering::Relaxed)
882 }
883
884 pub fn bump_topology_epoch(&self) {
892 self.topology_epoch
893 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
894 }
895
896 pub fn ack_replica(&self, id: &str, lsn: u64) {
897 let now_ms = crate::utils::now_unix_millis() as u128;
898 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
899 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
900 r.last_acked_lsn = r.last_acked_lsn.max(lsn);
901 r.last_durable_lsn = r.last_durable_lsn.max(lsn);
902 r.last_seen_at_unix_ms = now_ms;
903 }
904 drop(replicas);
905 self.commit_waiter.record_replica_ack(id, lsn);
906 }
907
908 pub fn ack_replica_lsn(&self, id: &str, applied_lsn: u64, durable_lsn: u64) {
914 self.ack_replica_lsn_with_observability(id, applied_lsn, durable_lsn, 0, 0);
915 }
916
917 pub fn ack_replica_lsn_with_observability(
918 &self,
919 id: &str,
920 applied_lsn: u64,
921 durable_lsn: u64,
922 apply_error_count: u64,
923 divergence_count: u64,
924 ) {
925 let now_ms = crate::utils::now_unix_millis() as u128;
926 self.advance_slot(id, applied_lsn, durable_lsn, now_ms);
927 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
928 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
929 r.last_acked_lsn = r.last_acked_lsn.max(applied_lsn);
930 r.last_durable_lsn = r.last_durable_lsn.max(durable_lsn);
931 r.apply_error_count = r.apply_error_count.max(apply_error_count);
932 r.divergence_count = r.divergence_count.max(divergence_count);
933 r.last_seen_at_unix_ms = now_ms;
934 }
935 drop(replicas);
939 self.commit_waiter.record_replica_ack(id, durable_lsn);
940 }
941
942 pub fn note_replica_pull(&self, id: &str, last_sent_lsn: u64) {
947 let now_ms = crate::utils::now_unix_millis() as u128;
948 self.touch_slot(id, now_ms);
949 let mut replicas = self.replicas.write().unwrap_or_else(|e| e.into_inner());
950 if let Some(r) = replicas.iter_mut().find(|r| r.id == id) {
951 r.last_sent_lsn = r.last_sent_lsn.max(last_sent_lsn);
952 r.last_seen_at_unix_ms = now_ms;
953 }
954 }
955
956 pub fn replica_snapshots(&self) -> Vec<ReplicaState> {
960 self.replicas
961 .read()
962 .unwrap_or_else(|e| e.into_inner())
963 .clone()
964 }
965
966 pub fn replication_progress(&self) -> Option<ReplicationProgress> {
967 let replicas = self.replicas.read().unwrap_or_else(|e| e.into_inner());
968 ReplicationProgress::from_replicas(&replicas)
969 }
970
971 pub fn slot_snapshots(&self) -> Vec<ReplicationSlot> {
972 self.slots
973 .read()
974 .unwrap_or_else(|e| e.into_inner())
975 .values()
976 .cloned()
977 .collect()
978 }
979
980 pub fn retention_floor_lsn(&self) -> Option<u64> {
981 let slots = self.slots.read().unwrap_or_else(|e| e.into_inner());
984 if self.ensure_slot_metadata_durable().is_err() {
985 return Some(0);
986 }
987 slots
988 .values()
989 .filter(|slot| slot.invalidation_reason.is_none())
990 .map(|slot| slot.restart_lsn)
991 .min()
992 }
993
994 pub fn prune_retained_wal_through(&self, archived_lsn: u64) -> io::Result<u64> {
995 self.enforce_retention_limits(crate::utils::now_unix_millis() as u128);
996 let prune_lsn = self
997 .retention_floor_lsn()
998 .map(|floor| floor.min(archived_lsn))
999 .unwrap_or(archived_lsn);
1000 if prune_lsn > 0 {
1001 if let Some(spool) = &self.logical_wal_spool {
1002 spool.prune_through(prune_lsn)?;
1003 }
1004 self.wal_buffer.prune_through(prune_lsn);
1005 }
1006 Ok(prune_lsn)
1007 }
1008
1009 pub fn replica_count(&self) -> usize {
1010 self.replicas
1011 .read()
1012 .unwrap_or_else(|e| e.into_inner())
1013 .len()
1014 }
1015
1016 pub fn current_logical_lsn(&self) -> u64 {
1020 self.logical_wal_spool
1021 .as_ref()
1022 .map(|spool| spool.current_lsn())
1023 .unwrap_or_else(|| self.wal_buffer.current_lsn())
1024 }
1025
1026 fn ensure_slot(&self, id: &str, initial_lsn: u64) -> u64 {
1027 let now_ms = crate::utils::now_unix_millis() as u128;
1028 let _publication = self
1029 .metadata_store
1030 .as_ref()
1031 .map(|store| store.vault_publication_lock.lock());
1032 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1033 if let Some(slot) = slots.get_mut(id) {
1034 slot.last_seen_at_unix_ms = now_ms;
1035 let restart_lsn = slot.restart_lsn;
1036 self.persist_slots_locked(&slots);
1037 return restart_lsn;
1038 }
1039 let mut slot = ReplicationSlot::new(
1040 id.to_string(),
1041 reddb_file::TimelineId::initial(),
1042 initial_lsn,
1043 );
1044 slot.last_seen_at_unix_ms = now_ms;
1045 slots.insert(id.to_string(), slot);
1046 let restart_lsn = initial_lsn;
1047 self.persist_slots_locked(&slots);
1048 restart_lsn
1049 }
1050
1051 fn advance_slot(&self, id: &str, confirmed_lsn: u64, restart_lsn: u64, now_ms: u128) {
1052 let _publication = self
1053 .metadata_store
1054 .as_ref()
1055 .map(|store| store.vault_publication_lock.lock());
1056 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1057 let slot = slots.entry(id.to_string()).or_insert_with(|| {
1058 let mut slot =
1059 ReplicationSlot::new(id.to_string(), reddb_file::TimelineId::initial(), 0);
1060 slot.last_seen_at_unix_ms = now_ms;
1061 slot
1062 });
1063 if slot.invalidation_reason.is_some() {
1064 return;
1065 }
1066 slot.confirmed_write_lsn = slot.confirmed_lsn().max(confirmed_lsn).max(restart_lsn);
1067 slot.restart_lsn = slot.restart_lsn.max(restart_lsn);
1068 slot.confirmed_flush_lsn = slot.confirmed_flush_lsn.max(slot.restart_lsn);
1069 slot.confirmed_apply_lsn = slot.confirmed_apply_lsn.max(slot.restart_lsn);
1070 slot.last_seen_at_unix_ms = now_ms;
1071 self.persist_slots_locked(&slots);
1072 }
1073
1074 pub fn touch_slot(&self, id: &str, now_ms: u128) {
1075 let _publication = self
1076 .metadata_store
1077 .as_ref()
1078 .map(|store| store.vault_publication_lock.lock());
1079 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1080 let mut changed = false;
1081 if let Some(slot) = slots.get_mut(id) {
1082 if slot.invalidation_reason.is_none() {
1083 slot.last_seen_at_unix_ms = now_ms;
1084 changed = true;
1085 }
1086 }
1087 if changed {
1088 self.persist_slots_locked(&slots);
1089 }
1090 }
1091
1092 pub fn enforce_retention_limits(
1093 &self,
1094 now_ms: u128,
1095 ) -> Vec<(String, ReplicationSlotInvalidationCause)> {
1096 let current_lsn = self.current_logical_lsn();
1097 let mut invalidated = Vec::new();
1098 let _publication = self
1099 .metadata_store
1100 .as_ref()
1101 .map(|store| store.vault_publication_lock.lock());
1102 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1103 for slot in slots.values_mut() {
1104 if slot.invalidation_reason.is_some() {
1105 continue;
1106 }
1107 let reason = if self.slot_retention_max_lag_lsn > 0
1108 && current_lsn.saturating_sub(slot.restart_lsn) > self.slot_retention_max_lag_lsn
1109 {
1110 Some(ReplicationSlotInvalidationCause::Horizon)
1111 } else if self.slot_idle_timeout_ms > 0
1112 && now_ms.saturating_sub(slot.last_seen_at_unix_ms)
1113 > u128::from(self.slot_idle_timeout_ms)
1114 {
1115 Some(ReplicationSlotInvalidationCause::IdleTimeout)
1116 } else {
1117 None
1118 };
1119 if let Some(reason) = reason {
1120 slot.invalidation_reason = Some(reason);
1121 slot.invalidated_at_unix_ms = Some(now_ms);
1122 invalidated.push((slot.replica_id.clone(), reason));
1123 }
1124 }
1125 if !invalidated.is_empty() {
1126 self.persist_slots_locked(&slots);
1127 }
1128 invalidated
1129 }
1130
1131 pub fn slot_rebootstrap_reason(
1132 &self,
1133 id: &str,
1134 requested_since_lsn: u64,
1135 oldest_available_lsn: Option<u64>,
1136 ) -> Option<ReplicationSlotInvalidationCause> {
1137 let now_ms = crate::utils::now_unix_millis() as u128;
1138 let _publication = self
1139 .metadata_store
1140 .as_ref()
1141 .map(|store| store.vault_publication_lock.lock());
1142 let mut slots = self.slots.write().unwrap_or_else(|e| e.into_inner());
1143 let slot = slots.get_mut(id)?;
1144 if let Some(reason) = slot.invalidation_reason {
1145 return Some(reason);
1146 }
1147 let slot_floor = slot.restart_lsn.max(requested_since_lsn);
1148 if oldest_available_lsn
1149 .map(|oldest| oldest > slot_floor.saturating_add(1))
1150 .unwrap_or(false)
1151 {
1152 slot.invalidation_reason = Some(ReplicationSlotInvalidationCause::WalRemoved);
1153 slot.invalidated_at_unix_ms = Some(now_ms);
1154 self.persist_slots_locked(&slots);
1155 return Some(ReplicationSlotInvalidationCause::WalRemoved);
1156 }
1157 None
1158 }
1159
1160 pub fn plan_replica_resume(
1169 &self,
1170 id: &str,
1171 requested_since_lsn: u64,
1172 oldest_available_lsn: Option<u64>,
1173 ) -> ResumeMode {
1174 if let Some(cause) =
1175 self.slot_rebootstrap_reason(id, requested_since_lsn, oldest_available_lsn)
1176 {
1177 self.full_resync_count
1178 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1179 return ResumeMode::FullRebootstrap { cause };
1180 }
1181 let resume_lsn = self
1182 .slot_snapshots()
1183 .into_iter()
1184 .find(|slot| slot.replica_id == id)
1185 .map(|slot| requested_since_lsn.max(slot.restart_lsn))
1186 .unwrap_or(requested_since_lsn);
1187 self.partial_resync_count
1188 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1189 ResumeMode::PartialResync { resume_lsn }
1190 }
1191
1192 pub fn partial_resync_count(&self) -> u64 {
1195 self.partial_resync_count
1196 .load(std::sync::atomic::Ordering::Relaxed)
1197 }
1198
1199 pub fn full_resync_count(&self) -> u64 {
1203 self.full_resync_count
1204 .load(std::sync::atomic::Ordering::Relaxed)
1205 }
1206
1207 fn persist_slots_locked(&self, slots: &BTreeMap<String, ReplicationSlot>) {
1208 if let Some(store) = &self.metadata_store {
1209 let result = slots::slot_catalog_from_map(slots)
1210 .and_then(|catalog| catalog.encode())
1211 .map_err(io::Error::other)
1212 .and_then(|bytes| super::metadata::write(store, super::metadata::SLOTS, &bytes));
1213 let mut last_error = self
1214 .metadata_error
1215 .lock()
1216 .unwrap_or_else(|error| error.into_inner());
1217 if let Err(error) = result {
1218 *last_error = Some(error.to_string());
1219 warn!(target: "reddb::replication::slots", %error,
1220 "failed to persist replication slots inside database");
1221 } else {
1222 *last_error = None;
1223 }
1224 return;
1225 }
1226 if let Err(err) = persist_replication_slots(self.slot_path.as_deref(), slots) {
1227 warn!(
1228 target: "reddb::replication::slots",
1229 error = %err,
1230 "failed to persist replication slots"
1231 );
1232 }
1233 if let Err(err) = persist_replication_slot_catalog(self.slot_catalog_path.as_deref(), slots)
1234 {
1235 warn!(
1236 target: "reddb::replication::slots",
1237 error = %err,
1238 "failed to persist binary replication slot catalog"
1239 );
1240 }
1241 }
1242
1243 pub(crate) fn ensure_slot_metadata_durable(&self) -> io::Result<()> {
1244 match self
1245 .metadata_error
1246 .lock()
1247 .unwrap_or_else(|error| error.into_inner())
1248 .as_ref()
1249 {
1250 Some(error) => Err(io::Error::other(error.clone())),
1251 None => Ok(()),
1252 }
1253 }
1254}
1255
1256#[cfg(test)]
1257mod tests;