Skip to content

Commit 36f9f4c

Browse files
authored
Merge pull request #1791 from input-output-hk/djo/1707/wal_not_truncated
Upkeep service for aggregator & signer
2 parents 5816ed4 + 35138ad commit 36f9f4c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1134
-185
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ As a minor extension, we have adopted a slightly different versioning convention
1717

1818
- Extended CI build and test steps for MacOS `arm64` runners and include pre-built binaries for MacOS `arm64` in the releases.
1919

20-
- Vacuum aggregator & signer SQLite databases at startup to reduce fragmentation and disk space usage.
20+
- Add a regularly run upkeep task to the `mithril-aggregator` and `mithril-signer` to clean up stale data and optimize their databases.
2121

2222
- **UNSTABLE** Cardano transactions certification:
2323
- Optimize the performances of the computation of the proof with a Merkle map.

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/mithril-persistence/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mithril-persistence"
3-
version = "0.2.13"
3+
version = "0.2.14"
44
description = "Common types, interfaces, and utilities to persist data for Mithril nodes."
55
authors = { workspace = true }
66
edition = { workspace = true }
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use slog::{debug, Logger};
2+
3+
use mithril_common::StdResult;
4+
5+
use crate::sqlite::SqliteConnection;
6+
7+
/// Tasks that can be performed by the SqliteCleaner
8+
#[derive(Eq, PartialEq, Copy, Clone)]
9+
pub enum SqliteCleaningTask {
10+
/// Reconstruct the database file, repacking it into a minimal amount of disk space.
11+
///
12+
/// see: <https://www.sqlite.org/lang_vacuum.html>
13+
///
14+
/// ⚠ This operation can be very slow on large databases ⚠
15+
Vacuum,
16+
/// Run a checkpoint to transfer the data from the WAL file to the main db file and truncate
17+
/// it afterward.
18+
///
19+
/// see: <https://www.sqlite.org/pragma.html#pragma_wal_checkpoint>
20+
WalCheckpointTruncate,
21+
}
22+
23+
impl SqliteCleaningTask {
24+
/// Get the log message for the task.
25+
pub fn log_message(self: SqliteCleaningTask) -> &'static str {
26+
match self {
27+
SqliteCleaningTask::Vacuum => "SqliteCleaner::Running `vacuum` on the database",
28+
SqliteCleaningTask::WalCheckpointTruncate => {
29+
"SqliteCleaner::Running `wal_checkpoint(TRUNCATE)` on the database"
30+
}
31+
}
32+
}
33+
}
34+
35+
/// The SqliteCleaner is responsible for cleaning up databases by performing tasks defined
36+
/// in [SqliteCleaningTask].
37+
pub struct SqliteCleaner<'a> {
38+
connection: &'a SqliteConnection,
39+
logger: Logger,
40+
tasks: Vec<SqliteCleaningTask>,
41+
}
42+
43+
impl<'a> SqliteCleaner<'a> {
44+
/// Create a new instance of the `SqliteCleaner`.
45+
pub fn new(connection: &'a SqliteConnection) -> Self {
46+
Self {
47+
connection,
48+
logger: Logger::root(slog::Discard, slog::o!()),
49+
tasks: vec![],
50+
}
51+
}
52+
53+
/// Set the logger to be used by the cleaner.
54+
pub fn with_logger(mut self, logger: Logger) -> Self {
55+
self.logger = logger;
56+
self
57+
}
58+
59+
/// Set the [SqliteCleaningTask] to be performed by the cleaner.
60+
pub fn with_tasks(mut self, tasks: &[SqliteCleaningTask]) -> Self {
61+
for option in tasks {
62+
self.tasks.push(*option);
63+
}
64+
self
65+
}
66+
67+
/// Cleanup the database by performing the defined tasks.
68+
pub fn run(self) -> StdResult<()> {
69+
if self.tasks.contains(&SqliteCleaningTask::Vacuum) {
70+
debug!(self.logger, "{}", SqliteCleaningTask::Vacuum.log_message());
71+
self.connection.execute("vacuum")?;
72+
}
73+
74+
// Important: If WAL is enabled Vacuuming the database will not shrink until a
75+
// checkpoint is run, so it must be done after vacuuming.
76+
// Note: running a checkpoint when the WAL is disabled is harmless.
77+
if self
78+
.tasks
79+
.contains(&SqliteCleaningTask::WalCheckpointTruncate)
80+
{
81+
debug!(
82+
self.logger,
83+
"{}",
84+
SqliteCleaningTask::WalCheckpointTruncate.log_message()
85+
);
86+
self.connection
87+
.execute("PRAGMA wal_checkpoint(TRUNCATE);")?;
88+
} else {
89+
self.connection.execute("PRAGMA wal_checkpoint(PASSIVE);")?;
90+
}
91+
92+
Ok(())
93+
}
94+
}
95+
96+
#[cfg(test)]
97+
mod tests {
98+
use std::ops::Range;
99+
use std::path::Path;
100+
101+
use mithril_common::test_utils::TempDir;
102+
103+
use crate::sqlite::{ConnectionBuilder, ConnectionOptions, SqliteConnection};
104+
105+
use super::*;
106+
107+
fn add_test_table(connection: &SqliteConnection) {
108+
connection
109+
.execute("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, text TEXT);")
110+
.unwrap();
111+
}
112+
113+
fn fill_test_table(connection: &SqliteConnection, ids: Range<u64>) {
114+
connection
115+
.execute(format!(
116+
"INSERT INTO test (id, text) VALUES {}",
117+
ids.map(|i| format!("({}, 'some text to fill the db')", i))
118+
.collect::<Vec<String>>()
119+
.join(", ")
120+
))
121+
.unwrap();
122+
}
123+
124+
fn delete_test_rows(connection: &SqliteConnection, ids: Range<u64>) {
125+
connection
126+
.execute(format!(
127+
"DELETE FROM test WHERE id >= {} and id < {}",
128+
ids.start, ids.end
129+
))
130+
.unwrap();
131+
}
132+
133+
/// Apply migrations, disable auto_vacuum and mangle the database to create some free pages
134+
/// for the vacuum to reclaim
135+
fn prepare_db_for_vacuum(connection: &SqliteConnection) {
136+
// Disable Auto vacuum to allow the test to check if the vacuum was run
137+
connection
138+
.execute("pragma auto_vacuum = none; vacuum;")
139+
.unwrap();
140+
add_test_table(connection);
141+
fill_test_table(connection, 0..10_000);
142+
// Checkpoint before deletion so entries are transferred from the WAL file to the main db
143+
connection
144+
.execute("PRAGMA wal_checkpoint(PASSIVE)")
145+
.unwrap();
146+
delete_test_rows(connection, 0..5_000);
147+
// Checkpoint after deletion to create free pages in the main db
148+
connection
149+
.execute("PRAGMA wal_checkpoint(PASSIVE)")
150+
.unwrap();
151+
}
152+
153+
fn file_size(path: &Path) -> u64 {
154+
path.metadata()
155+
.unwrap_or_else(|_| panic!("Failed to read len of '{}'", path.display()))
156+
.len()
157+
}
158+
159+
#[test]
160+
fn cleanup_empty_in_memory_db_should_not_crash() {
161+
let connection = ConnectionBuilder::open_memory().build().unwrap();
162+
163+
SqliteCleaner::new(&connection)
164+
.with_tasks(&[SqliteCleaningTask::Vacuum])
165+
.run()
166+
.expect("Vacuum should not fail");
167+
SqliteCleaner::new(&connection)
168+
.with_tasks(&[SqliteCleaningTask::WalCheckpointTruncate])
169+
.run()
170+
.expect("WalCheckpointTruncate should not fail");
171+
}
172+
173+
#[test]
174+
fn cleanup_empty_file_without_wal_db_should_not_crash() {
175+
let db_path = TempDir::create(
176+
"sqlite_cleaner",
177+
"cleanup_empty_file_without_wal_db_should_not_crash",
178+
)
179+
.join("test.db");
180+
let connection = ConnectionBuilder::open_file(&db_path).build().unwrap();
181+
182+
SqliteCleaner::new(&connection)
183+
.with_tasks(&[SqliteCleaningTask::Vacuum])
184+
.run()
185+
.expect("Vacuum should not fail");
186+
SqliteCleaner::new(&connection)
187+
.with_tasks(&[SqliteCleaningTask::WalCheckpointTruncate])
188+
.run()
189+
.expect("WalCheckpointTruncate should not fail");
190+
}
191+
192+
#[test]
193+
fn test_vacuum() {
194+
let db_dir = TempDir::create("sqlite_cleaner", "test_vacuum");
195+
let (db_path, db_wal_path) = (db_dir.join("test.db"), db_dir.join("test.db-wal"));
196+
let connection = ConnectionBuilder::open_file(&db_path)
197+
.with_options(&[ConnectionOptions::EnableWriteAheadLog])
198+
.build()
199+
.unwrap();
200+
prepare_db_for_vacuum(&connection);
201+
202+
let db_initial_size = file_size(&db_path);
203+
assert!(db_initial_size > 0);
204+
205+
SqliteCleaner::new(&connection)
206+
.with_tasks(&[SqliteCleaningTask::Vacuum])
207+
.run()
208+
.unwrap();
209+
210+
let db_after_vacuum_size = file_size(&db_path);
211+
212+
assert!(
213+
db_initial_size > db_after_vacuum_size,
214+
"db size should have decreased (vacuum enabled)"
215+
);
216+
assert!(
217+
file_size(&db_wal_path) > 0,
218+
"db wal file should not have been truncated (truncate disabled)"
219+
);
220+
}
221+
222+
#[test]
223+
fn test_truncate_wal() {
224+
let db_dir = TempDir::create("sqlite_cleaner", "test_truncate_wal");
225+
let (db_path, db_wal_path) = (db_dir.join("test.db"), db_dir.join("test.db-wal"));
226+
let connection = ConnectionBuilder::open_file(&db_path)
227+
.with_options(&[ConnectionOptions::EnableWriteAheadLog])
228+
.build()
229+
.unwrap();
230+
231+
// Make "neutral" changes to the db, this will fill the WAL files with some data
232+
// but won't change the db size after cleaning up.
233+
add_test_table(&connection);
234+
fill_test_table(&connection, 0..10_000);
235+
delete_test_rows(&connection, 0..10_000);
236+
237+
assert!(file_size(&db_wal_path) > 0);
238+
239+
SqliteCleaner::new(&connection)
240+
.with_tasks(&[SqliteCleaningTask::WalCheckpointTruncate])
241+
.run()
242+
.unwrap();
243+
244+
assert_eq!(
245+
file_size(&db_wal_path),
246+
0,
247+
"db wal file should have been truncated"
248+
);
249+
}
250+
}

internal/mithril-persistence/src/sqlite/connection_builder.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ use std::ops::Not;
22
use std::path::{Path, PathBuf};
33

44
use anyhow::Context;
5-
use slog::{info, Logger};
5+
use slog::Logger;
66
use sqlite::{Connection, ConnectionThreadSafe};
77

88
use mithril_common::StdResult;
99

1010
use crate::database::{ApplicationNodeType, DatabaseVersionChecker, SqlMigration};
11-
use crate::sqlite::vacuum_database;
1211

1312
/// Builder of SQLite connection
1413
pub struct ConnectionBuilder {
@@ -32,11 +31,6 @@ pub enum ConnectionOptions {
3231
///
3332
/// This option take priority over [ConnectionOptions::EnableForeignKeys] if both are enabled.
3433
ForceDisableForeignKeys,
35-
36-
/// Run a VACUUM operation on the database after the connection is opened
37-
///
38-
/// ⚠ This operation can be very slow on large databases ⚠
39-
Vacuum,
4034
}
4135

4236
impl ConnectionBuilder {
@@ -92,23 +86,6 @@ impl ConnectionBuilder {
9286
)
9387
})?;
9488

95-
if self.options.contains(&ConnectionOptions::Vacuum) {
96-
info!(
97-
self.logger,
98-
"Vacuuming SQLite database, this may take a while...";
99-
"database" => self.connection_path.display()
100-
);
101-
102-
vacuum_database(&connection)
103-
.with_context(|| "SQLite initialization: database VACUUM error")?;
104-
105-
info!(
106-
self.logger,
107-
"SQLite database vacuumed successfully";
108-
"database" => self.connection_path.display()
109-
);
110-
}
111-
11289
if self
11390
.options
11491
.contains(&ConnectionOptions::EnableWriteAheadLog)

0 commit comments

Comments
 (0)