Skip to main content

reth_libmdbx/
environment.rs

1use crate::{
2    database::Database,
3    error::{mdbx_result, Error, Result},
4    flags::EnvironmentFlags,
5    transaction::{RO, RW},
6    txn_manager::{TxnManager, TxnManagerMessage, TxnPtr},
7    txn_pool::ReadTxnPool,
8    Mode, SyncMode, Transaction, TransactionKind,
9};
10use byteorder::{ByteOrder, NativeEndian};
11use mem::size_of;
12use std::{
13    ffi::CString,
14    fmt::{self, Debug},
15    mem,
16    ops::{Bound, RangeBounds},
17    path::Path,
18    ptr,
19    sync::{mpsc::sync_channel, Arc},
20    thread::sleep,
21    time::Duration,
22};
23use tracing::warn;
24
25/// The default maximum duration of a read transaction.
26#[cfg(feature = "read-tx-timeouts")]
27const DEFAULT_MAX_READ_TRANSACTION_DURATION: Duration = Duration::from_secs(5 * 60);
28
29/// An environment supports multiple databases, all residing in the same shared-memory map.
30///
31/// Accessing the environment is thread-safe.
32/// The environment will be closed when the last instance of this type is dropped.
33#[derive(Clone)]
34pub struct Environment {
35    inner: Arc<EnvironmentInner>,
36}
37
38impl Environment {
39    /// Creates a new builder for specifying options for opening an MDBX environment.
40    pub fn builder() -> EnvironmentBuilder {
41        EnvironmentBuilder {
42            flags: EnvironmentFlags::default(),
43            max_readers: None,
44            max_dbs: None,
45            sync_bytes: None,
46            sync_period: None,
47            rp_augment_limit: None,
48            loose_limit: None,
49            dp_reserve_limit: None,
50            txn_dp_limit: None,
51            spill_max_denominator: None,
52            spill_min_denominator: None,
53            geometry: None,
54            log_level: None,
55            kind: Default::default(),
56            handle_slow_readers: None,
57            #[cfg(feature = "read-tx-timeouts")]
58            max_read_transaction_duration: None,
59        }
60    }
61
62    /// Returns true if the environment was opened as WRITEMAP.
63    #[inline]
64    pub fn is_write_map(&self) -> bool {
65        self.inner.env_kind.is_write_map()
66    }
67
68    /// Returns the kind of the environment.
69    #[inline]
70    pub fn env_kind(&self) -> EnvironmentKind {
71        self.inner.env_kind
72    }
73
74    /// Returns true if the environment was opened in [`crate::Mode::ReadWrite`] mode.
75    #[inline]
76    pub fn is_read_write(&self) -> Result<bool> {
77        Ok(!self.is_read_only()?)
78    }
79
80    /// Returns true if the environment was opened in [`crate::Mode::ReadOnly`] mode.
81    #[inline]
82    pub fn is_read_only(&self) -> Result<bool> {
83        Ok(matches!(self.info()?.mode(), Mode::ReadOnly))
84    }
85
86    /// Returns the transaction manager.
87    #[inline]
88    pub(crate) fn txn_manager(&self) -> &TxnManager {
89        &self.inner.txn_manager
90    }
91
92    /// Returns the number of timed out transactions that were not aborted by the user yet.
93    #[cfg(feature = "read-tx-timeouts")]
94    pub fn timed_out_not_aborted_transactions(&self) -> usize {
95        self.inner.txn_manager.timed_out_not_aborted_read_transactions().unwrap_or(0)
96    }
97
98    /// Create a read-only transaction for use with the environment.
99    ///
100    /// Reuses a previously-reset transaction handle from the internal pool when available,
101    /// avoiding the `lck_rdt_lock` mutex in MDBX's `mvcc_bind_slot` on each new transaction.
102    #[inline]
103    pub fn begin_ro_txn(&self) -> Result<Transaction<RO>> {
104        if let Some(txn_ptr) = self.inner.ro_txn_pool.pop() {
105            return Ok(Transaction::new_from_ptr(self.clone(), txn_ptr));
106        }
107        Transaction::new(self.clone())
108    }
109
110    /// Returns the read transaction pool.
111    #[inline]
112    pub(crate) fn ro_txn_pool(&self) -> &ReadTxnPool {
113        &self.inner.ro_txn_pool
114    }
115
116    /// Create a read-write transaction for use with the environment. This method will block while
117    /// there are any other read-write transactions open on the environment.
118    pub fn begin_rw_txn(&self) -> Result<Transaction<RW>> {
119        let mut warned = false;
120        let txn = loop {
121            let (tx, rx) = sync_channel(0);
122            self.txn_manager().send_message(TxnManagerMessage::Begin {
123                parent: TxnPtr(ptr::null_mut()),
124                flags: RW::OPEN_FLAGS,
125                sender: tx,
126            });
127            let res = rx.recv().unwrap();
128            if matches!(&res, Err(Error::Busy)) {
129                if !warned {
130                    warned = true;
131                    warn!(target: "libmdbx", "Process stalled, awaiting read-write transaction lock.");
132                }
133                sleep(Duration::from_millis(250));
134                continue
135            }
136
137            break res
138        }?;
139        Ok(Transaction::new_from_ptr(self.clone(), txn.0))
140    }
141
142    /// Returns a raw pointer to the underlying MDBX environment.
143    ///
144    /// The caller **must** ensure that the pointer is never dereferenced after the environment has
145    /// been dropped.
146    #[inline]
147    pub(crate) fn env_ptr(&self) -> *mut ffi::MDBX_env {
148        self.inner.env
149    }
150
151    /// Executes the given closure once
152    ///
153    /// This is only intended to be used when accessing mdbx ffi functions directly is required.
154    ///
155    /// The caller **must** ensure that the pointer is only used within the closure.
156    #[inline]
157    #[doc(hidden)]
158    pub fn with_raw_env_ptr<F, T>(&self, f: F) -> T
159    where
160        F: FnOnce(*mut ffi::MDBX_env) -> T,
161    {
162        f(self.env_ptr())
163    }
164
165    /// Flush the environment data buffers to disk.
166    pub fn sync(&self, force: bool) -> Result<bool> {
167        mdbx_result(unsafe { ffi::mdbx_env_sync_ex(self.env_ptr(), force, false) })
168    }
169
170    /// Retrieves statistics about this environment.
171    pub fn stat(&self) -> Result<Stat> {
172        unsafe {
173            let mut stat = Stat::new();
174            mdbx_result(ffi::mdbx_env_stat_ex(
175                self.env_ptr(),
176                ptr::null(),
177                stat.mdbx_stat(),
178                size_of::<Stat>(),
179            ))?;
180            Ok(stat)
181        }
182    }
183
184    /// Retrieves info about this environment.
185    pub fn info(&self) -> Result<Info> {
186        unsafe {
187            let mut info = Info(mem::zeroed());
188            mdbx_result(ffi::mdbx_env_info_ex(
189                self.env_ptr(),
190                ptr::null(),
191                &mut info.0,
192                size_of::<Info>(),
193            ))?;
194            Ok(info)
195        }
196    }
197
198    /// Retrieves the total number of pages on the freelist.
199    ///
200    /// Along with [`Environment::info()`], this can be used to calculate the exact number
201    /// of used pages as well as free pages in this environment.
202    ///
203    /// ```
204    /// # use reth_libmdbx::Environment;
205    /// let dir = tempfile::tempdir().unwrap();
206    /// let env = Environment::builder().open(dir.path()).unwrap();
207    /// let info = env.info().unwrap();
208    /// let stat = env.stat().unwrap();
209    /// let freelist = env.freelist().unwrap();
210    /// let last_pgno = info.last_pgno() + 1; // pgno is 0 based.
211    /// let total_pgs = info.map_size() / stat.page_size() as usize;
212    /// let pgs_in_use = last_pgno - freelist;
213    /// let pgs_free = total_pgs - pgs_in_use;
214    /// ```
215    ///
216    /// Note:
217    ///
218    /// * MDBX stores all the freelists in the designated database 0 in each environment, and the
219    ///   freelist count is stored at the beginning of the value as `uint32_t` in the native byte
220    ///   order.
221    ///
222    /// * It will create a read transaction to traverse the freelist database.
223    pub fn freelist(&self) -> Result<usize> {
224        let mut freelist: usize = 0;
225        let txn = self.begin_ro_txn()?;
226        let db = Database::freelist_db();
227        let cursor = txn.cursor(db.dbi())?;
228
229        for result in cursor.iter_slices() {
230            let (_key, value) = result?;
231            if value.len() < size_of::<u32>() {
232                return Err(Error::Corrupted)
233            }
234            let s = &value[..size_of::<u32>()];
235            freelist += NativeEndian::read_u32(s) as usize;
236        }
237
238        Ok(freelist)
239    }
240}
241
242/// Container type for Environment internals.
243///
244/// This holds the raw pointer to the MDBX environment and the transaction manager.
245/// The env is opened via [`mdbx_env_create`](ffi::mdbx_env_create) and closed when this type drops.
246struct EnvironmentInner {
247    /// The raw pointer to the MDBX environment.
248    ///
249    /// Accessing the environment is thread-safe as long as long as this type exists.
250    env: *mut ffi::MDBX_env,
251    /// Whether the environment was opened as WRITEMAP.
252    env_kind: EnvironmentKind,
253    /// Transaction manager
254    txn_manager: TxnManager,
255    /// Pool of reset read-only transaction handles for reuse.
256    ro_txn_pool: ReadTxnPool,
257}
258
259impl Drop for EnvironmentInner {
260    fn drop(&mut self) {
261        #[cfg(feature = "read-tx-timeouts")]
262        self.txn_manager.stop_read_transaction_monitor();
263
264        // Abort all pooled read transactions before closing the environment.
265        self.ro_txn_pool.drain();
266
267        // Close open mdbx environment on drop
268        unsafe {
269            ffi::mdbx_env_close_ex(self.env, false);
270        }
271    }
272}
273
274// SAFETY: internal type, only used inside [Environment]. Accessing the environment pointer is
275// thread-safe
276unsafe impl Send for EnvironmentInner {}
277unsafe impl Sync for EnvironmentInner {}
278
279/// Determines how data is mapped into memory
280///
281/// It only takes effect when the environment is opened.
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
283pub enum EnvironmentKind {
284    /// Open the environment in default mode, without WRITEMAP.
285    #[default]
286    Default,
287    /// Open the environment as mdbx-WRITEMAP.
288    /// Use a writable memory map unless the environment is opened as `MDBX_RDONLY`
289    /// ([`crate::Mode::ReadOnly`]).
290    ///
291    /// All data will be mapped into memory in the read-write mode [`crate::Mode::ReadWrite`]. This
292    /// offers a significant performance benefit, since the data will be modified directly in
293    /// mapped memory and then flushed to disk by single system call, without any memory
294    /// management nor copying.
295    ///
296    /// This mode is incompatible with nested transactions.
297    WriteMap,
298}
299
300impl EnvironmentKind {
301    /// Returns true if the environment was opened as WRITEMAP.
302    #[inline]
303    pub const fn is_write_map(&self) -> bool {
304        matches!(self, Self::WriteMap)
305    }
306
307    /// Additional flags required when opening the environment.
308    pub(crate) const fn extra_flags(&self) -> ffi::MDBX_env_flags_t {
309        match self {
310            Self::Default => ffi::MDBX_ENV_DEFAULTS,
311            Self::WriteMap => ffi::MDBX_WRITEMAP,
312        }
313    }
314}
315
316#[derive(Copy, Clone, Debug)]
317pub(crate) struct EnvPtr(pub(crate) *mut ffi::MDBX_env);
318unsafe impl Send for EnvPtr {}
319unsafe impl Sync for EnvPtr {}
320
321/// Environment statistics.
322///
323/// Contains information about the size and layout of an MDBX environment or database.
324#[derive(Debug)]
325#[repr(transparent)]
326pub struct Stat(ffi::MDBX_stat);
327
328impl Stat {
329    /// Create a new Stat with zero'd inner struct `ffi::MDBX_stat`.
330    pub(crate) const fn new() -> Self {
331        unsafe { Self(mem::zeroed()) }
332    }
333
334    /// Returns a mut pointer to `ffi::MDBX_stat`.
335    pub(crate) const fn mdbx_stat(&mut self) -> *mut ffi::MDBX_stat {
336        &mut self.0
337    }
338}
339
340impl Stat {
341    /// Size of a database page. This is the same for all databases in the environment.
342    #[inline]
343    pub const fn page_size(&self) -> u32 {
344        self.0.ms_psize
345    }
346
347    /// Depth (height) of the B-tree.
348    #[inline]
349    pub const fn depth(&self) -> u32 {
350        self.0.ms_depth
351    }
352
353    /// Number of internal (non-leaf) pages.
354    #[inline]
355    pub const fn branch_pages(&self) -> usize {
356        self.0.ms_branch_pages as usize
357    }
358
359    /// Number of leaf pages.
360    #[inline]
361    pub const fn leaf_pages(&self) -> usize {
362        self.0.ms_leaf_pages as usize
363    }
364
365    /// Number of overflow pages.
366    #[inline]
367    pub const fn overflow_pages(&self) -> usize {
368        self.0.ms_overflow_pages as usize
369    }
370
371    /// Number of data items.
372    #[inline]
373    pub const fn entries(&self) -> usize {
374        self.0.ms_entries as usize
375    }
376}
377
378#[derive(Debug)]
379#[repr(transparent)]
380pub struct GeometryInfo(ffi::MDBX_envinfo__bindgen_ty_1);
381
382impl GeometryInfo {
383    pub const fn min(&self) -> u64 {
384        self.0.lower
385    }
386}
387
388/// Environment information.
389///
390/// Contains environment information about the map size, readers, last txn id etc.
391#[derive(Debug)]
392#[repr(transparent)]
393pub struct Info(ffi::MDBX_envinfo);
394
395impl Info {
396    pub const fn geometry(&self) -> GeometryInfo {
397        GeometryInfo(self.0.mi_geo)
398    }
399
400    /// Size of memory map.
401    #[inline]
402    pub const fn map_size(&self) -> usize {
403        self.0.mi_mapsize as usize
404    }
405
406    /// Last used page number
407    #[inline]
408    pub const fn last_pgno(&self) -> usize {
409        self.0.mi_last_pgno as usize
410    }
411
412    /// Last transaction ID
413    #[inline]
414    pub const fn last_txnid(&self) -> usize {
415        self.0.mi_recent_txnid as usize
416    }
417
418    /// Max reader slots in the environment
419    #[inline]
420    pub const fn max_readers(&self) -> usize {
421        self.0.mi_maxreaders as usize
422    }
423
424    /// Max reader slots used in the environment
425    #[inline]
426    pub const fn num_readers(&self) -> usize {
427        self.0.mi_numreaders as usize
428    }
429
430    /// Transaction ID of the oldest active reader.
431    #[inline]
432    pub const fn latter_reader_txnid(&self) -> u64 {
433        self.0.mi_latter_reader_txnid
434    }
435
436    /// Return the internal page ops metrics
437    #[inline]
438    pub const fn page_ops(&self) -> PageOps {
439        PageOps {
440            newly: self.0.mi_pgop_stat.newly,
441            cow: self.0.mi_pgop_stat.cow,
442            clone: self.0.mi_pgop_stat.clone,
443            split: self.0.mi_pgop_stat.split,
444            merge: self.0.mi_pgop_stat.merge,
445            spill: self.0.mi_pgop_stat.spill,
446            unspill: self.0.mi_pgop_stat.unspill,
447            wops: self.0.mi_pgop_stat.wops,
448            prefault: self.0.mi_pgop_stat.prefault,
449            mincore: self.0.mi_pgop_stat.mincore,
450            msync: self.0.mi_pgop_stat.msync,
451            fsync: self.0.mi_pgop_stat.fsync,
452        }
453    }
454
455    /// Return the mode of the database
456    #[inline]
457    pub const fn mode(&self) -> Mode {
458        let mode = self.0.mi_mode as ffi::MDBX_env_flags_t;
459        if (mode & ffi::MDBX_RDONLY) != 0 {
460            Mode::ReadOnly
461        } else if (mode & ffi::MDBX_UTTERLY_NOSYNC) != 0 {
462            Mode::ReadWrite { sync_mode: SyncMode::UtterlyNoSync }
463        } else if (mode & ffi::MDBX_NOMETASYNC) != 0 {
464            Mode::ReadWrite { sync_mode: SyncMode::NoMetaSync }
465        } else if (mode & ffi::MDBX_SAFE_NOSYNC) != 0 {
466            Mode::ReadWrite { sync_mode: SyncMode::SafeNoSync }
467        } else {
468            Mode::ReadWrite { sync_mode: SyncMode::Durable }
469        }
470    }
471}
472
473impl fmt::Debug for Environment {
474    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475        f.debug_struct("Environment").field("kind", &self.inner.env_kind).finish_non_exhaustive()
476    }
477}
478
479///////////////////////////////////////////////////////////////////////////////////////////////////
480// Environment Builder
481///////////////////////////////////////////////////////////////////////////////////////////////////
482
483#[derive(Clone, Debug, PartialEq, Eq)]
484pub enum PageSize {
485    MinimalAcceptable,
486    Set(usize),
487}
488
489/// Statistics of page operations overall of all (running, completed and aborted) transactions
490#[derive(Clone, Debug, PartialEq, Eq)]
491pub struct PageOps {
492    /// Quantity of a new pages added
493    pub newly: u64,
494    /// Quantity of pages copied for update
495    pub cow: u64,
496    /// Quantity of parent's dirty pages clones for nested transactions
497    pub clone: u64,
498    /// Page splits
499    pub split: u64,
500    /// Page merges
501    pub merge: u64,
502    /// Quantity of spilled dirty pages
503    pub spill: u64,
504    /// Quantity of unspilled/reloaded pages
505    pub unspill: u64,
506    /// Number of explicit write operations (not a pages) to a disk
507    pub wops: u64,
508    /// Number of explicit msync/flush-to-disk operations
509    pub msync: u64,
510    /// Number of explicit fsync/flush-to-disk operations
511    pub fsync: u64,
512    /// Number of prefault write operations
513    pub prefault: u64,
514    /// Number of `mincore()` calls
515    pub mincore: u64,
516}
517
518/// Represents the geometry settings for the database environment
519#[derive(Clone, Debug, PartialEq, Eq)]
520pub struct Geometry<R> {
521    /// The size range in bytes.
522    pub size: Option<R>,
523    pub growth_step: Option<isize>,
524    pub shrink_threshold: Option<isize>,
525    pub page_size: Option<PageSize>,
526}
527
528impl<R> Default for Geometry<R> {
529    fn default() -> Self {
530        Self { size: None, growth_step: None, shrink_threshold: None, page_size: None }
531    }
532}
533
534/// Handle-Slow-Readers callback function to resolve database full/overflow issue due to a reader(s)
535/// which prevents the old data from being recycled.
536///
537/// Read transactions prevent reuse of pages freed by newer write transactions, thus the database
538/// can grow quickly. This callback will be called when there is not enough space in the database
539/// (i.e. before increasing the database size or before `MDBX_MAP_FULL` error) and thus can be
540/// used to resolve issues with a "long-lived" read transactions.
541///
542/// Depending on the arguments and needs, your implementation may wait,
543/// terminate a process or thread that is performing a long read, or perform
544/// some other action. In doing so it is important that the returned code always
545/// corresponds to the performed action.
546///
547/// # Arguments
548///
549/// * `process_id` – A process id of the reader process.
550/// * `thread_id` – A thread id of the reader thread.
551/// * `read_txn_id` – An oldest read transaction number on which stalled.
552/// * `gap` – A lag from the last committed txn.
553/// * `space` – A space that actually become available for reuse after this reader finished. The
554///   callback function can take this value into account to evaluate the impact that a long-running
555///   transaction has.
556/// * `retry` – A retry number starting from 0. If callback has returned 0 at least once, then at
557///   end of current handling loop the callback function will be called additionally with negative
558///   `retry` value to notify about the end of loop. The callback function can use this fact to
559///   implement timeout reset logic while waiting for a readers.
560///
561/// # Returns
562///
563/// A return code that determines the further actions for MDBX and must match the action which
564/// was executed by the callback:
565/// * `-2` or less – An error condition and the reader was not killed.
566/// * `-1` – The callback was unable to solve the problem and agreed on `MDBX_MAP_FULL` error; MDBX
567///   should increase the database size or return `MDBX_MAP_FULL` error.
568/// * `0` – The callback solved the problem or just waited for a while, libmdbx should rescan the
569///   reader lock table and retry. This also includes a situation when corresponding transaction
570///   terminated in normal way by `mdbx_txn_abort()` or `mdbx_txn_reset()`, and may be restarted.
571///   I.e. reader slot isn't needed to be cleaned from transaction.
572/// * `1` – Transaction aborted asynchronous and reader slot should be cleared immediately, i.e.
573///   read transaction will not continue but `mdbx_txn_abort()` nor `mdbx_txn_reset()` will be
574///   called later.
575/// * `2` or greater – The reader process was terminated or killed, and MDBX should entirely reset
576///   reader registration.
577pub type HandleSlowReadersCallback = extern "C" fn(
578    env: *const ffi::MDBX_env,
579    txn: *const ffi::MDBX_txn,
580    pid: ffi::mdbx_pid_t,
581    tid: ffi::mdbx_tid_t,
582    laggard: u64,
583    gap: std::ffi::c_uint,
584    space: usize,
585    retry: std::ffi::c_int,
586) -> HandleSlowReadersReturnCode;
587
588#[derive(Debug)]
589#[repr(i32)]
590pub enum HandleSlowReadersReturnCode {
591    /// An error condition and the reader was not killed.
592    Error = -2,
593    /// The callback was unable to solve the problem and agreed on `MDBX_MAP_FULL` error;
594    /// MDBX should increase the database size or return `MDBX_MAP_FULL` error.
595    ProceedWithoutKillingReader = -1,
596    /// The callback solved the problem or just waited for a while, libmdbx should rescan the
597    /// reader lock table and retry. This also includes a situation when corresponding transaction
598    /// terminated in normal way by `mdbx_txn_abort()` or `mdbx_txn_reset()`, and may be restarted.
599    /// I.e. reader slot isn't needed to be cleaned from transaction.
600    Success = 0,
601    /// Transaction aborted asynchronous and reader slot should be cleared immediately, i.e. read
602    /// transaction will not continue but `mdbx_txn_abort()` nor `mdbx_txn_reset()` will be called
603    /// later.
604    ClearReaderSlot = 1,
605    /// The reader process was terminated or killed, and MDBX should entirely reset reader
606    /// registration.
607    ReaderProcessTerminated = 2,
608}
609
610/// Options for opening or creating an environment.
611#[derive(Debug, Clone)]
612pub struct EnvironmentBuilder {
613    flags: EnvironmentFlags,
614    max_readers: Option<u64>,
615    max_dbs: Option<u64>,
616    sync_bytes: Option<u64>,
617    sync_period: Option<u64>,
618    rp_augment_limit: Option<u64>,
619    loose_limit: Option<u64>,
620    dp_reserve_limit: Option<u64>,
621    txn_dp_limit: Option<u64>,
622    spill_max_denominator: Option<u64>,
623    spill_min_denominator: Option<u64>,
624    geometry: Option<Geometry<(Option<usize>, Option<usize>)>>,
625    log_level: Option<ffi::MDBX_log_level_t>,
626    kind: EnvironmentKind,
627    handle_slow_readers: Option<HandleSlowReadersCallback>,
628    #[cfg(feature = "read-tx-timeouts")]
629    /// The maximum duration of a read transaction. If [None], but the `read-tx-timeout` feature is
630    /// enabled, the default value of [`DEFAULT_MAX_READ_TRANSACTION_DURATION`] is used.
631    max_read_transaction_duration: Option<read_transactions::MaxReadTransactionDuration>,
632}
633
634impl EnvironmentBuilder {
635    /// Open an environment.
636    ///
637    /// Database files will be opened with 644 permissions.
638    pub fn open(&self, path: &Path) -> Result<Environment> {
639        self.open_with_permissions(path, 0o644)
640    }
641
642    /// Open an environment with the provided UNIX permissions.
643    ///
644    /// The path may not contain the null character.
645    pub fn open_with_permissions(
646        &self,
647        path: &Path,
648        mode: ffi::mdbx_mode_t,
649    ) -> Result<Environment> {
650        let mut env: *mut ffi::MDBX_env = ptr::null_mut();
651        unsafe {
652            if let Some(log_level) = self.log_level {
653                // Returns the previously debug_flags in the 0-15 bits and log_level in the
654                // 16-31 bits, no need to use `mdbx_result`.
655                ffi::mdbx_setup_debug(log_level, ffi::MDBX_DBG_DONTCHANGE, None);
656            }
657
658            mdbx_result(ffi::mdbx_env_create(&mut env))?;
659
660            if let Err(e) = (|| {
661                if let Some(geometry) = &self.geometry {
662                    let mut min_size = -1;
663                    let mut max_size = -1;
664
665                    if let Some(size) = geometry.size {
666                        if let Some(size) = size.0 {
667                            min_size = size as isize;
668                        }
669
670                        if let Some(size) = size.1 {
671                            max_size = size as isize;
672                        }
673                    }
674
675                    mdbx_result(ffi::mdbx_env_set_geometry(
676                        env,
677                        min_size,
678                        -1,
679                        max_size,
680                        geometry.growth_step.unwrap_or(-1),
681                        geometry.shrink_threshold.unwrap_or(-1),
682                        match geometry.page_size {
683                            None => -1,
684                            Some(PageSize::MinimalAcceptable) => 0,
685                            Some(PageSize::Set(size)) => size as isize,
686                        },
687                    ))?;
688                }
689                for (opt, v) in [
690                    (ffi::MDBX_opt_max_db, self.max_dbs),
691                    (ffi::MDBX_opt_rp_augment_limit, self.rp_augment_limit),
692                    (ffi::MDBX_opt_loose_limit, self.loose_limit),
693                    (ffi::MDBX_opt_dp_reserve_limit, self.dp_reserve_limit),
694                    (ffi::MDBX_opt_txn_dp_limit, self.txn_dp_limit),
695                    (ffi::MDBX_opt_spill_max_denominator, self.spill_max_denominator),
696                    (ffi::MDBX_opt_spill_min_denominator, self.spill_min_denominator),
697                ] {
698                    if let Some(v) = v {
699                        mdbx_result(ffi::mdbx_env_set_option(env, opt, v))?;
700                    }
701                }
702
703                // set max readers if specified
704                if let Some(max_readers) = self.max_readers {
705                    mdbx_result(ffi::mdbx_env_set_option(
706                        env,
707                        ffi::MDBX_opt_max_readers,
708                        max_readers,
709                    ))?;
710                }
711
712                if let Some(handle_slow_readers) = self.handle_slow_readers {
713                    mdbx_result(ffi::mdbx_env_set_hsr(
714                        env,
715                        convert_hsr_fn(Some(handle_slow_readers)),
716                    ))?;
717                }
718
719                #[cfg(unix)]
720                fn path_to_bytes<P: AsRef<Path>>(path: P) -> Vec<u8> {
721                    use std::os::unix::ffi::OsStrExt;
722                    path.as_ref().as_os_str().as_bytes().to_vec()
723                }
724
725                #[cfg(windows)]
726                fn path_to_bytes<P: AsRef<Path>>(path: P) -> Vec<u8> {
727                    // On Windows, could use std::os::windows::ffi::OsStrExt to encode_wide(),
728                    // but we end up with a Vec<u16> instead of a Vec<u8>, so that doesn't
729                    // really help.
730                    path.as_ref().to_string_lossy().to_string().into_bytes()
731                }
732
733                let path = match CString::new(path_to_bytes(path)) {
734                    Ok(path) => path,
735                    Err(_) => return Err(Error::Invalid),
736                };
737                mdbx_result(ffi::mdbx_env_open(
738                    env,
739                    path.as_ptr(),
740                    self.flags.make_flags() | self.kind.extra_flags(),
741                    mode,
742                ))?;
743
744                for (opt, v) in [
745                    (ffi::MDBX_opt_sync_bytes, self.sync_bytes),
746                    (ffi::MDBX_opt_sync_period, self.sync_period),
747                ] {
748                    if let Some(v) = v {
749                        mdbx_result(ffi::mdbx_env_set_option(env, opt, v))?;
750                    }
751                }
752
753                Ok(())
754            })() {
755                ffi::mdbx_env_close_ex(env, false);
756
757                return Err(e)
758            }
759        }
760
761        let env_ptr = EnvPtr(env);
762
763        #[cfg(not(feature = "read-tx-timeouts"))]
764        let txn_manager = TxnManager::new(env_ptr);
765
766        #[cfg(feature = "read-tx-timeouts")]
767        let txn_manager = {
768            if let crate::MaxReadTransactionDuration::Set(duration) = self
769                .max_read_transaction_duration
770                .unwrap_or(read_transactions::MaxReadTransactionDuration::Set(
771                    DEFAULT_MAX_READ_TRANSACTION_DURATION,
772                ))
773            {
774                TxnManager::new_with_max_read_transaction_duration(env_ptr, duration)
775            } else {
776                TxnManager::new(env_ptr)
777            }
778        };
779
780        let env = EnvironmentInner {
781            env,
782            txn_manager,
783            env_kind: self.kind,
784            ro_txn_pool: ReadTxnPool::new(),
785        };
786
787        Ok(Environment { inner: Arc::new(env) })
788    }
789
790    /// Configures how this environment will be opened.
791    pub const fn set_kind(&mut self, kind: EnvironmentKind) -> &mut Self {
792        self.kind = kind;
793        self
794    }
795
796    /// Opens the environment with mdbx WRITEMAP
797    ///
798    /// See also [`EnvironmentKind`]
799    pub const fn write_map(&mut self) -> &mut Self {
800        self.set_kind(EnvironmentKind::WriteMap)
801    }
802
803    /// Sets the provided options in the environment.
804    pub const fn set_flags(&mut self, flags: EnvironmentFlags) -> &mut Self {
805        self.flags = flags;
806        self
807    }
808
809    /// Sets the maximum number of threads or reader slots for the environment.
810    ///
811    /// This defines the number of slots in the lock table that is used to track readers in the
812    /// environment. The default is 126. Starting a read-only transaction normally ties a lock
813    /// table slot to the [Transaction] object until it or the [Environment] object is destroyed.
814    pub const fn set_max_readers(&mut self, max_readers: u64) -> &mut Self {
815        self.max_readers = Some(max_readers);
816        self
817    }
818
819    /// Sets the maximum number of named databases for the environment.
820    ///
821    /// This function is only needed if multiple databases will be used in the
822    /// environment. Simpler applications that use the environment as a single
823    /// unnamed database can ignore this option.
824    ///
825    /// Currently a moderate number of slots are cheap but a huge number gets
826    /// expensive: 7-120 words per transaction, and every [`Transaction::open_db()`]
827    /// does a linear search of the opened slots.
828    pub const fn set_max_dbs(&mut self, v: usize) -> &mut Self {
829        self.max_dbs = Some(v as u64);
830        self
831    }
832
833    /// Sets the interprocess/shared threshold to force flush the data buffers to disk, if
834    /// [`SyncMode::SafeNoSync`] is used.
835    pub const fn set_sync_bytes(&mut self, v: usize) -> &mut Self {
836        self.sync_bytes = Some(v as u64);
837        self
838    }
839
840    /// Sets the interprocess/shared relative period since the last unsteady commit to force flush
841    /// the data buffers to disk, if [`SyncMode::SafeNoSync`] is used.
842    pub fn set_sync_period(&mut self, v: Duration) -> &mut Self {
843        // For this option, mdbx uses units of 1/65536 of a second.
844        let as_mdbx_units = (v.as_secs_f64() * 65536f64) as u64;
845        self.sync_period = Some(as_mdbx_units);
846        self
847    }
848
849    pub const fn set_rp_augment_limit(&mut self, v: u64) -> &mut Self {
850        self.rp_augment_limit = Some(v);
851        self
852    }
853
854    pub const fn set_loose_limit(&mut self, v: u64) -> &mut Self {
855        self.loose_limit = Some(v);
856        self
857    }
858
859    pub const fn set_dp_reserve_limit(&mut self, v: u64) -> &mut Self {
860        self.dp_reserve_limit = Some(v);
861        self
862    }
863
864    pub const fn set_txn_dp_limit(&mut self, v: u64) -> &mut Self {
865        self.txn_dp_limit = Some(v);
866        self
867    }
868
869    pub fn set_spill_max_denominator(&mut self, v: u8) -> &mut Self {
870        self.spill_max_denominator = Some(v.into());
871        self
872    }
873
874    pub fn set_spill_min_denominator(&mut self, v: u8) -> &mut Self {
875        self.spill_min_denominator = Some(v.into());
876        self
877    }
878
879    /// Set all size-related parameters of environment, including page size and the min/max size of
880    /// the memory map.
881    pub fn set_geometry<R: RangeBounds<usize>>(&mut self, geometry: Geometry<R>) -> &mut Self {
882        let convert_bound = |bound: Bound<&usize>| match bound {
883            Bound::Included(v) | Bound::Excluded(v) => Some(*v),
884            _ => None,
885        };
886        self.geometry = Some(Geometry {
887            size: geometry.size.map(|range| {
888                (convert_bound(range.start_bound()), convert_bound(range.end_bound()))
889            }),
890            growth_step: geometry.growth_step,
891            shrink_threshold: geometry.shrink_threshold,
892            page_size: geometry.page_size,
893        });
894        self
895    }
896
897    pub const fn set_log_level(&mut self, log_level: ffi::MDBX_log_level_t) -> &mut Self {
898        self.log_level = Some(log_level);
899        self
900    }
901
902    /// Set the Handle-Slow-Readers callback. See [`HandleSlowReadersCallback`] for more
903    /// information.
904    pub fn set_handle_slow_readers(&mut self, hsr: HandleSlowReadersCallback) -> &mut Self {
905        self.handle_slow_readers = Some(hsr);
906        self
907    }
908}
909
910#[cfg(feature = "read-tx-timeouts")]
911pub(crate) mod read_transactions {
912    use crate::EnvironmentBuilder;
913    use std::time::Duration;
914
915    /// The maximum duration of a read transaction.
916    #[derive(Debug, Clone, Copy)]
917    #[cfg(feature = "read-tx-timeouts")]
918    pub enum MaxReadTransactionDuration {
919        /// The maximum duration of a read transaction is unbounded.
920        Unbounded,
921        /// The maximum duration of a read transaction is set to the given duration.
922        Set(Duration),
923    }
924
925    #[cfg(feature = "read-tx-timeouts")]
926    impl MaxReadTransactionDuration {
927        pub const fn as_duration(&self) -> Option<Duration> {
928            match self {
929                Self::Unbounded => None,
930                Self::Set(duration) => Some(*duration),
931            }
932        }
933    }
934
935    impl EnvironmentBuilder {
936        /// Set the maximum time a read-only transaction can be open.
937        pub const fn set_max_read_transaction_duration(
938            &mut self,
939            max_read_transaction_duration: MaxReadTransactionDuration,
940        ) -> &mut Self {
941            self.max_read_transaction_duration = Some(max_read_transaction_duration);
942            self
943        }
944    }
945}
946
947/// Converts a [`HandleSlowReadersCallback`] to the actual FFI function pointer.
948fn convert_hsr_fn(callback: Option<HandleSlowReadersCallback>) -> ffi::MDBX_hsr_func {
949    unsafe { std::mem::transmute(callback) }
950}
951
952#[cfg(test)]
953mod tests {
954    use crate::{Environment, Error, Geometry, HandleSlowReadersReturnCode, PageSize, WriteFlags};
955    use std::{
956        ops::RangeInclusive,
957        sync::atomic::{AtomicBool, Ordering},
958    };
959
960    #[test]
961    fn test_handle_slow_readers_callback() {
962        static CALLED: AtomicBool = AtomicBool::new(false);
963
964        extern "C" fn handle_slow_readers(
965            _env: *const ffi::MDBX_env,
966            _txn: *const ffi::MDBX_txn,
967            _pid: ffi::mdbx_pid_t,
968            _tid: ffi::mdbx_tid_t,
969            _laggard: u64,
970            _gap: std::ffi::c_uint,
971            _space: usize,
972            _retry: std::ffi::c_int,
973        ) -> HandleSlowReadersReturnCode {
974            CALLED.store(true, Ordering::Relaxed);
975            HandleSlowReadersReturnCode::ProceedWithoutKillingReader
976        }
977
978        let tempdir = tempfile::tempdir().unwrap();
979        let env = Environment::builder()
980            .set_geometry(Geometry::<RangeInclusive<usize>> {
981                size: Some(0..=1024 * 1024), // Max 1MB, so we can hit the limit
982                page_size: Some(PageSize::MinimalAcceptable), // To create as many pages as possible
983                ..Default::default()
984            })
985            .set_handle_slow_readers(handle_slow_readers)
986            .open(tempdir.path())
987            .unwrap();
988
989        // Insert some data in the database, so the read transaction can lock on the snapshot of it
990        {
991            let tx = env.begin_rw_txn().unwrap();
992            let db = tx.open_db(None).unwrap();
993            for i in 0usize..1_000 {
994                tx.put(db.dbi(), i.to_le_bytes(), b"0", WriteFlags::empty()).unwrap()
995            }
996            tx.commit().unwrap();
997        }
998
999        // Create a read transaction
1000        let _tx_ro = env.begin_ro_txn().unwrap();
1001
1002        // Change previously inserted data, so the read transaction would use the previous snapshot
1003        {
1004            let tx = env.begin_rw_txn().unwrap();
1005            let db = tx.open_db(None).unwrap();
1006            for i in 0usize..1_000 {
1007                tx.put(db.dbi(), i.to_le_bytes(), b"1", WriteFlags::empty()).unwrap();
1008            }
1009            tx.commit().unwrap();
1010        }
1011
1012        // Insert more data in the database, so we hit the DB size limit error, and MDBX tries to
1013        // kick long-lived readers and delete their snapshots
1014        {
1015            let tx = env.begin_rw_txn().unwrap();
1016            let db = tx.open_db(None).unwrap();
1017            for i in 1_000usize..1_000_000 {
1018                match tx.put(db.dbi(), i.to_le_bytes(), b"0", WriteFlags::empty()) {
1019                    Ok(_) => {}
1020                    Err(Error::MapFull) => break,
1021                    result @ Err(_) => result.unwrap(),
1022                }
1023            }
1024            // The transaction may be in an error state after hitting MapFull,
1025            // so commit could fail. We don't care about the result here since
1026            // the purpose of this test is to verify the HSR callback was called.
1027            let _ = tx.commit();
1028        }
1029
1030        // Expect the HSR to be called
1031        assert!(CALLED.load(Ordering::Relaxed));
1032    }
1033}