From c6e3ccb0cd1e5ae9fbdc1e0666a9b9338275ec10 Mon Sep 17 00:00:00 2001 From: speeddragon Date: Tue, 14 Apr 2026 12:29:16 +0100 Subject: [PATCH] feat: add env_stat/1 NIF to expose map_size and used bytes Adds env_stat/1 which calls mdb_env_info (via lmdb-sys FFI) to return the configured map_size and an approximate used-bytes figure calculated as (last_pgno + 1) * page_size. Callers can compare the two values to detect proximity to the map_size limit before hitting MDB_MAP_FULL. lmdb 0.8 does not wrap mdb_env_info in its safe API, so lmdb-sys is added as an explicit dependency and the call is made through unsafe FFI. Co-Authored-By: Claude Sonnet 4.6 --- native/elmdb_nif/Cargo.toml | 1 + native/elmdb_nif/src/lib.rs | 35 +++++++++++++++++++++++++++++++++-- src/elmdb.erl | 23 ++++++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/native/elmdb_nif/Cargo.toml b/native/elmdb_nif/Cargo.toml index 80096ac..07b5907 100644 --- a/native/elmdb_nif/Cargo.toml +++ b/native/elmdb_nif/Cargo.toml @@ -10,4 +10,5 @@ crate-type = ["cdylib"] [dependencies] rustler = { version = "0.36.2", features = ["nif_version_2_16"] } lmdb = "0.8" +lmdb-sys = "0.8" lazy_static = "1.4" \ No newline at end of file diff --git a/native/elmdb_nif/src/lib.rs b/native/elmdb_nif/src/lib.rs index 5c85f47..39ec842 100644 --- a/native/elmdb_nif/src/lib.rs +++ b/native/elmdb_nif/src/lib.rs @@ -28,6 +28,7 @@ use rustler::{Env, Term, NifResult, Error, Encoder, ResourceArc}; use rustler::types::binary::Binary; use rustler::types::binary::OwnedBinary; use std::collections::{HashMap, HashSet, VecDeque}; +use std::mem; use std::sync::{Arc, Mutex}; use std::path::Path; use lmdb::{Environment, EnvironmentFlags, Database, DatabaseFlags, Transaction, WriteFlags, Cursor}; @@ -1598,15 +1599,45 @@ struct DbOptions { #[rustler::nif] fn env_status<'a>(env: Env<'a>, env_handle: ResourceArc) -> NifResult> { let closed = env_handle.is_closed().map_err(|_| Error::BadArg)?; - + let ref_count = { let ref_count = env_handle.ref_count.lock().map_err(|_| Error::BadArg)?; *ref_count }; - + Ok((atoms::ok(), closed, ref_count, env_handle.path.clone()).encode(env)) } +/// Returns the configured map_size and the approximate number of bytes currently +/// used by the environment. +/// +/// Used bytes are estimated as `(last_pgno + 1) * page_size`, which counts every +/// page that has ever been allocated (including free-list pages). The value is +/// therefore an upper bound on live data size, but it is the right metric for +/// deciding how close the environment is to hitting the map_size limit. +/// +/// `lmdb 0.8` does not expose `mdb_env_info` in its safe API, so we call it +/// through the `lmdb-sys` FFI layer directly. +#[rustler::nif] +fn env_stat<'a>(env: Env<'a>, env_handle: ResourceArc) -> NifResult> { + let (lmdb_env, _gen) = env_handle.ensure_open().map_err(|_| Error::BadArg)?; + let stat = lmdb_env.stat().map_err(|_| Error::BadArg)?; + let page_size = stat.page_size() as usize; + + // mdb_env_info is not wrapped by the `lmdb` crate, so call it directly. + let (map_size, last_pgno) = unsafe { + let mut info: lmdb_sys::MDB_envinfo = mem::zeroed(); + let rc = lmdb_sys::mdb_env_info(lmdb_env.env(), &mut info); + if rc != 0 { + return Err(Error::BadArg); + } + (info.me_mapsize, info.me_last_pgno) + }; + + let used_bytes = (last_pgno + 1) * page_size; + Ok((atoms::ok(), map_size, used_bytes).encode(env)) +} + // Initialize the NIF module // explicit fuctions are deprecated but here is a list // [env_open, env_close, env_close_by_name, db_open, db_close, put, put_batch, get, list, flush, env_status] diff --git a/src/elmdb.erl b/src/elmdb.erl index 3ee6ebc..04bacdd 100644 --- a/src/elmdb.erl +++ b/src/elmdb.erl @@ -9,7 +9,7 @@ -module(elmdb). %% Environment management --export([env_open/2, env_sync/1, env_close/1, env_close_by_name/1, env_status/1]). +-export([env_open/2, env_sync/1, env_close/1, env_close_by_name/1, env_status/1, env_stat/1]). %% Database operations -export([db_open/2, db_close/1]). @@ -119,6 +119,27 @@ env_close_by_name(_Path) -> env_status(_Env) -> erlang:nif_error(nif_not_loaded). +%% @doc Get size statistics for an LMDB environment. +%% +%% Returns the configured map_size limit and an estimate of bytes currently in +%% use. Used bytes are calculated as `(last_pgno + 1) * page_size`, which +%% counts every page ever allocated (including free-list pages) and therefore +%% represents the high-water mark rather than live data only. This is the +%% correct value for gauging proximity to the map_size limit. +%% +%% Only meaningful for writable environments; calling this on a read-only +%% environment (opened with no_lock) is safe but may return stale values if +%% another process is the primary writer. +%% +%% @param Env Environment handle from env_open +%% @returns {ok, MapSize, UsedBytes} where both values are non-negative integers +%% in bytes, or {error, badarg} if the environment cannot be opened. +-spec env_stat(Env :: term()) -> + {ok, MapSize :: non_neg_integer(), UsedBytes :: non_neg_integer()} | + {error, badarg}. +env_stat(_Env) -> + erlang:nif_error(nif_not_loaded). + %%%=================================================================== %%% Database Operations