Skip to main content

powdb_backup/
bootstrap.rs

1use 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
19/// Restore a sync-enabled full backup into an empty replica path and pin the
20/// primary-side retained history needed to catch that replica up.
21///
22/// This restores the snapshot and pins the primary-side cursor; local tail apply
23/// is intentionally owned by `powdb-sync::apply_retained_tail` so bootstrap and
24/// replica catch-up can be tested independently.
25pub 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}