Skip to content

Commit 4f3bcd2

Browse files
Dragyxfaukah
authored andcommitted
Implement eager store backend.
Focuses on correctness over speed.
1 parent 8370db5 commit 4f3bcd2

File tree

5 files changed

+362
-155
lines changed

5 files changed

+362
-155
lines changed

src/store.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#![allow(clippy::mem_forget)]
2-
2+
//! Provides an interface for querying data from the nix store.
3+
//!
4+
//! - [LazyDBConnection] is a lazy connection the underlying sqlite database.
5+
mod db_common;
36
mod db_eager;
47
mod db_lazy;
58
mod nix_command;
69
mod queries;
710

811
pub mod store {
9-
pub use crate::store::db_lazy::DBConnection;
12+
pub use crate::store::db_lazy::LazyDBConnection;
1013
}
1114

1215
use std::{
@@ -26,8 +29,9 @@ use crate::{
2629
DerivationId,
2730
StorePath,
2831
store::{
32+
db_eager::EagerDBConnection,
2933
nix_command::CommandBackend,
30-
store::DBConnection,
34+
store::LazyDBConnection,
3135
},
3236
};
3337
/// The normal database connection
@@ -83,6 +87,35 @@ impl<'a> CombinedStoreBackend<'a> {
8387
pub fn new(backends: Vec<Box<dyn StoreBackendPrintable<'a>>>) -> Self {
8488
Self { backends }
8589
}
90+
91+
/// Returns a backend that is focused on performance.
92+
///
93+
/// The first choice is using direct sqlite queries that
94+
/// return the rows lazily, but might skip rows should
95+
/// a row conversion after the first row fail. Note that
96+
/// this should be extremely unlikely / impossible since
97+
/// the current row mappings perform only very basic conversion.
98+
pub fn default_lazy() -> Self {
99+
CombinedStoreBackend::new(vec![
100+
Box::new(LazyDBConnection::new(DATABASE_PATH)),
101+
Box::new(EagerDBConnection::new(DATABASE_PATH_IMMUTABLE)),
102+
Box::new(CommandBackend),
103+
])
104+
}
105+
106+
/// Returns a backend that is focused solely on absolutely guaranteeing
107+
/// correct results at the cost of memory usage and database speed.
108+
///
109+
/// Note that [DATABASE_PATH_IMMUTABLE] is not used here, since opening
110+
/// the database can lead to undefined results (also silently with no errors)
111+
/// if the database is actually modified while opened.
112+
pub fn default_eager() -> Self {
113+
CombinedStoreBackend::new(vec![
114+
Box::new(EagerDBConnection::new(DATABASE_PATH)),
115+
Box::new(CommandBackend),
116+
])
117+
}
118+
86119
// tries to execute a query until it succeeds or all connected backends have
87120
// been tried
88121
fn fallback_query<'b, F, Ret>(&'b self, query: F, path: &Path) -> Result<Ret>
@@ -121,11 +154,7 @@ impl<'a> CombinedStoreBackend<'a> {
121154

122155
impl<'a> Default for CombinedStoreBackend<'a> {
123156
fn default() -> Self {
124-
CombinedStoreBackend::new(vec![
125-
Box::new(DBConnection::new(DATABASE_PATH)),
126-
Box::new(DBConnection::new(DATABASE_PATH_IMMUTABLE)),
127-
Box::new(CommandBackend),
128-
])
157+
Self::default_lazy()
129158
}
130159
}
131160

src/store/db_common.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::path::Path;
2+
3+
use anyhow::{
4+
Context as _,
5+
Result,
6+
anyhow,
7+
};
8+
use rusqlite::{
9+
Connection,
10+
OpenFlags,
11+
};
12+
use size::Size;
13+
14+
use crate::{
15+
path_to_canonical_string,
16+
store::queries,
17+
};
18+
19+
pub(crate) fn default_sqlite_connection(path: &str) -> Result<Connection> {
20+
let inner = rusqlite::Connection::open_with_flags(
21+
path,
22+
OpenFlags::SQLITE_OPEN_READ_ONLY // We only run queries, safeguard against corrupting the DB.
23+
| OpenFlags::SQLITE_OPEN_NO_MUTEX // Part of the default flags, rusqlite takes care of locking anyways.
24+
| OpenFlags::SQLITE_OPEN_URI,
25+
)
26+
.with_context(|| format!("failed to connect to Nix database at {}", path))?;
27+
28+
// Perform a batched query to set some settings using PRAGMA
29+
// the main performance bottleneck when dix was run before
30+
// was that the database file has to be brought from disk into
31+
// memory.
32+
//
33+
// We read a large part of the DB anyways in each query,
34+
// so it makes sense to set aside a large region of memory-mapped
35+
// I/O prevent incurring page faults which can be done using
36+
// `mmap_size`.
37+
//
38+
// This made a performance difference of about 500ms (but only
39+
// when it was first run for a long time!).
40+
//
41+
// The file pages of the store can be evicted from main memory
42+
// using:
43+
//
44+
// ```bash
45+
// dd of=/nix/var/nix/db/db.sqlite oflag=nocache conv=notrunc,fdatasync count=0
46+
// ```
47+
//
48+
// If you want to test this. Source: <https://unix.stackexchange.com/questions/36907/drop-a-specific-file-from-the-linux-filesystem-cache>.
49+
//
50+
// Documentation about the settings can be found here: <https://www.sqlite.org/pragma.html>
51+
//
52+
// [0]: 256MB, enough to fit the whole DB (at least on my system - Dragyx).
53+
// [1]: Always store temporary tables in memory.
54+
inner
55+
.execute_batch(
56+
"
57+
PRAGMA mmap_size=268435456; -- See [0].
58+
PRAGMA temp_store=2; -- See [1].
59+
PRAGMA query_only;
60+
",
61+
)
62+
.with_context(|| format!("failed to cache Nix database at {}", path))?;
63+
Ok(inner)
64+
}
65+
66+
// FIXME: why is this marked as dead code? It is used by both the lazy
67+
// and eager backend implementation
68+
pub(crate) fn default_close_inner_connection(
69+
path: &str,
70+
maybe_conn: &mut Option<Connection>,
71+
) -> Result<()> {
72+
let conn = maybe_conn.take().ok_or_else(|| {
73+
anyhow!("Tried to close connection to {} that does not exist", path)
74+
})?;
75+
conn.close().map_err(|(conn_old, err)| {
76+
*maybe_conn = Some(conn_old);
77+
anyhow::Error::from(err).context("failed to close Nix database")
78+
})
79+
}
80+
81+
pub(crate) fn query_closure_size(
82+
conn: &Connection,
83+
path: &Path,
84+
) -> Result<Size> {
85+
let path = path_to_canonical_string(path)?;
86+
87+
let closure_size = conn
88+
.prepare_cached(queries::QUERY_CLOSURE_SIZE)?
89+
.query_row([path], |row| Ok(Size::from_bytes(row.get::<_, i64>(0)?)))?;
90+
91+
Ok(closure_size)
92+
}

src/store/db_eager.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use std::{
2+
fmt::{
3+
self,
4+
Display,
5+
},
6+
path::Path,
7+
};
8+
9+
use anyhow::{
10+
Result,
11+
anyhow,
12+
};
13+
use rusqlite::Row;
14+
15+
use crate::{
16+
DerivationId,
17+
StorePath,
18+
path_to_canonical_string,
19+
store::{
20+
StoreBackend,
21+
db_common::{
22+
self,
23+
},
24+
queries,
25+
},
26+
};
27+
28+
#[derive(Debug)]
29+
pub struct EagerDBConnection<'a> {
30+
path: &'a str,
31+
conn: Option<rusqlite::Connection>,
32+
}
33+
34+
impl Display for EagerDBConnection<'_> {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
write!(f, "DBConnection({})", self.path)
37+
}
38+
}
39+
40+
impl<'a> EagerDBConnection<'a> {
41+
/// Create a new connection.
42+
pub fn new(path: &'a str) -> EagerDBConnection<'a> {
43+
EagerDBConnection { path, conn: None }
44+
}
45+
/// returns a reference to the inner connection
46+
///
47+
/// raises an error if the connection has not been established
48+
fn get_inner(&self) -> Result<&rusqlite::Connection> {
49+
self
50+
.conn
51+
.as_ref()
52+
.ok_or_else(|| anyhow!("Attempted to use database before connecting."))
53+
}
54+
55+
/// Executes a query that returns multiple rows and returns
56+
/// an iterator over them where the `map` is used to map
57+
/// the rows to `T`.
58+
///
59+
/// Note that this function collects all rows before returning
60+
/// and raises the first error that is encountered (if any exist).
61+
pub(crate) fn execute_row_query_with_path<T, M>(
62+
&self,
63+
query: &str,
64+
path: &Path,
65+
map: M,
66+
) -> Result<Box<dyn Iterator<Item = T> + '_>>
67+
where
68+
T: 'static,
69+
M: Fn(&Row) -> rusqlite::Result<T> + 'static,
70+
{
71+
let path = path_to_canonical_string(path)?;
72+
let mut results = Vec::new();
73+
let mut query = self.get_inner()?.prepare_cached(query)?;
74+
let queried_rows = query.query_map([path], map)?;
75+
for row in queried_rows {
76+
results.push(row?);
77+
}
78+
Ok(Box::new(results.into_iter()))
79+
}
80+
}
81+
82+
impl<'a> StoreBackend<'a> for EagerDBConnection<'_> {
83+
fn connect(&mut self) -> Result<()> {
84+
self.conn = Some(db_common::default_sqlite_connection(self.path)?);
85+
Ok(())
86+
}
87+
88+
fn connected(&self) -> bool {
89+
self.conn.is_some()
90+
}
91+
92+
fn close(&mut self) -> Result<()> {
93+
db_common::default_close_inner_connection(self.path, &mut self.conn)
94+
}
95+
96+
fn query_closure_size(&self, path: &std::path::Path) -> Result<size::Size> {
97+
db_common::query_closure_size(self.get_inner()?, path)
98+
}
99+
100+
fn query_system_derivations(
101+
&self,
102+
system: &std::path::Path,
103+
) -> Result<Box<dyn Iterator<Item = crate::StorePath> + '_>> {
104+
self.execute_row_query_with_path(
105+
queries::QUERY_SYSTEM_DERIVATIONS,
106+
system,
107+
|row| Ok(StorePath(row.get::<_, String>(0)?.into())),
108+
)
109+
}
110+
111+
fn query_dependents(
112+
&self,
113+
path: &std::path::Path,
114+
) -> Result<Box<dyn Iterator<Item = crate::StorePath> + '_>> {
115+
self.execute_row_query_with_path(queries::QUERY_DEPENDENTS, path, |row| {
116+
Ok(StorePath(row.get::<_, String>(0)?.into()))
117+
})
118+
}
119+
120+
fn query_dependency_graph(
121+
&self,
122+
path: &std::path::Path,
123+
) -> Result<
124+
Box<dyn Iterator<Item = (crate::DerivationId, crate::DerivationId)> + '_>,
125+
> {
126+
self.execute_row_query_with_path(
127+
queries::QUERY_DEPENDENCY_GRAPH,
128+
path,
129+
|row| Ok((DerivationId(row.get(0)?), DerivationId(row.get(1)?))),
130+
)
131+
}
132+
}

0 commit comments

Comments
 (0)