mithril_signer/services/cardano_transactions/importer/
importer_with_vacuum.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use slog::{debug, Logger};
5
6use mithril_common::entities::BlockNumber;
7use mithril_common::logging::LoggerExtensions;
8use mithril_common::signable_builder::TransactionsImporter;
9use mithril_common::StdResult;
10use mithril_persistence::sqlite::{SqliteCleaner, SqliteCleaningTask, SqliteConnectionPool};
11
12/// A decorator of [TransactionsImporter] that vacuums the database after running the import.
13pub struct TransactionsImporterWithVacuum {
14    connection_pool: Arc<SqliteConnectionPool>,
15    wrapped_importer: Arc<dyn TransactionsImporter>,
16    logger: Logger,
17}
18
19impl TransactionsImporterWithVacuum {
20    /// Create a new instance of [TransactionsImporterWithVacuum].
21    pub fn new(
22        connection_pool: Arc<SqliteConnectionPool>,
23        wrapped_importer: Arc<dyn TransactionsImporter>,
24        logger: Logger,
25    ) -> Self {
26        Self {
27            connection_pool,
28            wrapped_importer,
29            logger: logger.new_with_component_name::<Self>(),
30        }
31    }
32}
33
34#[async_trait]
35impl TransactionsImporter for TransactionsImporterWithVacuum {
36    async fn import(&self, up_to_beacon: BlockNumber) -> StdResult<()> {
37        self.wrapped_importer.import(up_to_beacon).await?;
38
39        debug!(
40            self.logger,
41            "Transaction Import finished - Vacuuming database to reclaim disk space"
42        );
43        let connection = self.connection_pool.connection()?;
44
45        SqliteCleaner::new(&connection)
46            .with_tasks(&[SqliteCleaningTask::Vacuum])
47            .run()?;
48
49        Ok(())
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use mockall::mock;
56    use sqlite::Connection;
57
58    use mithril_common::test_utils::TempDir;
59    use mithril_persistence::sqlite::SqliteConnection;
60
61    use crate::test_tools::TestLogger;
62
63    use super::*;
64
65    mock! {
66        pub TransactionImporterImpl {}
67
68        #[async_trait]
69        impl TransactionsImporter for TransactionImporterImpl {
70            async fn import(&self, up_to_beacon: BlockNumber) -> StdResult<()>;
71        }
72    }
73
74    impl TransactionsImporterWithVacuum {
75        pub(crate) fn new_with_mock<I>(
76            connection_pool: Arc<SqliteConnectionPool>,
77            importer_mock_config: I,
78        ) -> Self
79        where
80            I: FnOnce(&mut MockTransactionImporterImpl),
81        {
82            let mut transaction_importer = MockTransactionImporterImpl::new();
83            importer_mock_config(&mut transaction_importer);
84
85            Self::new(
86                connection_pool,
87                Arc::new(transaction_importer),
88                TestLogger::stdout(),
89            )
90        }
91    }
92
93    /// Create a table, insert a lot of data and drop the table to make the database size grow.
94    fn mangle_db(connection: &SqliteConnection) {
95        connection
96            .execute("CREATE TABLE test (id INTEGER PRIMARY KEY, text TEXT);")
97            .unwrap();
98        connection
99            .execute(format!(
100                "INSERT INTO test (id, text) VALUES {}",
101                (0..10_000)
102                    .map(|i| format!("({}, 'some text to fill the db')", i))
103                    .collect::<Vec<String>>()
104                    .join(", ")
105            ))
106            .unwrap();
107        connection.execute("DROP TABLE test").unwrap();
108    }
109
110    #[tokio::test]
111    async fn test_database_size_shrink_after_import() {
112        let db_path = TempDir::create("mithril-persistence", "test_vacuum").join("test.db");
113        let connection = Connection::open_thread_safe(&db_path).unwrap();
114        // make the database size grow
115        mangle_db(&connection);
116
117        let importer = TransactionsImporterWithVacuum::new_with_mock(
118            Arc::new(SqliteConnectionPool::build_from_connection(connection)),
119            |mock| {
120                mock.expect_import().once().returning(|_| Ok(()));
121            },
122        );
123
124        let initial_size = db_path.metadata().unwrap().len();
125
126        importer
127            .import(BlockNumber(100))
128            .await
129            .expect("Import should not fail");
130
131        let after_import_size = db_path.metadata().unwrap().len();
132
133        assert!(
134            initial_size > after_import_size,
135            "Database size did not shrink after import: \
136            initial_size: {initial_size} -> after_import_size: {after_import_size}"
137        );
138    }
139}