mod db;
pub mod nix_command;
mod queries;
#[cfg(test)] pub(crate) mod test_utils;
use std::{
fmt::Display,
path::Path,
};
pub use db::DbConnection;
use eyre::{
Result,
eyre,
};
pub use nix_command::CommandBackend;
use size::Size;
use tracing::warn;
use crate::StorePath;
pub const DATABASE_PATH: &str = "file:/nix/var/nix/db/db.sqlite";
pub const DATABASE_PATH_IMMUTABLE: &str =
"file:/nix/var/nix/db/db.sqlite?immutable=1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorePathInfo {
path: StorePath,
nar_size: Size,
}
impl StorePathInfo {
#[must_use]
pub const fn new(path: StorePath, nar_size: Size) -> Self {
Self { path, nar_size }
}
#[must_use]
pub const fn path(&self) -> &StorePath {
&self.path
}
#[must_use]
pub const fn nar_size(&self) -> Size {
self.nar_size
}
}
pub trait StoreBackend: Display {
fn connect(&mut self) -> Result<()>;
fn connected(&self) -> bool;
fn close(&mut self) -> Result<()>;
fn query_closure_size(&self, path: &Path) -> Result<Size> {
Ok(Size::from_bytes(
self
.query_closure_path_info(path)?
.iter()
.map(|info| info.nar_size().bytes())
.sum::<i64>(),
))
}
fn query_system_derivations(&self, system: &Path) -> Result<Vec<StorePath>>;
fn query_dependents(&self, path: &Path) -> Result<Vec<StorePath>>;
fn query_closure_path_info(&self, path: &Path) -> Result<Vec<StorePathInfo>>;
}
pub struct CombinedStoreBackend {
backends: Vec<Box<dyn StoreBackend>>,
}
impl CombinedStoreBackend {
#[must_use]
pub fn new(backends: Vec<Box<dyn StoreBackend>>) -> Self {
Self { backends }
}
#[must_use]
pub fn for_correctness(force_correctness: bool) -> Self {
if force_correctness {
Self::default_correct()
} else {
Self::default_fast()
}
}
pub(crate) fn query_with_correctness<T>(
force_correctness: bool,
query: impl FnOnce(&Self) -> Result<T>,
) -> Result<T> {
let mut backend = Self::for_correctness(force_correctness);
backend.connect()?;
let result = query(&backend);
let close_result = backend.close();
match result {
Ok(value) => {
close_result?;
Ok(value)
},
Err(error) => Err(error),
}
}
#[must_use]
pub fn default_fast() -> Self {
Self::new(vec![
Box::new(DbConnection::new(DATABASE_PATH)),
Box::new(DbConnection::new(DATABASE_PATH_IMMUTABLE)),
Box::new(CommandBackend::default()),
])
}
#[must_use]
pub fn default_correct() -> Self {
Self::new(vec![
Box::new(DbConnection::new(DATABASE_PATH)),
Box::new(CommandBackend::default()),
])
}
fn fallback_query<F, Ret>(&self, query: F, path: &Path) -> Result<Ret>
where
F: Fn(&dyn StoreBackend, &Path) -> Result<Ret>,
{
let mut combined_err: Option<eyre::Report> = None;
for (i, backend) in self.backends.iter().enumerate() {
if !backend.connected() {
warn!(
"Skipping backend {i} ({backend}) in query {path:?}: not connected"
);
continue;
}
let res = query(backend.as_ref(), path);
match res {
Ok(_) => return res,
Err(err) => {
warn!(
"Failed to query path {path:?} on current backend {backend} \
({i}): {}",
&err
);
combined_err = match combined_err {
Some(combined) => Some(combined.wrap_err(err)),
None => Some(err),
};
},
}
}
warn!("All store backends for path {path:?} failed");
Err(combined_err.unwrap_or_else(|| eyre!("No internal stores to query.")))
}
}
impl Display for CombinedStoreBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CombinedStoreBackend({} backends)", self.backends.len())
}
}
impl Default for CombinedStoreBackend {
fn default() -> Self {
Self::default_fast()
}
}
impl StoreBackend for CombinedStoreBackend {
fn connect(&mut self) -> Result<()> {
tracing::debug!(
backend_count = self.backends.len(),
"connecting to store backends"
);
let mut combined_err: Option<eyre::Report> = None;
let mut connected_count = 0;
for (i, backend) in self.backends.iter_mut().enumerate() {
tracing::trace!(backend_index = i, backend = %backend, "attempting to connect to backend");
if let Err(err) = backend.connect() {
warn!(
"Unable to connect to store backend {i}: {backend}, trying next. \
(error: {err})"
);
combined_err = match combined_err {
Some(combined) => Some(combined.wrap_err(err)),
None => Some(err),
}
} else {
connected_count += 1;
tracing::debug!(backend_index = i, backend = %backend, "backend connected successfully");
}
}
tracing::info!(
connected_count = connected_count,
total_count = self.backends.len(),
"backend connection complete"
);
let any_succeeded = self.backends.iter().any(|f| f.connected());
if let Some(err) = &combined_err
&& any_succeeded
{
warn!("Some backends failed to connect: {err}");
}
if any_succeeded {
Ok(())
} else {
combined_err =
combined_err.map(|err| err.wrap_err("All backends failed to connect."));
Err(combined_err.unwrap_or_else(|| eyre!("No backends to connect to.")))
}
}
fn connected(&self) -> bool {
self.backends.iter().any(|backend| backend.connected())
}
fn close(&mut self) -> Result<()> {
let mut combined_err: Option<eyre::Report> = None;
for (i, backend) in self.backends.iter_mut().enumerate() {
if backend.connected()
&& let Err(err) = backend.close()
{
warn!("Unable to close store backend {i}: {backend}. (error: {err})");
combined_err = match combined_err {
Some(combined) => Some(combined.wrap_err(err)),
None => Some(err),
};
}
}
combined_err.map_or_else(
|| Ok(()),
|err| Err(err.wrap_err("One or more backends failed to close.")),
)
}
fn query_system_derivations(&self, system: &Path) -> Result<Vec<StorePath>> {
self.fallback_query(
|backend, system| backend.query_system_derivations(system),
system,
)
}
fn query_dependents(&self, path: &Path) -> Result<Vec<StorePath>> {
self.fallback_query(|backend, path| backend.query_dependents(path), path)
}
fn query_closure_path_info(&self, path: &Path) -> Result<Vec<StorePathInfo>> {
self.fallback_query(
|backend, path| backend.query_closure_path_info(path),
path,
)
}
}
#[cfg(test)]
mod test {
use std::{
cell::RefCell,
fmt,
};
use super::*;
struct MockStoreBackend {
name: String,
connected: bool,
fail_connect: bool,
fail_query: bool,
query_called: RefCell<bool>,
}
impl MockStoreBackend {
fn new(name: &str, fail_connect: bool, fail_query: bool) -> Self {
Self {
name: name.to_string(),
connected: false,
fail_connect,
fail_query,
query_called: RefCell::new(false),
}
}
}
impl Display for MockStoreBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MockStoreBackend({})", self.name)
}
}
impl StoreBackend for MockStoreBackend {
fn connect(&mut self) -> Result<()> {
if self.fail_connect {
Err(eyre!("Connection failed"))
} else {
self.connected = true;
Ok(())
}
}
fn connected(&self) -> bool {
self.connected
}
fn close(&mut self) -> Result<()> {
self.connected = false;
Ok(())
}
fn query_system_derivations(
&self,
_system: &Path,
) -> Result<Vec<StorePath>> {
*self.query_called.borrow_mut() = true;
if self.fail_query {
Err(eyre!("Query failed"))
} else {
Ok(Vec::new())
}
}
fn query_dependents(&self, _path: &Path) -> Result<Vec<StorePath>> {
*self.query_called.borrow_mut() = true;
if self.fail_query {
Err(eyre!("Query failed"))
} else {
Ok(Vec::new())
}
}
fn query_closure_path_info(
&self,
_path: &Path,
) -> Result<Vec<StorePathInfo>> {
*self.query_called.borrow_mut() = true;
if self.fail_query {
Err(eyre!("Query failed"))
} else {
Ok(vec![StorePathInfo::new(
StorePath::try_from(
Path::new("/nix/store/0123456789abcdefghijklmnopqrstuv-dummy")
.to_path_buf(),
)?,
Size::from_bytes(100),
)])
}
}
}
#[test]
fn test_connect_fallback() {
let f1 = Box::new(MockStoreBackend::new("f1", true, false));
let f2 = Box::new(MockStoreBackend::new("f2", false, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
assert!(combined.connect().is_ok());
assert!(combined.connected());
}
#[test]
fn test_connect_all_fail() {
let f1 = Box::new(MockStoreBackend::new("f1", true, false));
let f2 = Box::new(MockStoreBackend::new("f2", true, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
assert!(combined.connect().is_err());
assert!(!combined.connected());
}
#[test]
fn test_query_fallback() {
let f1 = Box::new(MockStoreBackend::new("f1", false, true));
let f2 = Box::new(MockStoreBackend::new("f2", false, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
combined.connect().unwrap();
let res = combined.query_closure_size(Path::new("/dummy"));
assert!(res.is_ok());
assert_eq!(res.unwrap(), Size::from_bytes(100));
}
#[test]
fn test_path_query_fallback() {
let f1 = Box::new(MockStoreBackend::new("f1", false, true));
let f2 = Box::new(MockStoreBackend::new("f2", false, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
combined.connect().unwrap();
let res = combined.query_dependents(Path::new("/dummy"));
assert!(res.is_ok());
assert_eq!(res.unwrap(), Vec::new());
}
#[test]
fn test_query_skip_unconnected() {
let f1 = Box::new(MockStoreBackend::new("f1", true, false));
let f2 = Box::new(MockStoreBackend::new("f2", false, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
combined.connect().unwrap();
let res = combined.query_closure_size(Path::new("/dummy"));
assert!(res.is_ok());
assert_eq!(res.unwrap(), Size::from_bytes(100));
let f1 = Box::new(MockStoreBackend::new("f1", true, false));
let f2 = Box::new(MockStoreBackend::new("f2", true, false));
let f3 = Box::new(MockStoreBackend::new("f3", false, false));
let mut combined = CombinedStoreBackend::new(vec![f1, f2, f3]);
combined.connect().unwrap();
let res = combined.query_closure_size(Path::new("/dummy"));
assert_eq!(res.unwrap(), Size::from_bytes(100));
assert!(combined.connect().is_ok());
assert!(combined.connected());
}
#[test]
fn test_query_all_fail() {
let f1 = Box::new(MockStoreBackend::new("f1", false, true));
let f2 = Box::new(MockStoreBackend::new("f2", false, true));
let mut combined = CombinedStoreBackend::new(vec![f1, f2]);
combined.connect().unwrap();
let res = combined.query_closure_size(Path::new("/dummy"));
assert!(res.is_err());
}
}