use std::fs::{File, OpenOptions};
use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::eyre::{Report, eyre};
use fs4::FileExt;
use tracing::debug;
use crate::{
TestBootstrapSettings,
error::{BootstrapError, BootstrapResult},
observability::LOG_TARGET,
};
pub(crate) const SETUP_LOCK_FILE: &str = ".pg-embed-setup.lock";
#[derive(Debug)]
pub(super) struct SetupLock {
_file: File,
}
impl SetupLock {
pub(super) fn install_root(bootstrap: &TestBootstrapSettings) -> BootstrapResult<Utf8PathBuf> {
Utf8PathBuf::from_path_buf(bootstrap.settings.installation_dir.clone())
.map_err(|path| eyre!("installation_dir is not UTF-8: {}", path.display()).into())
}
pub(super) fn acquire(bootstrap: &TestBootstrapSettings) -> BootstrapResult<Self> {
Self::acquire_at(&Self::install_root(bootstrap)?)
}
pub(super) fn acquire_at(install_dir: &Utf8Path) -> BootstrapResult<Self> {
let (lock, waited) = Self::lock_timed(install_dir)?;
log_waited(install_dir, waited);
Ok(lock)
}
fn lock_timed(install_dir: &Utf8Path) -> BootstrapResult<(Self, std::time::Duration)> {
std::fs::create_dir_all(install_dir).map_err(|err| lock_error(install_dir, err))?;
let path = install_dir.join(SETUP_LOCK_FILE);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|err| lock_error(&path, err))?;
let started = std::time::Instant::now();
FileExt::lock(&file).map_err(|err| lock_error(&path, err))?;
Ok((Self { _file: file }, started.elapsed()))
}
#[cfg(feature = "async-api")]
pub(super) async fn acquire_async_at(install_dir: Utf8PathBuf) -> BootstrapResult<Self> {
let dir = install_dir.clone();
let (lock, waited) = tokio::task::spawn_blocking(move || Self::lock_timed(&dir))
.await
.map_err(|err| {
BootstrapError::from(Report::new(err).wrap_err("setup lock task failed"))
})??;
log_waited(&install_dir, waited);
Ok(lock)
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum InstallLock<'a> {
Held,
Released {
root: &'a Utf8Path,
},
}
impl<'a> InstallLock<'a> {
pub(super) fn after_cache_lookup(
setup: SetupLock,
cache_hit: bool,
install_root: &'a Utf8Path,
) -> (Option<SetupLock>, Self) {
if cache_hit {
drop(setup);
(None, Self::Released { root: install_root })
} else {
(Some(setup), Self::Held)
}
}
}
impl InstallLock<'_> {
pub(super) fn for_hook(self) -> BootstrapResult<Option<SetupLock>> {
match self {
Self::Released { root } => SetupLock::acquire_at(root).map(Some),
Self::Held => Ok(None),
}
}
#[cfg(feature = "async-api")]
pub(super) async fn for_hook_async(self) -> BootstrapResult<Option<SetupLock>> {
match self {
Self::Released { root } => SetupLock::acquire_async_at(root.to_owned()).await.map(Some),
Self::Held => Ok(None),
}
}
}
fn log_waited(install_dir: &Utf8Path, waited: std::time::Duration) {
debug!(
target: LOG_TARGET,
lock = %install_dir.join(SETUP_LOCK_FILE),
waited_ms = waited.as_millis(),
"took the install tree's setup lock"
);
}
fn lock_error(path: &Utf8Path, err: std::io::Error) -> BootstrapError {
BootstrapError::from(Report::new(err).wrap_err(format!("cannot take the setup lock at {path}")))
}
#[cfg(test)]
#[path = "setup_lock_tests.rs"]
mod tests;