powdb_backup/
bootstrap.rs1use std::fs;
2use std::io;
3use std::path::Path;
4
5use crate::manifest::{BackupManifest, SyncSnapshotMetadata};
6use crate::restore::{restore_with_sync_mode, RestoreSyncMode};
7use powdb_storage::catalog::Catalog;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ReplicaBootstrapSummary {
11 pub replica_id: String,
12 pub snapshot_lsn: u64,
13 pub remote_lsn: u64,
14 pub retained_units_available: usize,
15 pub retained_tail_start_lsn: Option<u64>,
16 pub retained_tail_end_lsn: Option<u64>,
17}
18
19pub fn bootstrap_replica_from_full_backup(
26 primary: &mut Catalog,
27 backup_dir: &Path,
28 replica_data_dir: &Path,
29 replica_id: &str,
30) -> io::Result<ReplicaBootstrapSummary> {
31 let manifest = BackupManifest::read(backup_dir)?;
32 let sync = manifest.sync.as_ref().ok_or_else(|| {
33 invalid_input("replica bootstrap requires a sync-enabled full backup manifest")
34 })?;
35 let primary_data_dir = primary.data_dir().to_path_buf();
36 let identity = validate_bootstrap_identity(&primary_data_dir, sync)?;
37
38 powdb_sync::checkpoint_preserving_retained_segments_if_enabled(primary)?;
39 let remote_lsn = primary.max_lsn();
40 if remote_lsn < manifest.source_lsn {
41 return Err(invalid_input(format!(
42 "remote LSN {remote_lsn} is behind snapshot LSN {}",
43 manifest.source_lsn
44 )));
45 }
46 if powdb_sync::read_replica_cursors(&primary_data_dir)?
47 .iter()
48 .any(|cursor| cursor.replica_id == replica_id && cursor.active)
49 {
50 return Err(io::Error::new(
51 io::ErrorKind::AlreadyExists,
52 "replica cursor is already active; retire it before bootstrap",
53 ));
54 }
55
56 let availability = powdb_sync::validate_v1_retained_tail_applyable(
57 &powdb_sync::retained_segments_dir(&primary_data_dir),
58 identity.segment_identity(),
59 manifest.source_lsn,
60 remote_lsn,
61 )?;
62 restore_with_sync_mode(
63 backup_dir,
64 replica_data_dir,
65 RestoreSyncMode::PreserveSyncIdentity,
66 )?;
67 if let Err(err) = powdb_sync::seed_retained_apply_boundary(
68 replica_data_dir,
69 identity.segment_identity(),
70 manifest.source_lsn,
71 ) {
72 return cleanup_restored_replica(replica_data_dir, err);
73 }
74 if let Err(err) = powdb_sync::register_bootstrap_cursor(
75 &primary_data_dir,
76 replica_id,
77 manifest.source_lsn,
78 remote_lsn,
79 ) {
80 return cleanup_restored_replica(replica_data_dir, err);
81 }
82
83 Ok(ReplicaBootstrapSummary {
84 replica_id: replica_id.to_string(),
85 snapshot_lsn: manifest.source_lsn,
86 remote_lsn,
87 retained_units_available: availability.units_available,
88 retained_tail_start_lsn: availability.first_lsn,
89 retained_tail_end_lsn: availability.last_lsn,
90 })
91}
92
93fn cleanup_restored_replica<T>(replica_data_dir: &Path, err: io::Error) -> io::Result<T> {
94 let cleanup_result = fs::remove_dir_all(replica_data_dir);
95 match cleanup_result {
96 Ok(()) => Err(err),
97 Err(cleanup_err) if cleanup_err.kind() == io::ErrorKind::NotFound => Err(err),
98 Err(cleanup_err) => Err(io::Error::new(
99 err.kind(),
100 format!(
101 "{err}; additionally failed to clean up restored replica directory {}: {cleanup_err}",
102 replica_data_dir.display()
103 ),
104 )),
105 }
106}
107
108fn validate_bootstrap_identity(
109 primary_data_dir: &Path,
110 sync: &SyncSnapshotMetadata,
111) -> io::Result<powdb_sync::DatabaseIdentity> {
112 let manifest_identity = sync.identity.identity()?;
113 let primary_identity = powdb_sync::read_identity(primary_data_dir)?;
114 if primary_identity != manifest_identity {
115 return Err(invalid_input(
116 "primary sync identity does not match bootstrap snapshot identity",
117 ));
118 }
119 Ok(primary_identity)
120}
121
122fn invalid_input(message: impl Into<String>) -> io::Error {
123 io::Error::new(io::ErrorKind::InvalidInput, message.into())
124}