Skip to content

Make Env.closed volatile and document the close() lifecycle contract - #288

Open
bernardladenthin wants to merge 1 commit into
lmdbjava:masterfrom
bernardladenthin:fix/env-close-visibility-and-docs
Open

Make Env.closed volatile and document the close() lifecycle contract#288
bernardladenthin wants to merge 1 commit into
lmdbjava:masterfrom
bernardladenthin:fix/env-close-visibility-and-docs

Conversation

@bernardladenthin

Copy link
Copy Markdown

Summary

Two no-cost, no-behaviour-change hardening changes for the close-during-read hazard that surfaces as a native SIGSEGV in mdb_txn_renew0 under concurrent access. Motivated by #253 (whose crash stack is mdb_txn_renew0 ← mdb_txn_begin ← containsAddress), which I traced to closing an Env while a reader is still inside txnRead()/mdb_txn_begin on another thread.

Neither change adds locking to the hot path, consistent with the package policy that "LmdbJava DO NOT provide any concurrency guarantees". They make the failure less likely and clearly documented without changing the zero-overhead contract. (A separate, opt-in follow-up PR proposes an actual drain-on-close guard for callers who want it — deliberately kept out of this PR.)

C — Env.closed is now volatile

close() may run on a different thread than the readers calling checkNotClosed(). As a plain field there was no happens-before between the write and those reads, so a reader could indefinitely observe a stale false (the JIT may even hoist the check out of a hot loop) and proceed into a native call on a freed env.

volatile does not make close() atomic w.r.t. an in-flight txnRead()/txnWrite() — the check-then-mdb_txn_begin window remains — but it removes the pure visibility bug and turns more of those races into a clean AlreadyClosedException instead of a JVM crash. Cost is a single volatile read on paths that already read the field.

D — document the lifecycle contract

Env.close() previously documented only "silently return if already closed". It now spells out the contract LMDB's C API imposes but lmdbjava never surfaced: all txns/cursors/dbis must be closed and no other thread may be touching the env (or a derived handle) during close(), and violating this is undefined behaviour that crashes the JVM (SIGSEGV / EXCEPTION_ACCESS_VIOLATION 0xC0000005), not a Java exception. txnRead()/txnWrite() gain a cross-reference and @throws AlreadyClosedException.

Test plan

  • EnvTest 38/38 (covers idempotent close() and the existing AlreadyClosedException paths).
  • com.spotify.fmt:fmt-maven-plugin:check clean (0/96 non-complying).
  • No behavioural change to the default hot path.

Refs #253.

Two no-cost hardening changes for the close-during-read hazard that surfaces as a
native SIGSEGV in mdb_txn_renew0 (see lmdbjava#253):

C) Env.closed is now volatile. close() may run on a different thread than the
   readers calling checkNotClosed(); as a plain field there was no happens-before
   between the write and those reads, so a reader could indefinitely observe a
   stale false (the JIT may even hoist the check out of a hot loop) and proceed
   into a native call on a freed env. volatile does not make close() atomic w.r.t.
   an in-flight txnRead()/txnWrite() -- the check-then-mdb_txn_begin window
   remains -- but it removes the pure visibility bug and turns more of those races
   into a clean AlreadyClosedException instead of a JVM crash. Cost is a single
   volatile read on paths that already read the field.

D) Env.close() now documents the lifecycle contract that LMDB's C API imposes but
   lmdbjava never surfaced: all txns/cursors/dbis must be closed and no other
   thread may be touching the env (or a handle derived from it) during close(),
   and violating this is undefined behaviour that crashes the JVM (SIGSEGV /
   EXCEPTION_ACCESS_VIOLATION 0xC0000005), not a Java exception. txnRead()/
   txnWrite() gain a cross-reference and an @throws AlreadyClosedException.

No behavioural change to the default hot path; consistent with the package policy
that LmdbJava provides no concurrency guarantees. A follow-up branch proposes an
opt-in safe close (drain in-flight readers) on top of this.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GdiZ3ABYsVHXBRNCkEizpE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant