reth_libmdbx/
transaction.rs

1use crate::{
2    database::Database,
3    environment::Environment,
4    error::{mdbx_result, Result},
5    flags::{DatabaseFlags, WriteFlags},
6    txn_manager::{TxnManagerMessage, TxnPtr},
7    Cursor, Error, Stat, TableObject,
8};
9use ffi::{MDBX_txn_flags_t, MDBX_TXN_RDONLY, MDBX_TXN_READWRITE};
10use indexmap::IndexSet;
11use parking_lot::{Mutex, MutexGuard};
12use std::{
13    ffi::{c_uint, c_void},
14    fmt::{self, Debug},
15    mem::size_of,
16    ptr, slice,
17    sync::{atomic::AtomicBool, mpsc::sync_channel, Arc},
18    time::Duration,
19};
20
21#[cfg(feature = "read-tx-timeouts")]
22use ffi::mdbx_txn_renew;
23
24mod private {
25    use super::*;
26
27    pub trait Sealed {}
28
29    impl Sealed for RO {}
30    impl Sealed for RW {}
31}
32
33pub trait TransactionKind: private::Sealed + Send + Sync + Debug + 'static {
34    #[doc(hidden)]
35    const OPEN_FLAGS: MDBX_txn_flags_t;
36
37    /// Convenience flag for distinguishing between read-only and read-write transactions.
38    #[doc(hidden)]
39    const IS_READ_ONLY: bool;
40}
41
42#[derive(Debug)]
43#[non_exhaustive]
44pub struct RO;
45
46#[derive(Debug)]
47#[non_exhaustive]
48pub struct RW;
49
50impl TransactionKind for RO {
51    const OPEN_FLAGS: MDBX_txn_flags_t = MDBX_TXN_RDONLY;
52    const IS_READ_ONLY: bool = true;
53}
54impl TransactionKind for RW {
55    const OPEN_FLAGS: MDBX_txn_flags_t = MDBX_TXN_READWRITE;
56    const IS_READ_ONLY: bool = false;
57}
58
59/// An MDBX transaction.
60///
61/// All database operations require a transaction.
62pub struct Transaction<K>
63where
64    K: TransactionKind,
65{
66    inner: Arc<TransactionInner<K>>,
67}
68
69impl<K> Transaction<K>
70where
71    K: TransactionKind,
72{
73    pub(crate) fn new(env: Environment) -> Result<Self> {
74        let mut txn: *mut ffi::MDBX_txn = ptr::null_mut();
75        unsafe {
76            mdbx_result(ffi::mdbx_txn_begin_ex(
77                env.env_ptr(),
78                ptr::null_mut(),
79                K::OPEN_FLAGS,
80                &mut txn,
81                ptr::null_mut(),
82            ))?;
83            Ok(Self::new_from_ptr(env, txn))
84        }
85    }
86
87    pub(crate) fn new_from_ptr(env: Environment, txn_ptr: *mut ffi::MDBX_txn) -> Self {
88        let txn = TransactionPtr::new(txn_ptr);
89
90        #[cfg(feature = "read-tx-timeouts")]
91        if K::IS_READ_ONLY {
92            env.txn_manager().add_active_read_transaction(txn_ptr, txn.clone())
93        }
94
95        let inner = TransactionInner {
96            txn,
97            primed_dbis: Mutex::new(IndexSet::new()),
98            committed: AtomicBool::new(false),
99            env,
100            _marker: Default::default(),
101        };
102
103        Self { inner: Arc::new(inner) }
104    }
105
106    /// Executes the given closure once the lock on the transaction is acquired.
107    ///
108    /// The caller **must** ensure that the pointer is not used after the
109    /// lifetime of the transaction.
110    #[inline]
111    pub fn txn_execute<F, T>(&self, f: F) -> Result<T>
112    where
113        F: FnOnce(*mut ffi::MDBX_txn) -> T,
114    {
115        self.inner.txn_execute(f)
116    }
117
118    /// Executes the given closure once the lock on the transaction is acquired. If the transaction
119    /// is timed out, it will be renewed first.
120    ///
121    /// Returns the result of the closure or an error if the transaction renewal fails.
122    #[inline]
123    pub(crate) fn txn_execute_renew_on_timeout<F, T>(&self, f: F) -> Result<T>
124    where
125        F: FnOnce(*mut ffi::MDBX_txn) -> T,
126    {
127        self.inner.txn_execute_renew_on_timeout(f)
128    }
129
130    /// Returns a copy of the raw pointer to the underlying MDBX transaction.
131    #[doc(hidden)]
132    #[cfg(test)]
133    pub fn txn(&self) -> *mut ffi::MDBX_txn {
134        self.inner.txn.txn
135    }
136
137    /// Returns a raw pointer to the MDBX environment.
138    pub fn env(&self) -> &Environment {
139        &self.inner.env
140    }
141
142    /// Returns the transaction id.
143    pub fn id(&self) -> Result<u64> {
144        self.txn_execute(|txn| unsafe { ffi::mdbx_txn_id(txn) })
145    }
146
147    /// Gets an item from a database.
148    ///
149    /// This function retrieves the data associated with the given key in the
150    /// database. If the database supports duplicate keys
151    /// ([`DatabaseFlags::DUP_SORT`]) then the first data item for the key will be
152    /// returned. Retrieval of other items requires the use of
153    /// [Cursor]. If the item is not in the database, then
154    /// [None] will be returned.
155    pub fn get<Key>(&self, dbi: ffi::MDBX_dbi, key: &[u8]) -> Result<Option<Key>>
156    where
157        Key: TableObject,
158    {
159        let key_val: ffi::MDBX_val =
160            ffi::MDBX_val { iov_len: key.len(), iov_base: key.as_ptr() as *mut c_void };
161        let mut data_val: ffi::MDBX_val = ffi::MDBX_val { iov_len: 0, iov_base: ptr::null_mut() };
162
163        self.txn_execute(|txn| unsafe {
164            match ffi::mdbx_get(txn, dbi, &key_val, &mut data_val) {
165                ffi::MDBX_SUCCESS => Key::decode_val::<K>(txn, data_val).map(Some),
166                ffi::MDBX_NOTFOUND => Ok(None),
167                err_code => Err(Error::from_err_code(err_code)),
168            }
169        })?
170    }
171
172    /// Commits the transaction.
173    ///
174    /// Any pending operations will be saved.
175    pub fn commit(self) -> Result<(bool, CommitLatency)> {
176        self.commit_and_rebind_open_dbs().map(|v| (v.0, v.1))
177    }
178
179    pub fn prime_for_permaopen(&self, db: Database) {
180        self.inner.primed_dbis.lock().insert(db.dbi());
181    }
182
183    /// Commits the transaction and returns table handles permanently open until dropped.
184    pub fn commit_and_rebind_open_dbs(self) -> Result<(bool, CommitLatency, Vec<Database>)> {
185        let result = {
186            let result = self.txn_execute(|txn| {
187                if K::IS_READ_ONLY {
188                    #[cfg(feature = "read-tx-timeouts")]
189                    self.env().txn_manager().remove_active_read_transaction(txn);
190
191                    let mut latency = CommitLatency::new();
192                    mdbx_result(unsafe {
193                        ffi::mdbx_txn_commit_ex(txn, latency.mdb_commit_latency())
194                    })
195                    .map(|v| (v, latency))
196                } else {
197                    let (sender, rx) = sync_channel(0);
198                    self.env()
199                        .txn_manager()
200                        .send_message(TxnManagerMessage::Commit { tx: TxnPtr(txn), sender });
201                    rx.recv().unwrap()
202                }
203            })?;
204
205            self.inner.set_committed();
206            result
207        };
208        result.map(|(v, latency)| {
209            (
210                v,
211                latency,
212                self.inner
213                    .primed_dbis
214                    .lock()
215                    .iter()
216                    .map(|&dbi| Database::new_from_ptr(dbi, self.env().clone()))
217                    .collect(),
218            )
219        })
220    }
221
222    /// Opens a handle to an MDBX database.
223    ///
224    /// If `name` is [None], then the returned handle will be for the default database.
225    ///
226    /// If `name` is not [None], then the returned handle will be for a named database. In this
227    /// case the environment must be configured to allow named databases through
228    /// [`EnvironmentBuilder::set_max_dbs()`](crate::EnvironmentBuilder::set_max_dbs).
229    ///
230    /// The returned database handle may be shared among any transaction in the environment.
231    ///
232    /// The database name may not contain the null character.
233    pub fn open_db(&self, name: Option<&str>) -> Result<Database> {
234        Database::new(self, name, 0)
235    }
236
237    /// Gets the option flags for the given database in the transaction.
238    pub fn db_flags(&self, db: &Database) -> Result<DatabaseFlags> {
239        let mut flags: c_uint = 0;
240        unsafe {
241            self.txn_execute(|txn| {
242                mdbx_result(ffi::mdbx_dbi_flags_ex(txn, db.dbi(), &mut flags, ptr::null_mut()))
243            })??;
244        }
245
246        // The types are not the same on Windows. Great!
247        #[cfg_attr(not(windows), allow(clippy::useless_conversion))]
248        Ok(DatabaseFlags::from_bits_truncate(flags.try_into().unwrap()))
249    }
250
251    /// Retrieves database statistics.
252    pub fn db_stat(&self, db: &Database) -> Result<Stat> {
253        self.db_stat_with_dbi(db.dbi())
254    }
255
256    /// Retrieves database statistics by the given dbi.
257    pub fn db_stat_with_dbi(&self, dbi: ffi::MDBX_dbi) -> Result<Stat> {
258        unsafe {
259            let mut stat = Stat::new();
260            self.txn_execute(|txn| {
261                mdbx_result(ffi::mdbx_dbi_stat(txn, dbi, stat.mdb_stat(), size_of::<Stat>()))
262            })??;
263            Ok(stat)
264        }
265    }
266
267    /// Open a new cursor on the given database.
268    pub fn cursor(&self, db: &Database) -> Result<Cursor<K>> {
269        Cursor::new(self.clone(), db.dbi())
270    }
271
272    /// Open a new cursor on the given dbi.
273    pub fn cursor_with_dbi(&self, dbi: ffi::MDBX_dbi) -> Result<Cursor<K>> {
274        Cursor::new(self.clone(), dbi)
275    }
276
277    /// Disables a timeout for this read transaction.
278    #[cfg(feature = "read-tx-timeouts")]
279    pub fn disable_timeout(&self) {
280        if K::IS_READ_ONLY {
281            self.env().txn_manager().remove_active_read_transaction(self.inner.txn.txn);
282        }
283    }
284}
285
286impl<K> Clone for Transaction<K>
287where
288    K: TransactionKind,
289{
290    fn clone(&self) -> Self {
291        Self { inner: Arc::clone(&self.inner) }
292    }
293}
294
295impl<K> fmt::Debug for Transaction<K>
296where
297    K: TransactionKind,
298{
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.debug_struct("RoTransaction").finish_non_exhaustive()
301    }
302}
303
304/// Internals of a transaction.
305struct TransactionInner<K>
306where
307    K: TransactionKind,
308{
309    /// The transaction pointer itself.
310    txn: TransactionPtr,
311    /// A set of database handles that are primed for permaopen.
312    primed_dbis: Mutex<IndexSet<ffi::MDBX_dbi>>,
313    /// Whether the transaction has committed.
314    committed: AtomicBool,
315    env: Environment,
316    _marker: std::marker::PhantomData<fn(K)>,
317}
318
319impl<K> TransactionInner<K>
320where
321    K: TransactionKind,
322{
323    /// Marks the transaction as committed.
324    fn set_committed(&self) {
325        self.committed.store(true, std::sync::atomic::Ordering::SeqCst);
326    }
327
328    fn has_committed(&self) -> bool {
329        self.committed.load(std::sync::atomic::Ordering::SeqCst)
330    }
331
332    #[inline]
333    fn txn_execute<F, T>(&self, f: F) -> Result<T>
334    where
335        F: FnOnce(*mut ffi::MDBX_txn) -> T,
336    {
337        self.txn.txn_execute_fail_on_timeout(f)
338    }
339
340    #[inline]
341    fn txn_execute_renew_on_timeout<F, T>(&self, f: F) -> Result<T>
342    where
343        F: FnOnce(*mut ffi::MDBX_txn) -> T,
344    {
345        self.txn.txn_execute_renew_on_timeout(f)
346    }
347}
348
349impl<K> Drop for TransactionInner<K>
350where
351    K: TransactionKind,
352{
353    fn drop(&mut self) {
354        // To be able to abort a timed out transaction, we need to renew it first.
355        // Hence the usage of `txn_execute_renew_on_timeout` here.
356        self.txn
357            .txn_execute_renew_on_timeout(|txn| {
358                if !self.has_committed() {
359                    if K::IS_READ_ONLY {
360                        #[cfg(feature = "read-tx-timeouts")]
361                        self.env.txn_manager().remove_active_read_transaction(txn);
362
363                        unsafe {
364                            ffi::mdbx_txn_abort(txn);
365                        }
366                    } else {
367                        let (sender, rx) = sync_channel(0);
368                        self.env
369                            .txn_manager()
370                            .send_message(TxnManagerMessage::Abort { tx: TxnPtr(txn), sender });
371                        rx.recv().unwrap().unwrap();
372                    }
373                }
374            })
375            .unwrap();
376    }
377}
378
379impl Transaction<RW> {
380    fn open_db_with_flags(&self, name: Option<&str>, flags: DatabaseFlags) -> Result<Database> {
381        Database::new(self, name, flags.bits())
382    }
383
384    /// Opens a handle to an MDBX database, creating the database if necessary.
385    ///
386    /// If the database is already created, the given option flags will be added to it.
387    ///
388    /// If `name` is [None], then the returned handle will be for the default database.
389    ///
390    /// If `name` is not [None], then the returned handle will be for a named database. In this
391    /// case the environment must be configured to allow named databases through
392    /// [`EnvironmentBuilder::set_max_dbs()`](crate::EnvironmentBuilder::set_max_dbs).
393    ///
394    /// This function will fail with [`Error::BadRslot`] if called by a thread with an open
395    /// transaction.
396    pub fn create_db(&self, name: Option<&str>, flags: DatabaseFlags) -> Result<Database> {
397        self.open_db_with_flags(name, flags | DatabaseFlags::CREATE)
398    }
399
400    /// Stores an item into a database.
401    ///
402    /// This function stores key/data pairs in the database. The default
403    /// behavior is to enter the new key/data pair, replacing any previously
404    /// existing key if duplicates are disallowed, or adding a duplicate data
405    /// item if duplicates are allowed ([`DatabaseFlags::DUP_SORT`]).
406    pub fn put(
407        &self,
408        dbi: ffi::MDBX_dbi,
409        key: impl AsRef<[u8]>,
410        data: impl AsRef<[u8]>,
411        flags: WriteFlags,
412    ) -> Result<()> {
413        let key = key.as_ref();
414        let data = data.as_ref();
415        let key_val: ffi::MDBX_val =
416            ffi::MDBX_val { iov_len: key.len(), iov_base: key.as_ptr() as *mut c_void };
417        let mut data_val: ffi::MDBX_val =
418            ffi::MDBX_val { iov_len: data.len(), iov_base: data.as_ptr() as *mut c_void };
419        mdbx_result(self.txn_execute(|txn| unsafe {
420            ffi::mdbx_put(txn, dbi, &key_val, &mut data_val, flags.bits())
421        })?)?;
422
423        Ok(())
424    }
425
426    /// Returns a buffer which can be used to write a value into the item at the
427    /// given key and with the given length. The buffer must be completely
428    /// filled by the caller.
429    pub fn reserve(
430        &self,
431        db: &Database,
432        key: impl AsRef<[u8]>,
433        len: usize,
434        flags: WriteFlags,
435    ) -> Result<&mut [u8]> {
436        let key = key.as_ref();
437        let key_val: ffi::MDBX_val =
438            ffi::MDBX_val { iov_len: key.len(), iov_base: key.as_ptr() as *mut c_void };
439        let mut data_val: ffi::MDBX_val =
440            ffi::MDBX_val { iov_len: len, iov_base: ptr::null_mut::<c_void>() };
441        unsafe {
442            mdbx_result(self.txn_execute(|txn| {
443                ffi::mdbx_put(
444                    txn,
445                    db.dbi(),
446                    &key_val,
447                    &mut data_val,
448                    flags.bits() | ffi::MDBX_RESERVE,
449                )
450            })?)?;
451            Ok(slice::from_raw_parts_mut(data_val.iov_base as *mut u8, data_val.iov_len))
452        }
453    }
454
455    /// Delete items from a database.
456    /// This function removes key/data pairs from the database.
457    ///
458    /// The data parameter is NOT ignored regardless the database does support sorted duplicate data
459    /// items or not. If the data parameter is [Some] only the matching data item will be
460    /// deleted. Otherwise, if data parameter is [None], any/all value(s) for specified key will
461    /// be deleted.
462    ///
463    /// Returns `true` if the key/value pair was present.
464    pub fn del(
465        &self,
466        dbi: ffi::MDBX_dbi,
467        key: impl AsRef<[u8]>,
468        data: Option<&[u8]>,
469    ) -> Result<bool> {
470        let key = key.as_ref();
471        let key_val: ffi::MDBX_val =
472            ffi::MDBX_val { iov_len: key.len(), iov_base: key.as_ptr() as *mut c_void };
473        let data_val: Option<ffi::MDBX_val> = data.map(|data| ffi::MDBX_val {
474            iov_len: data.len(),
475            iov_base: data.as_ptr() as *mut c_void,
476        });
477
478        mdbx_result({
479            self.txn_execute(|txn| {
480                if let Some(d) = data_val {
481                    unsafe { ffi::mdbx_del(txn, dbi, &key_val, &d) }
482                } else {
483                    unsafe { ffi::mdbx_del(txn, dbi, &key_val, ptr::null()) }
484                }
485            })?
486        })
487        .map(|_| true)
488        .or_else(|e| match e {
489            Error::NotFound => Ok(false),
490            other => Err(other),
491        })
492    }
493
494    /// Empties the given database. All items will be removed.
495    pub fn clear_db(&self, dbi: ffi::MDBX_dbi) -> Result<()> {
496        mdbx_result(self.txn_execute(|txn| unsafe { ffi::mdbx_drop(txn, dbi, false) })?)?;
497
498        Ok(())
499    }
500
501    /// Drops the database from the environment.
502    ///
503    /// # Safety
504    /// Caller must close ALL other [Database] and [Cursor] instances pointing to the same dbi
505    /// BEFORE calling this function.
506    pub unsafe fn drop_db(&self, db: Database) -> Result<()> {
507        mdbx_result(self.txn_execute(|txn| ffi::mdbx_drop(txn, db.dbi(), true))?)?;
508
509        Ok(())
510    }
511}
512
513impl Transaction<RO> {
514    /// Closes the database handle.
515    ///
516    /// # Safety
517    /// Caller must close ALL other [Database] and [Cursor] instances pointing to the same dbi
518    /// BEFORE calling this function.
519    pub unsafe fn close_db(&self, db: Database) -> Result<()> {
520        mdbx_result(ffi::mdbx_dbi_close(self.env().env_ptr(), db.dbi()))?;
521
522        Ok(())
523    }
524}
525
526impl Transaction<RW> {
527    /// Begins a new nested transaction inside of this transaction.
528    pub fn begin_nested_txn(&mut self) -> Result<Self> {
529        if self.inner.env.is_write_map() {
530            return Err(Error::NestedTransactionsUnsupportedWithWriteMap)
531        }
532        self.txn_execute(|txn| {
533            let (tx, rx) = sync_channel(0);
534            self.env().txn_manager().send_message(TxnManagerMessage::Begin {
535                parent: TxnPtr(txn),
536                flags: RW::OPEN_FLAGS,
537                sender: tx,
538            });
539
540            rx.recv().unwrap().map(|ptr| Self::new_from_ptr(self.env().clone(), ptr.0))
541        })?
542    }
543}
544
545/// A shareable pointer to an MDBX transaction.
546#[derive(Debug, Clone)]
547pub(crate) struct TransactionPtr {
548    txn: *mut ffi::MDBX_txn,
549    #[cfg(feature = "read-tx-timeouts")]
550    timed_out: Arc<AtomicBool>,
551    lock: Arc<Mutex<()>>,
552}
553
554impl TransactionPtr {
555    fn new(txn: *mut ffi::MDBX_txn) -> Self {
556        Self {
557            txn,
558            #[cfg(feature = "read-tx-timeouts")]
559            timed_out: Arc::new(AtomicBool::new(false)),
560            lock: Arc::new(Mutex::new(())),
561        }
562    }
563
564    /// Returns `true` if the transaction is timed out.
565    ///
566    /// When transaction is timed out via `TxnManager`, it's actually reset using
567    /// `mdbx_txn_reset`. It makes the transaction unusable (MDBX fails on any usages of such
568    /// transactions).
569    ///
570    /// Importantly, we can't rely on `MDBX_TXN_FINISHED` flag to check if the transaction is timed
571    /// out using `mdbx_txn_reset`, because MDBX uses it in other cases too.
572    #[cfg(feature = "read-tx-timeouts")]
573    fn is_timed_out(&self) -> bool {
574        self.timed_out.load(std::sync::atomic::Ordering::SeqCst)
575    }
576
577    #[cfg(feature = "read-tx-timeouts")]
578    pub(crate) fn set_timed_out(&self) {
579        self.timed_out.store(true, std::sync::atomic::Ordering::SeqCst);
580    }
581
582    /// Acquires the inner transaction lock to guarantee exclusive access to the transaction
583    /// pointer.
584    fn lock(&self) -> MutexGuard<'_, ()> {
585        if let Some(lock) = self.lock.try_lock() {
586            lock
587        } else {
588            tracing::trace!(
589                target: "libmdbx",
590                txn = %self.txn as usize,
591                backtrace = %std::backtrace::Backtrace::capture(),
592                "Transaction lock is already acquired, blocking...
593                To display the full backtrace, run with `RUST_BACKTRACE=full` env variable."
594            );
595            self.lock.lock()
596        }
597    }
598
599    /// Executes the given closure once the lock on the transaction is acquired.
600    ///
601    /// Returns the result of the closure or an error if the transaction is timed out.
602    #[inline]
603    pub(crate) fn txn_execute_fail_on_timeout<F, T>(&self, f: F) -> Result<T>
604    where
605        F: FnOnce(*mut ffi::MDBX_txn) -> T,
606    {
607        let _lck = self.lock();
608
609        // No race condition with the `TxnManager` timing out the transaction is possible here,
610        // because we're taking a lock for any actions on the transaction pointer, including a call
611        // to the `mdbx_txn_reset`.
612        #[cfg(feature = "read-tx-timeouts")]
613        if self.is_timed_out() {
614            return Err(Error::ReadTransactionTimeout)
615        }
616
617        Ok((f)(self.txn))
618    }
619
620    /// Executes the given closure once the lock on the transaction is acquired. If the transaction
621    /// is timed out, it will be renewed first.
622    ///
623    /// Returns the result of the closure or an error if the transaction renewal fails.
624    #[inline]
625    pub(crate) fn txn_execute_renew_on_timeout<F, T>(&self, f: F) -> Result<T>
626    where
627        F: FnOnce(*mut ffi::MDBX_txn) -> T,
628    {
629        let _lck = self.lock();
630
631        // To be able to do any operations on the transaction, we need to renew it first.
632        #[cfg(feature = "read-tx-timeouts")]
633        if self.is_timed_out() {
634            mdbx_result(unsafe { mdbx_txn_renew(self.txn) })?;
635        }
636
637        Ok((f)(self.txn))
638    }
639}
640
641/// Commit latencies info.
642///
643/// Contains information about latency of commit stages.
644/// Inner struct stores this info in 1/65536 of seconds units.
645#[derive(Debug)]
646#[repr(transparent)]
647pub struct CommitLatency(ffi::MDBX_commit_latency);
648
649impl CommitLatency {
650    /// Create a new `CommitLatency` with zero'd inner struct `ffi::MDBX_commit_latency`.
651    pub(crate) const fn new() -> Self {
652        unsafe { Self(std::mem::zeroed()) }
653    }
654
655    /// Returns a mut pointer to `ffi::MDBX_commit_latency`.
656    pub(crate) fn mdb_commit_latency(&mut self) -> *mut ffi::MDBX_commit_latency {
657        &mut self.0
658    }
659}
660
661impl CommitLatency {
662    /// Duration of preparation (commit child transactions, update
663    /// sub-databases records and cursors destroying).
664    #[inline]
665    pub const fn preparation(&self) -> Duration {
666        Self::time_to_duration(self.0.preparation)
667    }
668
669    /// Duration of GC update by wall clock.
670    #[inline]
671    pub const fn gc_wallclock(&self) -> Duration {
672        Self::time_to_duration(self.0.gc_wallclock)
673    }
674
675    /// Duration of internal audit if enabled.
676    #[inline]
677    pub const fn audit(&self) -> Duration {
678        Self::time_to_duration(self.0.audit)
679    }
680
681    /// Duration of writing dirty/modified data pages to a filesystem,
682    /// i.e. the summary duration of a `write()` syscalls during commit.
683    #[inline]
684    pub const fn write(&self) -> Duration {
685        Self::time_to_duration(self.0.write)
686    }
687
688    /// Duration of syncing written data to the disk/storage, i.e.
689    /// the duration of a `fdatasync()` or a `msync()` syscall during commit.
690    #[inline]
691    pub const fn sync(&self) -> Duration {
692        Self::time_to_duration(self.0.sync)
693    }
694
695    /// Duration of transaction ending (releasing resources).
696    #[inline]
697    pub const fn ending(&self) -> Duration {
698        Self::time_to_duration(self.0.ending)
699    }
700
701    /// The total duration of a commit.
702    #[inline]
703    pub const fn whole(&self) -> Duration {
704        Self::time_to_duration(self.0.whole)
705    }
706
707    /// User-mode CPU time spent on GC update.
708    #[inline]
709    pub const fn gc_cputime(&self) -> Duration {
710        Self::time_to_duration(self.0.gc_cputime)
711    }
712
713    #[inline]
714    const fn time_to_duration(time: u32) -> Duration {
715        Duration::from_nanos(time as u64 * (1_000_000_000 / 65_536))
716    }
717}
718
719// SAFETY: Access to the transaction is synchronized by the lock.
720unsafe impl Send for TransactionPtr {}
721
722// SAFETY: Access to the transaction is synchronized by the lock.
723unsafe impl Sync for TransactionPtr {}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    const fn assert_send_sync<T: Send + Sync>() {}
730
731    #[allow(dead_code)]
732    const fn test_txn_send_sync() {
733        assert_send_sync::<Transaction<RO>>();
734        assert_send_sync::<Transaction<RW>>();
735    }
736}