use crate::lease::{new_instance_id, BucketLease, LeaseError, LeaseWatch};
use crate::restore::{
install_interrupted, restore_in_place, staging_leftovers, sweep_in_place,
undo_interrupted_install, RestoreS3Error, RestoreS3Outcome,
};
use crate::store::BackupConfig;
use object_store::ObjectStore;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
#[derive(Debug)]
pub struct BootLease {
lease: Arc<BucketLease>,
renewer: JoinHandle<()>,
op_timeout: Duration,
}
impl BootLease {
fn hold(lease: BucketLease, op_timeout: Duration) -> Self {
let lease = Arc::new(lease);
let renewer = tokio::spawn(renew(Arc::clone(&lease), op_timeout));
Self {
lease,
renewer,
op_timeout,
}
}
pub fn op_timeout(&self) -> Duration {
self.op_timeout
}
pub fn detach(self) -> Arc<BucketLease> {
self.renewer.abort();
self.lease
}
pub fn instance_id(&self) -> &str {
self.lease.instance_id()
}
pub async fn into_lease(self) -> Arc<BucketLease> {
self.renewer.abort();
let _ = self.renewer.await;
self.lease
}
pub async fn release(self) {
let op_timeout = self.op_timeout;
let lease = self.into_lease().await;
let Ok(lease) = Arc::try_unwrap(lease) else {
return;
};
match tokio::time::timeout(op_timeout, lease.release()).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::warn!(error = %e, "could not release the backup bucket lease; it expires by itself")
}
Err(_) => tracing::warn!("releasing the backup bucket lease timed out"),
}
}
}
async fn renew(lease: Arc<BucketLease>, op_timeout: Duration) {
let every = lease.renew_interval().max(Duration::from_millis(1));
loop {
tokio::time::sleep(every).await;
match tokio::time::timeout(op_timeout, lease.renew()).await {
Ok(Ok(())) => {}
Ok(Err(e @ LeaseError::Lost { .. })) => {
tracing::warn!(error = %e, "lost the backup bucket lease during the restore");
return;
}
Ok(Err(e)) => tracing::warn!(error = %e, "bucket lease renewal failed; retrying"),
Err(_) => tracing::warn!("bucket lease renewal timed out; retrying"),
}
}
}
#[derive(Debug)]
pub enum BootRestore {
Skipped,
NoBackup { lease: BootLease },
Restored {
outcome: Box<RestoreS3Outcome>,
lease: BootLease,
undone: Vec<String>,
leftovers: Vec<std::path::PathBuf>,
},
}
#[derive(Debug)]
pub enum BootError {
ForeignLease { holder: String },
Lease(String),
Restore(RestoreS3Error),
Io(std::io::Error),
}
impl std::fmt::Display for BootError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BootError::ForeignLease { holder } => write!(
f,
"the backup bucket's lease is held by another instance ({holder}), which may \
still be writing to the backup; nothing was restored. Stop that instance, or \
wait for its lease to expire (MUSHROOMDB_BACKUP_LEASE_TTL), and start again"
),
BootError::Lease(m) => write!(f, "taking the bucket lease: {m}"),
BootError::Restore(e) => write!(f, "{e}"),
BootError::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for BootError {}
pub async fn restore_on_boot(
config: &BackupConfig,
store: Arc<dyn ObjectStore>,
data_dir: &Path,
) -> Result<BootRestore, BootError> {
let prefix = config
.location()
.map_err(|e| BootError::Restore(RestoreS3Error::Config(e.to_string())))?
.prefix;
let dir = data_dir.to_path_buf();
let (interrupted, holds) = blocking(move || {
Ok((
install_interrupted(&dir),
mushroomdb::restore::holds_a_store(&dir),
))
})
.await?;
if holds && !interrupted {
let dir = data_dir.to_path_buf();
for path in blocking(move || Ok(sweep_in_place(&dir))).await? {
tracing::info!(path = %path.display(), "removed a killed restore's leftover");
}
return Ok(BootRestore::Skipped);
}
let op_timeout = Duration::from_millis(config.op_timeout_ms);
let (instance_id, mut watch) = (new_instance_id(), LeaseWatch::default());
let acquire = BucketLease::acquire_as(
Arc::clone(&store),
&prefix,
config.lease_ttl_s,
&instance_id,
&mut watch,
);
let lease = match tokio::time::timeout(op_timeout * 4, acquire).await {
Ok(Ok(lease)) => BootLease::hold(lease, op_timeout),
Ok(Err(LeaseError::ForeignActive { instance_id })) => {
return Err(BootError::ForeignLease {
holder: instance_id,
})
}
Ok(Err(e @ LeaseError::Store(_))) if config.lease_conditional => {
return Err(BootError::Lease(format!(
"{e} (if this service rejects conditional PUT headers, set \
MUSHROOMDB_BACKUP_LEASE_CONDITIONAL=off)"
)))
}
Ok(Err(e)) => return Err(BootError::Lease(e.to_string())),
Err(_) => {
return Err(BootError::Lease(
"the bucket did not answer within the request deadline".into(),
))
}
};
let undone = if interrupted {
let dir = data_dir.to_path_buf();
match blocking(move || undo_interrupted_install(&dir).map_err(BootError::Io)).await {
Ok(undone) => {
tracing::warn!(
files = undone.len(),
"removed the files of a restore on boot that a crash interrupted"
);
undone
}
Err(e) => {
lease.release().await;
return Err(e);
}
}
} else {
Vec::new()
};
match restore_in_place(store, &prefix, data_dir).await {
Ok(outcome) => {
let dir = data_dir.to_path_buf();
let leftovers = blocking(move || Ok(staging_leftovers(&dir))).await?;
for path in &leftovers {
tracing::warn!(
path = %path.display(),
"a killed restore's leftover stays in the data directory"
);
}
Ok(BootRestore::Restored {
outcome: Box::new(outcome),
lease,
undone,
leftovers,
})
}
Err(RestoreS3Error::NoBackup { .. }) => {
let dir = data_dir.to_path_buf();
for path in blocking(move || Ok(sweep_in_place(&dir))).await? {
tracing::info!(path = %path.display(), "removed a killed restore's leftover");
}
Ok(BootRestore::NoBackup { lease })
}
Err(e) => {
lease.release().await;
Err(BootError::Restore(e))
}
}
}
async fn blocking<T: Send + 'static>(
f: impl FnOnce() -> Result<T, BootError> + Send + 'static,
) -> Result<T, BootError> {
tokio::task::spawn_blocking(f)
.await
.map_err(|e| BootError::Io(std::io::Error::other(e.to_string())))?
}