Skip to main content

reth_db/implementation/mdbx/
tx.rs

1//! Transaction wrapper for libmdbx-sys.
2
3use super::{cursor::Cursor, utils::*, TableDbis};
4use crate::{
5    metrics::{DatabaseEnvMetrics, Operation, TransactionMode, TransactionOutcome},
6    DatabaseError, Tables,
7};
8use reth_db_api::{
9    table::{Compress, DupSort, Encode, IntoVec, Table, TableImporter},
10    transaction::{DbTx, DbTxMut},
11};
12use reth_libmdbx::{ffi::MDBX_dbi, CommitLatency, Transaction, TransactionKind, WriteFlags, RW};
13use reth_primitives_traits::FastInstant as Instant;
14use reth_storage_errors::db::{DatabaseWriteError, DatabaseWriteOperation};
15use std::{
16    backtrace::Backtrace,
17    marker::PhantomData,
18    sync::{
19        atomic::{AtomicBool, Ordering},
20        Arc,
21    },
22    time::Duration,
23};
24use tracing::{debug, instrument, trace, warn};
25
26/// Duration after which we emit the log about long-lived database transactions.
27const LONG_TRANSACTION_DURATION: Duration = Duration::from_secs(60);
28
29/// Wrapper for the libmdbx transaction.
30#[derive(Debug)]
31pub struct Tx<K: TransactionKind> {
32    /// Libmdbx-sys transaction.
33    inner: Transaction<K>,
34
35    /// Cached MDBX DBIs for reuse.
36    dbis: Arc<TableDbis>,
37
38    /// Handler for metrics with its own [Drop] implementation for cases when the transaction isn't
39    /// closed by [`Tx::commit`] or [`Tx::abort`], but we still need to report it in the metrics.
40    ///
41    /// If [Some], then metrics are reported.
42    metrics_handler: Option<MetricsHandler<K>>,
43    /// Persistent read-ahead setting for gets and newly created cursors.
44    #[cfg(target_os = "linux")]
45    prefetch: AtomicBool,
46}
47
48impl<K: TransactionKind> Tx<K> {
49    /// Creates new `Tx` object with a `RO` or `RW` transaction and optionally enables metrics.
50    #[inline]
51    #[track_caller]
52    pub(crate) fn new(
53        inner: Transaction<K>,
54        dbis: Arc<TableDbis>,
55        env_metrics: Option<Arc<DatabaseEnvMetrics>>,
56    ) -> reth_libmdbx::Result<Self> {
57        let metrics_handler = env_metrics
58            .map(|env_metrics| {
59                let handler = MetricsHandler::<K>::new(inner.id()?, env_metrics);
60                handler.env_metrics.record_opened_transaction(handler.transaction_mode());
61                handler.log_transaction_opened();
62                Ok(handler)
63            })
64            .transpose()?;
65        Ok(Self {
66            inner,
67            dbis,
68            metrics_handler,
69            #[cfg(target_os = "linux")]
70            prefetch: AtomicBool::new(false),
71        })
72    }
73
74    /// Returns a reference to the inner libmdbx transaction.
75    pub const fn inner(&self) -> &Transaction<K> {
76        &self.inner
77    }
78
79    /// Gets this transaction ID.
80    pub fn id(&self) -> reth_libmdbx::Result<u64> {
81        self.metrics_handler.as_ref().map_or_else(|| self.inner.id(), |handler| Ok(handler.txn_id))
82    }
83
84    /// Gets a table database handle by name if it exists, otherwise, check the
85    /// database, opening the DB if it exists.
86    pub fn get_dbi_raw(&self, name: &str) -> Result<MDBX_dbi, DatabaseError> {
87        self.get_table_dbi(name.parse::<Tables>().ok(), name)
88    }
89
90    /// Gets a table database handle by name if it exists, otherwise, check the
91    /// database, opening the DB if it exists.
92    pub fn get_dbi<T: Table>(&self) -> Result<MDBX_dbi, DatabaseError> {
93        self.get_table_dbi(T::TABLE, T::NAME)
94    }
95
96    /// Gets the tracked database handle of a table, which is looked up by `table` if it is a
97    /// [`Tables`] entry and by `name` otherwise, opening the DB by name if it is not tracked.
98    fn get_table_dbi(&self, table: Option<Tables>, name: &str) -> Result<MDBX_dbi, DatabaseError> {
99        if let Some(dbi) = self.dbis.get(table, name) {
100            Ok(dbi)
101        } else {
102            self.inner
103                .open_db(Some(name))
104                .map(|db| db.dbi())
105                .map_err(|e| DatabaseError::Open(e.into()))
106        }
107    }
108
109    /// Create db Cursor
110    pub fn new_cursor<T: Table>(&self) -> Result<Cursor<K, T>, DatabaseError> {
111        let inner = self
112            .inner
113            .cursor_with_dbi(self.get_dbi::<T>()?)
114            .map_err(|e| DatabaseError::InitCursor(e.into()))?;
115
116        Ok(Cursor::new_with_metrics(
117            inner,
118            self.metrics_handler
119                .as_ref()
120                .zip(T::TABLE)
121                .map(|(h, table)| h.env_metrics.table_operation_metrics(table)),
122            #[cfg(target_os = "linux")]
123            self.prefetch.load(Ordering::Relaxed),
124        ))
125    }
126
127    /// If `self.metrics_handler == Some(_)`, measure the time it takes to execute the closure and
128    /// record a metric with the provided transaction outcome.
129    ///
130    /// Otherwise, just execute the closure.
131    fn execute_with_close_transaction_metric<R>(
132        mut self,
133        outcome: TransactionOutcome,
134        f: impl FnOnce(Self) -> (R, Option<CommitLatency>),
135    ) -> R {
136        let run = |tx| {
137            let start = Instant::now();
138            let (result, commit_latency) = f(tx);
139            let total_duration = start.elapsed();
140
141            if outcome.is_commit() {
142                debug!(
143                    target: "storage::db::mdbx",
144                    ?total_duration,
145                    ?commit_latency,
146                    is_read_only = K::IS_READ_ONLY,
147                    "Commit"
148                );
149            }
150
151            (result, commit_latency, total_duration)
152        };
153
154        if let Some(mut metrics_handler) = self.metrics_handler.take() {
155            metrics_handler.close_recorded = true;
156            metrics_handler.log_backtrace_on_long_read_transaction();
157
158            let (result, commit_latency, close_duration) = run(self);
159            let open_duration = metrics_handler.start.elapsed();
160            metrics_handler.env_metrics.record_closed_transaction(
161                metrics_handler.transaction_mode(),
162                outcome,
163                open_duration,
164                Some(close_duration),
165                commit_latency,
166            );
167
168            result
169        } else {
170            run(self).0
171        }
172    }
173
174    /// If `self.metrics_handler == Some(_)`, measure the time it takes to execute the closure and
175    /// record a metric with the provided operation.
176    ///
177    /// Otherwise, just execute the closure.
178    fn execute_with_operation_metric<T: Table, R>(
179        &self,
180        operation: Operation,
181        value_size: Option<usize>,
182        f: impl FnOnce(&Transaction<K>) -> R,
183    ) -> R {
184        if let Some(metrics_handler) = &self.metrics_handler {
185            metrics_handler.log_backtrace_on_long_read_transaction();
186            metrics_handler
187                .env_metrics
188                .record_operation(T::TABLE, operation, value_size, || f(&self.inner))
189        } else {
190            f(&self.inner)
191        }
192    }
193}
194
195#[derive(Debug)]
196struct MetricsHandler<K: TransactionKind> {
197    /// Cached internal transaction ID provided by libmdbx.
198    txn_id: u64,
199    /// The time when transaction has started.
200    ///
201    /// This is a TSC-backed [`reth_primitives_traits::FastInstant`] rather than
202    /// [`std::time::Instant`] because
203    /// [`MetricsHandler::log_backtrace_on_long_read_transaction`] reads it on every database
204    /// operation.
205    start: Instant,
206    /// The time after which we emit the log about long-lived database transactions.
207    ///
208    /// Stored as a deadline so that the check on every database operation only compares two
209    /// timestamps instead of computing the open duration.
210    long_transaction_deadline: Instant,
211    /// If `true`, the metric about transaction closing has already been recorded and we don't need
212    /// to do anything on [`Drop::drop`].
213    close_recorded: bool,
214    /// If `true`, the backtrace of transaction will be recorded and logged.
215    /// See [`MetricsHandler::log_backtrace_on_long_read_transaction`].
216    record_backtrace: bool,
217    /// If `true`, the backtrace of transaction has already been recorded and logged.
218    /// See [`MetricsHandler::log_backtrace_on_long_read_transaction`].
219    backtrace_recorded: AtomicBool,
220    /// Shared database environment metrics.
221    env_metrics: Arc<DatabaseEnvMetrics>,
222    /// Backtrace of the location where the transaction has been opened. Reported only with debug
223    /// assertions outside DST, because capturing every transaction opening is expensive.
224    #[cfg(all(debug_assertions, not(feature = "fuzz")))]
225    open_backtrace: Backtrace,
226    _marker: PhantomData<K>,
227}
228
229impl<K: TransactionKind> MetricsHandler<K> {
230    fn new(txn_id: u64, env_metrics: Arc<DatabaseEnvMetrics>) -> Self {
231        let start = Instant::now();
232        Self {
233            txn_id,
234            start,
235            long_transaction_deadline: start + LONG_TRANSACTION_DURATION,
236            close_recorded: false,
237            record_backtrace: true,
238            backtrace_recorded: AtomicBool::new(false),
239            #[cfg(all(debug_assertions, not(feature = "fuzz")))]
240            open_backtrace: Backtrace::force_capture(),
241            env_metrics,
242            _marker: PhantomData,
243        }
244    }
245
246    const fn transaction_mode(&self) -> TransactionMode {
247        if K::IS_READ_ONLY {
248            TransactionMode::ReadOnly
249        } else {
250            TransactionMode::ReadWrite
251        }
252    }
253
254    /// Logs the caller location and ID of the transaction that was opened.
255    #[track_caller]
256    fn log_transaction_opened(&self) {
257        trace!(
258            target: "storage::db::mdbx",
259            caller = %core::panic::Location::caller(),
260            id = %self.txn_id,
261            mode = %self.transaction_mode().as_str(),
262            "Transaction opened",
263        );
264    }
265
266    /// Logs the backtrace of current call if the duration that the read transaction has been open
267    /// is more than [`LONG_TRANSACTION_DURATION`] and `record_backtrace == true`.
268    /// The backtrace is recorded and logged just once, guaranteed by `backtrace_recorded` atomic.
269    ///
270    /// NOTE: Backtrace is recorded using [`Backtrace::force_capture`], so `RUST_BACKTRACE` env var
271    /// is not needed.
272    fn log_backtrace_on_long_read_transaction(&self) {
273        if self.record_backtrace &&
274            !self.backtrace_recorded.load(Ordering::Relaxed) &&
275            self.transaction_mode().is_read_only()
276        {
277            let now = Instant::now();
278            if now >= self.long_transaction_deadline {
279                let open_duration = now - self.start;
280                self.backtrace_recorded.store(true, Ordering::Relaxed);
281                #[cfg(all(debug_assertions, not(feature = "fuzz")))]
282                let open_backtrace = format_args!("{}", self.open_backtrace);
283                #[cfg(not(all(debug_assertions, not(feature = "fuzz"))))]
284                let open_backtrace = tracing::field::Empty;
285                warn!(
286                    target: "storage::db::mdbx",
287                    ?open_duration,
288                    id=%self.txn_id,
289                    backtrace=%Backtrace::force_capture(),
290                    open_backtrace,
291                    "A database read transaction has been open for too long"
292                );
293            }
294        }
295    }
296}
297
298impl<K: TransactionKind> Drop for MetricsHandler<K> {
299    fn drop(&mut self) {
300        if !self.close_recorded {
301            self.log_backtrace_on_long_read_transaction();
302            self.env_metrics.record_closed_transaction(
303                self.transaction_mode(),
304                TransactionOutcome::Drop,
305                self.start.elapsed(),
306                None,
307                None,
308            );
309        }
310    }
311}
312
313impl TableImporter for Tx<RW> {}
314
315impl<K: TransactionKind> DbTx for Tx<K> {
316    type Cursor<T: Table> = Cursor<K, T>;
317    type DupCursor<T: DupSort> = Cursor<K, T>;
318
319    fn get<T: Table>(&self, key: T::Key) -> Result<Option<<T as Table>::Value>, DatabaseError> {
320        self.get_by_encoded_key::<T>(&key.encode())
321    }
322
323    fn get_by_encoded_key<T: Table>(
324        &self,
325        key: &<T::Key as Encode>::Encoded,
326    ) -> Result<Option<T::Value>, DatabaseError> {
327        self.execute_with_operation_metric::<T, _>(Operation::Get, None, |tx| {
328            let dbi = self.get_dbi::<T>()?;
329            #[cfg(target_os = "linux")]
330            let value = if self.prefetch.load(Ordering::Relaxed) {
331                tx.get::<super::value_prefetch::PrefetchValue<'_>>(dbi, key.as_ref())
332                    .map(|value| value.map(|value| value.0))
333            } else {
334                tx.get(dbi, key.as_ref())
335            };
336            #[cfg(not(target_os = "linux"))]
337            let value = tx.get(dbi, key.as_ref());
338            value.map_err(|e| DatabaseError::Read(e.into()))?.map(decode_one::<T>).transpose()
339        })
340    }
341
342    #[instrument(name = "Tx::commit", level = "debug", target = "providers::db", skip_all)]
343    fn commit(self) -> Result<(), DatabaseError> {
344        self.execute_with_close_transaction_metric(TransactionOutcome::Commit, |this| {
345            match this.inner.commit().map_err(|e| DatabaseError::Commit(e.into())) {
346                Ok(latency) => (Ok(()), Some(latency)),
347                Err(e) => (Err(e), None),
348            }
349        })
350    }
351
352    fn abort(self) {
353        self.execute_with_close_transaction_metric(TransactionOutcome::Abort, |this| {
354            (drop(this.inner), None)
355        })
356    }
357
358    // Iterate over read only values in database.
359    fn cursor_read<T: Table>(&self) -> Result<Self::Cursor<T>, DatabaseError> {
360        self.new_cursor()
361    }
362
363    /// Iterate over read only values in database.
364    fn cursor_dup_read<T: DupSort>(&self) -> Result<Self::DupCursor<T>, DatabaseError> {
365        self.new_cursor()
366    }
367
368    /// Returns number of entries in the table using cheap DB stats invocation.
369    fn entries<T: Table>(&self) -> Result<usize, DatabaseError> {
370        Ok(self
371            .inner
372            .db_stat_with_dbi(self.get_dbi::<T>()?)
373            .map_err(|e| DatabaseError::Stats(e.into()))?
374            .entries())
375    }
376
377    /// Disables long-lived read transaction safety guarantees, such as backtrace recording and
378    /// timeout.
379    fn disable_long_read_transaction_safety(&mut self) {
380        if let Some(metrics_handler) = self.metrics_handler.as_mut() {
381            metrics_handler.record_backtrace = false;
382        }
383
384        self.inner.disable_timeout();
385    }
386
387    #[cfg(target_os = "linux")]
388    fn prefetch(&self, enabled: bool) -> &Self {
389        self.prefetch.store(enabled, Ordering::Relaxed);
390        self
391    }
392}
393
394#[derive(Clone, Copy)]
395enum PutKind {
396    /// Default kind that inserts a new key-value or overwrites an existed key.
397    Upsert,
398    /// Append the key-value to the end of the table -- fast path when the new
399    /// key is the highest so far, like the latest block number.
400    Append,
401}
402
403impl PutKind {
404    const fn into_operation_and_flags(self) -> (Operation, DatabaseWriteOperation, WriteFlags) {
405        match self {
406            Self::Upsert => {
407                (Operation::PutUpsert, DatabaseWriteOperation::PutUpsert, WriteFlags::UPSERT)
408            }
409            Self::Append => {
410                (Operation::PutAppend, DatabaseWriteOperation::PutAppend, WriteFlags::APPEND)
411            }
412        }
413    }
414}
415
416impl Tx<RW> {
417    /// The inner implementation mapping to `mdbx_put` that supports different
418    /// put kinds like upserting and appending.
419    fn put<T: Table>(
420        &self,
421        kind: PutKind,
422        key: T::Key,
423        value: T::Value,
424    ) -> Result<(), DatabaseError> {
425        let key = key.encode();
426        let value = value.compress();
427        let (operation, write_operation, flags) = kind.into_operation_and_flags();
428        self.execute_with_operation_metric::<T, _>(operation, Some(value.as_ref().len()), |tx| {
429            tx.put(self.get_dbi::<T>()?, key.as_ref(), value, flags).map_err(|e| {
430                DatabaseWriteError {
431                    info: e.into(),
432                    operation: write_operation,
433                    table_name: T::NAME,
434                    key: key.into_vec(),
435                }
436                .into()
437            })
438        })
439    }
440}
441
442impl DbTxMut for Tx<RW> {
443    type CursorMut<T: Table> = Cursor<RW, T>;
444    type DupCursorMut<T: DupSort> = Cursor<RW, T>;
445
446    fn put<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
447        self.put::<T>(PutKind::Upsert, key, value)
448    }
449
450    fn append<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
451        self.put::<T>(PutKind::Append, key, value)
452    }
453
454    fn delete<T: Table>(
455        &self,
456        key: T::Key,
457        value: Option<T::Value>,
458    ) -> Result<bool, DatabaseError> {
459        let mut data = None;
460
461        let value = value.map(Compress::compress);
462        if let Some(value) = &value {
463            data = Some(value.as_ref());
464        };
465
466        self.execute_with_operation_metric::<T, _>(Operation::Delete, None, |tx| {
467            tx.del(self.get_dbi::<T>()?, key.encode(), data)
468                .map_err(|e| DatabaseError::Delete(e.into()))
469        })
470    }
471
472    fn clear<T: Table>(&self) -> Result<(), DatabaseError> {
473        self.inner.clear_db(self.get_dbi::<T>()?).map_err(|e| DatabaseError::Delete(e.into()))?;
474
475        Ok(())
476    }
477
478    fn cursor_write<T: Table>(&self) -> Result<Self::CursorMut<T>, DatabaseError> {
479        self.new_cursor()
480    }
481
482    fn cursor_dup_write<T: DupSort>(&self) -> Result<Self::DupCursorMut<T>, DatabaseError> {
483        self.new_cursor()
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use crate::{mdbx::DatabaseArguments, tables, DatabaseEnv, DatabaseEnvKind};
490    use reth_db_api::{database::Database, models::ClientVersion, transaction::DbTx};
491    use reth_libmdbx::MaxReadTransactionDuration;
492    use reth_storage_errors::db::DatabaseError;
493    use std::{sync::atomic::Ordering, thread::sleep, time::Duration};
494    use tempfile::tempdir;
495
496    #[test]
497    fn long_read_transaction_safety_disabled() {
498        const MAX_DURATION: Duration = Duration::from_secs(1);
499
500        let dir = tempdir().unwrap();
501        let args = DatabaseArguments::new(ClientVersion::default())
502            .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Set(
503                MAX_DURATION,
504            )));
505        let db = DatabaseEnv::open(dir.path(), DatabaseEnvKind::RW, args)
506            .unwrap()
507            .with_metrics()
508            .with_disable_long_read_transaction_safety();
509
510        let mut tx = db.tx().unwrap();
511        let metrics_handler = tx.metrics_handler.as_mut().unwrap();
512        metrics_handler.long_transaction_deadline = metrics_handler.start + MAX_DURATION;
513        // Give the `TxnManager` some time to time out the transaction.
514        sleep(MAX_DURATION + Duration::from_millis(100));
515
516        // Transaction has not timed out.
517        assert!(matches!(
518            tx.get::<tables::Transactions>(0).unwrap_err(),
519            DatabaseError::Open(err) if err == reth_libmdbx::Error::NotFound.into()));
520        // Backtrace is not recorded.
521        assert!(!tx.metrics_handler.unwrap().backtrace_recorded.load(Ordering::Relaxed));
522    }
523
524    #[test]
525    fn long_read_transaction_safety_enabled() {
526        const MAX_DURATION: Duration = Duration::from_secs(1);
527
528        let dir = tempdir().unwrap();
529        let args = DatabaseArguments::new(ClientVersion::default())
530            .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Set(
531                MAX_DURATION,
532            )));
533        let db = DatabaseEnv::open(dir.path(), DatabaseEnvKind::RW, args).unwrap().with_metrics();
534
535        let mut tx = db.tx().unwrap();
536        let metrics_handler = tx.metrics_handler.as_mut().unwrap();
537        metrics_handler.long_transaction_deadline = metrics_handler.start + MAX_DURATION;
538        // Give the `TxnManager` some time to time out the transaction.
539        sleep(MAX_DURATION + Duration::from_millis(100));
540
541        // Transaction has timed out.
542        assert!(matches!(
543            tx.get::<tables::Transactions>(0).unwrap_err(),
544            DatabaseError::Open(err) if err == reth_libmdbx::Error::ReadTransactionTimeout.into()));
545        // Backtrace is recorded.
546        assert!(tx.metrics_handler.unwrap().backtrace_recorded.load(Ordering::Relaxed));
547    }
548}