# BST-TIMEOUT: 1200 # 112 cases, 152s measured locally; the nightly's browser run still exceeds 300s . "$BST_LIB" # LMDB 0.9.31 -- Howard Chu's Lightning Memory-Mapped Database, as meta-oe ships # it from recipes-dbs. A transactional, MVCC, copy-on-write B+tree store in two # C files; the engine behind OpenLDAP and a very large amount of everything # else. # # --------------------------------------------------------------------------- # WHY THIS SUITE IS SHAPED THE WAY IT IS # # LMDB is not a library that happens to use mmap. BEING A MEMORY MAP IS THE # DESIGN: a reader does no I/O at all, it dereferences pointers into a # MAP_SHARED view of the data file, and a writer commits by pwrite()ing pages # that the readers' existing map is expected to pick up. So porting it asks this # platform one question that no other package in this image asks -- # # IS A MAP_SHARED FILE MAPPING A LIVE VIEW OF THE FILE, OR A SNAPSHOT? # # -- and the answer here is "a snapshot", which is why this suite has the shape # it has. Three separate platform gaps were measured while porting it, each # against a native x86_64 control running the identical probe: # # 1. fcntl(F_SETLK/F_SETLKW/F_GETLK) -> EINVAL. POSIX record locking is # unsupported (brintos.io/brintos/linux/issues/16). mdb_env_setup_locks() # takes a whole-file F_WRLCK on lock.mdb before doing anything else, so the # DEFAULT mdb_env_open() returns EINVAL and every mdb_* tool fails at line # one. MDB_NOLOCK skips the lock file entirely and opens fine -- so on its # own this gap costs only multi-process safety, not function. # # 2. mmap(MAP_SHARED) with PROT_WRITE -> ENODEV # (brintos.io/brintos/linux/issues/30, found by the gdbm port). This is # what MDB_WRITEMAP asks for, and MDB_WRITEMAP is LMDB'S OWN ESCAPE HATCH # from gap 3 -- with it, writes go THROUGH the map and no coherence is # needed. So gap 2 is what makes gap 3 unworkable-around. # # 3. A PROT_READ MAP_SHARED map of a regular file IS A SNAPSHOT TAKEN AT # mmap() TIME (brintos.io/brintos/linux/issues/43 -- filed by this port). # No write becomes visible through it: not pwrite on the same fd, not # write+fsync on a second fd, not a second PROCESS writing. Meanwhile # read(2) and a freshly created second mapping both see the new bytes at # once, and msync(MS_INVALIDATE) is ENOSYS. Identical on tmpfs and # virtiofs. # # THE CONSEQUENCE FOR LMDB IS EXACT, and asserting it is the point of this # suite. Under MDB_NOLOCK a write transaction commits correctly and the bytes DO # reach the file -- but the read transaction that follows it in the same process # still sees the map as it was at open, so: # # nolock/get-after-commit MDB_NOTFOUND <- reads back EMPTY # nolock/cursor-walk count=0 # reopen/get-persisted OK value-07-lmdb <- but reopen finds all of it # # That pair is the single most informative thing this port produced, and the # suite asserts BOTH halves: durability is fine, visibility is not. A test that # only checked "can I read my write back" would have reported "lmdb is broken" # and been useless to whoever has to fix it. # # EXPECTATIONS COME FROM A NATIVE x86_64 CONTROL, never from reading mdb.c. # data/lmdb-native.txt is the verbatim output of data/lmdb-probe's source built # against a native build of the identical 0.9.31 tree (same three meta-oe # patches) and run under the same arrangement. The probe prints only booleans, # small counts and fixed strings -- never a pointer, a pid, a path or a page # size -- which is what makes the two runs comparable at all for a library whose # whole subject is addresses. # # THE BLOCK IS COMPUTED FROM THIS RUN, NOT HARD-CODED. Whether the map is # coherent is decided below by comparing the probe's own reopen/commit pair, so # the day issue 43 or issue 30 lands, this suite goes green by itself with no # edit here. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY under dash before it ships. The runner never sets it, so on the image # these are exactly the absolute paths. R="${BST_LMDB_ROOT:-}" ARCHIVE=$R/usr/lib/liblmdb.a HDR=$R/usr/include/lmdb.h PROBE=$BST_DATA/lmdb-probe NATIVE=$BST_DATA/lmdb-native.txt ISSUE_LOCK="blocked by brintos.io/brintos/linux/issues/16 (fcntl record locking returns EINVAL on wasm32-hwjs). mdb_env_setup_locks() takes a whole-file fcntl(F_SETLK) on lock.mdb before anything else, so the default mdb_env_open() cannot succeed; MDB_NOLOCK opens fine" ISSUE_MAP="blocked by brintos.io/brintos/linux/issues/43 (a PROT_READ MAP_SHARED file mapping is a snapshot taken at mmap() time -- no write from any source ever becomes visible through it, while read(2) and a fresh mmap see it at once). LMDB readers dereference the map, so a committed write reads back empty until the environment is reopened" ISSUE_WMAP="blocked by brintos.io/brintos/linux/issues/30 (mmap(MAP_SHARED) with PROT_WRITE returns ENODEV). MDB_WRITEMAP is LMDB's own workaround for issue 43 -- writes would go through the map and need no coherence -- so this is the gap that makes the other one unworkable-around" if [ ! -x "$R/usr/bin/mdb_stat" ] && [ ! -f "$ARCHIVE" ]; then bst_skip lmdb/present "lmdb not installed on this image" exit 0 fi bst_ok lmdb/present # --- 1. the package is whole ------------------------------------------------- # # ${PN} is the four tools, -dev the header, -staticdev the archive. A missing # piece here means a packaging split went wrong, not that the platform refused # anything. for t in mdb_stat mdb_copy mdb_dump mdb_load; do check "lmdb/bin-$t" test -x "$R/usr/bin/$t" done check lmdb/header test -f "$HDR" check lmdb/staticlib test -f "$ARCHIVE" # The bbappend declines to build liblmdb.so because this arm refuses the # -shared side-module link. A .so here would mean that override stopped taking # effect -- which would not fail the build and would not fail any other case. if [ -e "$R/usr/lib/liblmdb.so" ] || [ -e "$R/usr/lib/liblmdb.so.0.9.31" ]; then bst_notok lmdb/no-shared "a shared liblmdb is installed on a static-only distro" else bst_ok lmdb/no-shared fi # The stock recipe DELETES liblmdb.a after installing (it ships the .so); here # the archive is the deliverable, so its presence is a deliberate divergence # worth asserting rather than assuming. if [ -s "$ARCHIVE" ]; then bst_ok lmdb/archive-nonempty else bst_notok lmdb/archive-nonempty "$ARCHIVE is absent or empty"; fi # --- 2. the header declares the API ------------------------------------------ for fn in mdb_env_create mdb_env_open mdb_txn_begin mdb_txn_commit mdb_get mdb_put \ mdb_cursor_open mdb_cursor_get mdb_dbi_open mdb_strerror; do if grep -q "$fn" "$HDR" 2>/dev/null; then bst_ok "lmdb/hdr-$fn" else bst_notok "lmdb/hdr-$fn" "$fn missing from $HDR"; fi done # MDB_NOLOCK and MDB_WRITEMAP are the two flags this port's findings turn on, # so the header must actually offer them. check_out lmdb/hdr-nolock "MDB_NOLOCK" cat "$HDR" check_out lmdb/hdr-writemap "MDB_WRITEMAP" cat "$HDR" # --- 3. archive symbols ------------------------------------------------------ # On this arm the archive is ThinLTO bitcode, so symbol NAMES survive as plain # text while string constants do not -- the limitation libutempter recorded. if command -v strings >/dev/null 2>&1; then syms=$(strings "$ARCHIVE" 2>/dev/null) for s in mdb_env_open mdb_txn_commit mdb_cursor_get mdb_midl_alloc; do check_out "lmdb/sym-$s" "$s" printf '%s' "$syms" done else for s in mdb_env_open mdb_txn_commit mdb_cursor_get mdb_midl_alloc; do bst_skip "lmdb/sym-$s" "strings not installed on this image" done fi # --- 4. the tools answer at all ---------------------------------------------- # Every mdb_* tool opens an environment before it can do anything useful, so # these are the only assertions about them that are not downstream of issue 16. # `mdb_stat -V` prints the library version and exits without touching a file. check_out lmdb/tool-version "0.9.31" "$R/usr/bin/mdb_stat" -V if "$R/usr/bin/mdb_dump" -V 2>&1 | grep -q '0\.9\.31'; then bst_ok lmdb/dump-version else bst_notok lmdb/dump-version "mdb_dump -V did not report 0.9.31"; fi # A tool given a directory that is not an environment must refuse cleanly rather # than crash. This runs even where issue 16 blocks a real open, because the # failure happens before the lock file matters. work=${TMPDIR:-/tmp}/bst-lmdb.$$ rm -rf "$work"; mkdir -p "$work/empty" || true check_rc lmdb/stat-refuses-nonenv 1 "$R/usr/bin/mdb_stat" "$work/empty" # --- 5. the probe: the library actually running ------------------------------ for f in "$PROBE" "$NATIVE"; do if [ ! -f "$f" ]; then bst_skip lmdb/probe-runs "$(basename "$f") absent from the image" rm -rf "$work" exit 0 fi done # data/* installs 0644, so stage a copy and make it executable. The probe writes # its environments into the cwd, so run it inside the scratch dir. pout=$work/probe.out pbin=$work/lmdb-probe mkdir -p "$work/run" if ! cp "$PROBE" "$pbin" 2>/dev/null || ! chmod +x "$pbin" 2>/dev/null; then bst_notok lmdb/probe-runs "cannot stage the probe to $pbin" rm -rf "$work" exit 0 fi perr=$(cd "$work/run" && "$pbin" 2>&1 >"$pout"); prc=$? if [ "$prc" -eq 0 ]; then bst_ok lmdb/probe-runs else bst_notok lmdb/probe-runs "probe exited rc=$prc $(_bst_1line "$perr")"; fi # A terminator, so a run that died halfway is distinguishable from one that # merely disagreed. if grep -q '^==LMDB-PROBE-END==$' "$pout" 2>/dev/null; then bst_ok lmdb/probe-complete else bst_notok lmdb/probe-complete "probe output has no ==LMDB-PROBE-END== terminator" fi # val -> the probe's value for that case, or empty if absent. val() { awk -v want="$2" '$1 == "LP" && $2 == want { out=""; for (i = 3; i <= NF; i++) out = out (i > 3 ? " " : "") $i; print out; exit }' "$1" } # --- 6. decide, from THIS run, which platform gaps are in effect ------------- # # Each is read off the probe rather than assumed, so the suite tracks the # platform instead of describing a moment in July 2026. # # LOCKED -- the default (lock-file) open path is refused # MAPSNAP -- a committed write is invisible to the next read txn, but a # reopen finds it: the snapshot-map signature, and NOT merely # "the write was lost" # NOWMAP -- MDB_WRITEMAP cannot be opened d_open=$(val "$pout" open-default) n_after=$(val "$pout" nolock/get-after-commit) n_reopen=$(val "$pout" reopen/get-persisted) w_open=$(val "$pout" open-writemap) [ "$d_open" = "OK" ] && LOCKED="" || LOCKED="$ISSUE_LOCK" [ "$w_open" = "OK" ] && NOWMAP="" || NOWMAP="$ISSUE_WMAP" if [ "$n_after" = "OK value-07-lmdb" ]; then MAPSNAP="" else MAPSNAP="$ISSUE_MAP"; fi # --- 7. what works regardless of the gaps ------------------------------------ # # These are compared against the native control by value, and they are the # substance of the port: they say the library links, initialises, computes and # refuses exactly as it does on x86_64. for c in version-triple version-macro-match version-string-nonempty \ strerror-notfound strerror-corrupted strerror-success \ env-create env-set-mapsize env-set-maxdbs env-set-maxreaders \ env-get-maxreaders env-maxkeysize env-get-flags env-close-unopened \ open-nonexistent-dir; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done # --- 8. the default open path, and everything downstream of it --------------- if [ -z "$LOCKED" ]; then bst_ok lmdb/open-default for c in default/txn-begin-write default/dbi-open default/put-16 \ default/get-in-txn default/txn-commit default/txn-begin-read \ default/dbi-reopen default/get-after-commit default/stat-entries \ default/cursor-open default/cursor-walk default/cursor-last \ default/cursor-set-key default/get-missing default/env-sync; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done else # This one is a HARD FAILURE, because it is the finding: the default way to # open an LMDB environment does not work on this platform. bst_notok lmdb/open-default "mdb_env_open() with default flags returned $d_open (want OK); $ISSUE_LOCK" # The rest are not independent observations -- they are the same fact # restated, and they flip back together the day issue 16 lands. for c in default/txn-begin-write default/dbi-open default/put-16 \ default/get-in-txn default/txn-commit default/txn-begin-read \ default/dbi-reopen default/get-after-commit default/stat-entries \ default/cursor-open default/cursor-walk default/cursor-last \ default/cursor-set-key default/get-missing default/env-sync; do bst_skip "lmdb/$c" "$ISSUE_LOCK" done fi # --- 9. MDB_NOLOCK: the environment opens, and the write path is CORRECT ----- # # Everything up to and including the commit is asserted unconditionally: these # work here, they are the port's real functional coverage, and none of them # touches the shared map (a write txn reads from its own dirty page list). for c in open-nolock nolock/txn-begin-write nolock/dbi-open nolock/put-16 \ nolock/get-in-txn nolock/txn-commit nolock/txn-begin-read \ nolock/dbi-reopen nolock/get-missing nolock/env-sync \ nolock/env-info nolock/env-copy; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done # --- 10. the read-after-commit group: where the snapshot map lands ----------- if [ -z "$MAPSNAP" ]; then for c in nolock/get-after-commit nolock/stat-entries nolock/cursor-open \ nolock/cursor-walk nolock/cursor-last nolock/cursor-set-key; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done else # The hard failure, stated as what a user would see. bst_notok lmdb/read-after-commit \ "a committed record is not visible to the next read txn (want:'OK value-07-lmdb' got:'$n_after'); $ISSUE_MAP" for c in nolock/get-after-commit nolock/stat-entries nolock/cursor-walk \ nolock/cursor-last nolock/cursor-set-key; do bst_skip "lmdb/$c" "$ISSUE_MAP" done # cursor-open itself does NOT depend on the map contents, so it stays a real # assertion -- opening a cursor on an empty tree must still succeed. check_eq lmdb/nolock/cursor-open "$(val "$NATIVE" nolock/cursor-open)" \ "$(val "$pout" nolock/cursor-open)" fi # --- 11. durability, which is the OTHER half of the finding ------------------ # # This is asserted unconditionally and must be green on both platforms. It is # what distinguishes "the map is stale" from "the write was lost", and without # it the read-after-commit red above would be unattributable -- and would look # like data loss, which it is not. for c in reopen-nolock reopen/txn-begin reopen/dbi-open reopen/get-persisted; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done # --- 12. MDB_WRITEMAP -------------------------------------------------------- if [ -z "$NOWMAP" ]; then for c in open-writemap writemap/txn-begin-write writemap/dbi-open \ writemap/put-16 writemap/get-in-txn writemap/txn-commit \ writemap/txn-begin-read writemap/dbi-reopen \ writemap/get-after-commit writemap/stat-entries \ writemap/cursor-open writemap/cursor-walk writemap/cursor-last \ writemap/cursor-set-key writemap/get-missing writemap/env-sync; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done else bst_notok lmdb/open-writemap \ "mdb_env_open(MDB_WRITEMAP) returned $w_open (want OK); $ISSUE_WMAP" for c in writemap/txn-begin-write writemap/dbi-open writemap/put-16 \ writemap/get-in-txn writemap/txn-commit writemap/txn-begin-read \ writemap/dbi-reopen writemap/get-after-commit \ writemap/stat-entries writemap/cursor-open writemap/cursor-walk \ writemap/cursor-last writemap/cursor-set-key writemap/get-missing \ writemap/env-sync; do bst_skip "lmdb/$c" "$ISSUE_WMAP" done fi # --- 13. named sub-databases, all inside one write txn ----------------------- # # Green here even with every gap in effect, because nothing in this group reads # through the map -- which is exactly why it is worth asserting separately. It # covers MDB_CREATE, MDB_NOOVERWRITE's MDB_KEYEXIST refusal, and delete. for c in open-named named/txn-begin named/dbi-create named/put \ named/get-in-txn named/put-nooverwrite named/del \ named/get-after-del named/txn-commit; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done # --- 14. refusals ------------------------------------------------------------ # A 1 KB key (maxkeysize is 511) and a zero-length key must both be # MDB_BAD_VALSIZE, not silently accepted. for c in put-oversized-key put-empty-key; do check_eq "lmdb/$c" "$(val "$NATIVE" "$c")" "$(val "$pout" "$c")" done # --- 15. the aggregate ------------------------------------------------------- # # A byte-compare of the whole probe output against the native control, done in # the guest with cmp(1) rather than by shipping the text through the console # (the BRINTOS-260 lesson that a console dump truncates long lines). It is a # skip while any gap is in effect -- the outputs cannot match by construction -- # and becomes a real assertion the moment they all clear. if [ -n "$LOCKED" ] || [ -n "$MAPSNAP" ] || [ -n "$NOWMAP" ]; then bst_skip lmdb/matches-native \ "cannot match the native control while a platform gap is in effect: ${LOCKED:+16 }${MAPSNAP:+43 }${NOWMAP:+30 }" elif cmp -s "$pout" "$NATIVE"; then bst_ok lmdb/matches-native else bst_notok lmdb/matches-native "probe output differs from data/lmdb-native.txt" fi rm -rf "$work"