From 5609b3d4c850233201233bb0aaaaad2b697b6b3b Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:11:39 +0000 Subject: [PATCH 01/61] Add RefCounter to Env to prevent closure while in use WIP --- src/main/java/org/lmdbjava/Cursor.java | 37 +- src/main/java/org/lmdbjava/Env.java | 288 ++++++++------ src/main/java/org/lmdbjava/EnvState.java | 9 + .../java/org/lmdbjava/NoOpRefCounter.java | 40 ++ src/main/java/org/lmdbjava/RefCounter.java | 55 +++ .../lmdbjava/SingleThreadedRefCounter.java | 98 +++++ .../org/lmdbjava/StripedRefCounterImpl.java | 375 ++++++++++++++++++ src/main/java/org/lmdbjava/Txn.java | 30 +- .../java/org/lmdbjava/CursorIterableTest.java | 17 +- src/test/java/org/lmdbjava/CursorTest.java | 19 +- .../java/org/lmdbjava/RefCounterTest.java | 175 ++++++++ 11 files changed, 999 insertions(+), 144 deletions(-) create mode 100644 src/main/java/org/lmdbjava/EnvState.java create mode 100644 src/main/java/org/lmdbjava/NoOpRefCounter.java create mode 100644 src/main/java/org/lmdbjava/RefCounter.java create mode 100644 src/main/java/org/lmdbjava/SingleThreadedRefCounter.java create mode 100644 src/main/java/org/lmdbjava/StripedRefCounterImpl.java create mode 100644 src/test/java/org/lmdbjava/RefCounterTest.java diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 0e320930..1471ab6f 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -30,6 +30,7 @@ import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.SeekOp.MDB_PREV; +import java.util.concurrent.atomic.AtomicBoolean; import jnr.ffi.Pointer; import jnr.ffi.byref.NativeLongByReference; @@ -40,19 +41,27 @@ */ public final class Cursor implements AutoCloseable { - private boolean closed; + private AtomicBoolean closed; private final KeyVal kv; private final Pointer ptrCursor; private Txn txn; private final Env env; + private final RefCounter.RefCounterReleaser refCounterReleaser; - Cursor(final Pointer ptr, final Txn txn, final Env env) { + Cursor(final Pointer ptr, + final Txn txn, + final Env env) { requireNonNull(ptr); requireNonNull(txn); + requireNonNull(env); this.ptrCursor = ptr; this.txn = txn; + // The env needs to track open cursors to prevent env closure before the cursors are closed + System.out.println("Acquiring for cursor"); + this.refCounterReleaser = env.acquire(); this.kv = txn.newKeyVal(); this.env = env; + this.closed = new AtomicBoolean(false); } /** @@ -63,18 +72,20 @@ public final class Cursor implements AutoCloseable { */ @Override public void close() { - if (closed) { - return; - } - kv.close(); - if (SHOULD_CHECK) { - env.checkNotClosed(); - if (!txn.isReadOnly()) { - txn.checkReady(); + if (closed.compareAndSet(false, true)) { + kv.close(); + if (SHOULD_CHECK) { + env.checkNotClosed(); + } + // Cannot close the mdb_cursor if the txn is writable and not in a ready state + if (txn.isReadOnly() || txn.isReady()) { + LIB.mdb_cursor_close(ptrCursor); } + System.out.println("Closing cursor"); + refCounterReleaser.release(); + } else { + System.out.println("Already closed"); } - LIB.mdb_cursor_close(ptrCursor); - closed = true; } /** @@ -518,7 +529,7 @@ public T val() { } private void checkNotClosed() { - if (closed) { + if (closed.get()) { throw new ClosedException(); } } diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 4bc6cca8..4f80b8c3 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -51,7 +51,9 @@ */ public final class Env implements AutoCloseable { - /** Java system property name that can be set to disable optional checks. */ + /** + * Java system property name that can be set to disable optional checks. + */ public static final String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks"; /** @@ -67,7 +69,7 @@ public final class Env implements AutoCloseable { */ public static final boolean SHOULD_CHECK = !getBoolean(DISABLE_CHECKS_PROP); - private boolean closed; + private final RefCounter refCounter; private final int maxKeySize; private final boolean noSubDir; private final BufferProxy proxy; @@ -75,6 +77,7 @@ public final class Env implements AutoCloseable { private final boolean readOnly; private final Path path; private final EnvFlagSet envFlagSet; + private final boolean isSingleThreaded; private Env( final BufferProxy proxy, @@ -82,7 +85,8 @@ private Env( final boolean readOnly, final boolean noSubDir, final Path path, - final EnvFlagSet envFlagSet) { + final EnvFlagSet envFlagSet, + final boolean isSingleThreaded) { this.proxy = proxy; this.readOnly = readOnly; this.noSubDir = noSubDir; @@ -91,6 +95,16 @@ private Env( this.maxKeySize = LIB.mdb_env_get_maxkeysize(ptr); this.path = path; this.envFlagSet = envFlagSet; + this.isSingleThreaded = isSingleThreaded; + if (SHOULD_CHECK) { + if (isSingleThreaded) { + this.refCounter = new SingleThreadedRefCounter(this::closeMdbEnv); + } else { + this.refCounter = new StripedRefCounterImpl(this::closeMdbEnv); + } + } else { + this.refCounter = new NoOpRefCounter(); + } } /** @@ -105,7 +119,7 @@ public static Builder create() { /** * Create an {@link Env} using the passed {@link BufferProxy}. * - * @param buffer type + * @param buffer type * @param proxy the proxy to use (required) * @return the environment (never null) */ @@ -114,13 +128,13 @@ public static Builder create(final BufferProxy proxy) { } /** - * @param path file system destination - * @param size size in megabytes + * @param path file system destination + * @param size size in megabytes * @param flags the flags for this new environment * @return env the environment (never null) * @deprecated Instead use {@link Env#create()} or {@link Env#create(BufferProxy)} - *

Opens an environment with a single default database in 0664 mode using the {@link - * ByteBufferProxy#PROXY_OPTIMAL}. + *

Opens an environment with a single default database in 0664 mode using the {@link + * ByteBufferProxy#PROXY_OPTIMAL}. */ @Deprecated public static Env open(final File path, final int size, final EnvFlags... flags) { @@ -134,10 +148,11 @@ public static Env open(final File path, final int size, final EnvFla */ @Override public void close() { - if (closed) { - return; - } - closed = true; + System.out.println("Closing Env"); + refCounter.close(); + } + + private void closeMdbEnv() { LIB.mdb_env_close(ptr); } @@ -156,7 +171,7 @@ public void close() { * transactions, because it employs a read-only transaction. See long-lived transactions under * "Caveats" in the LMDB native documentation. * - * @param path writable destination path as described above + * @param path writable destination path as described above * @param flags special options for this copy * @deprecated Use {@link Env#copy(Path, CopyFlagSet)} */ @@ -202,7 +217,7 @@ public void copy(final Path path) { * transactions, because it employs a read-only transaction. See long-lived transactions under * "Caveats" in the LMDB native documentation. * - * @param path writable destination path as described above + * @param path writable destination path as described above * @param flags special options for this copy */ public void copy(final Path path, final CopyFlagSet flags) { @@ -274,7 +289,7 @@ public void setMapSize(final long mapSize) { /** * Set the size of the data memory map. * - * @param mapSize new map size in the units of byteUnit. + * @param mapSize new map size in the units of byteUnit. * @param byteUnit The unit that mapSize is in. */ public void setMapSize(final long mapSize, final ByteUnit byteUnit) { @@ -297,9 +312,7 @@ public int getMaxKeySize() { * @return an immutable information object. */ public EnvInfo info() { - if (closed) { - throw new AlreadyClosedException(); - } + checkNotClosed(); final MDB_envinfo info = new MDB_envinfo(RUNTIME); checkRc(LIB.mdb_env_info(ptr, info)); @@ -325,7 +338,8 @@ public EnvInfo info() { * @return true if closed */ public boolean isClosed() { - return closed; + // TODO should this return true if state == CLOSING, or state != OPEN ? + return refCounter.isClosed(); } /** @@ -350,12 +364,12 @@ public DbiBuilder createDbi() { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and default - * {@link Comparator} that is not invoked from native code. + *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and default + * {@link Comparator} that is not invoked from native code. */ @Deprecated() public Dbi openDbi(final String name, final DbiFlags... flags) { @@ -363,19 +377,19 @@ public Dbi openDbi(final String name, final DbiFlags... flags) { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator for cursor start/stop key comparisons. If null, LMDB's - * comparator will be used. - * @param flags to open the database with + * comparator will be used. + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated - * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys. - *

It is very important that the passed comparator behaves in the same way as the - * comparator LMDB uses for its insertion order (for the type of data that will be stored in - * the database), or you fully understand the implications of them behaving differently. - * LMDB's comparator is unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is - * used. + *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys. + *

It is very important that the passed comparator behaves in the same way as the + * comparator LMDB uses for its insertion order (for the type of data that will be stored in + * the database), or you fully understand the implications of them behaving differently. + * LMDB's comparator is unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is + * used. */ @Deprecated() public Dbi openDbi( @@ -384,18 +398,18 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator for cursor start/stop key comparisons and optionally for - * LMDB to call back to. If null, LMDB's comparator will be used. - * @param nativeCb whether LMDB native code calls back to the Java comparator - * @param flags to open the database with + * LMDB to call back to. If null, LMDB's comparator will be used. + * @param nativeCb whether LMDB native code calls back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated - * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing - * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be - * called by LMDB to determine insertion/iteration order. Calling back to a java comparator - * may significantly impact performance. + *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing + * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be + * called by LMDB to determine insertion/iteration order. Calling back to a java comparator + * may significantly impact performance. */ @Deprecated() public Dbi openDbi( @@ -407,12 +421,12 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is - * not invoked from native code. + *

Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is + * not invoked from native code. */ @Deprecated() public Dbi openDbi(final byte[] name, final DbiFlags... flags) { @@ -420,13 +434,13 @@ public Dbi openDbi(final byte[] name, final DbiFlags... flags) { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom iterator comparator (or null to use LMDB default) - * @param flags to open the database with + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that - * is not invoked from native code. + *

Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that + * is not invoked from native code. */ @Deprecated() public Dbi openDbi( @@ -435,16 +449,16 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native code calls back to the Java comparator - * @param flags to open the database with + * @param nativeCb whether native code calls back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that - * may be invoked from native code if specified. - *

This method will automatically commit the private transaction before returning. This - * ensures the Dbi is available in the Env. + *

Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that + * may be invoked from native code if specified. + *

This method will automatically commit the private transaction before returning. This + * ensures the Dbi is available in the Env. */ @Deprecated() public Dbi openDbi( @@ -461,27 +475,27 @@ public Dbi openDbi( } /** - * @param txn transaction to use (required; not closed) - * @param name name of the database (or null if no name is required) + * @param txn transaction to use (required; not closed) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native LMDB code should call back to the Java comparator - * @param flags to open the database with + * @param nativeCb whether native LMDB code should call back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

Open the {@link Dbi} using the passed {@link Txn}. - *

The caller must commit the transaction after this method returns in order to retain the - * Dbi in the Env. - *

A {@link Comparator} may be provided when calling this method. Such comparator is - * primarily used by {@link CursorIterable} instances. A secondary (but uncommon) use of the - * comparator is to act as a callback from the native library if nativeCb is - * true. This is usually avoided due to the overhead of native code calling back - * into Java. It is instead highly recommended to set the correct {@link DbiFlags} to allow - * the native library to correctly order the intended keys. - *

A default comparator will be provided if null is passed as the comparator. - * If a custom comparator is provided, it must strictly match the lexicographical order of - * keys in the native LMDB database. - *

This method (and its overloaded convenience variants) must not be called from concurrent - * threads. + *

Open the {@link Dbi} using the passed {@link Txn}. + *

The caller must commit the transaction after this method returns in order to retain the + * Dbi in the Env. + *

A {@link Comparator} may be provided when calling this method. Such comparator is + * primarily used by {@link CursorIterable} instances. A secondary (but uncommon) use of the + * comparator is to act as a callback from the native library if nativeCb is + * true. This is usually avoided due to the overhead of native code calling back + * into Java. It is instead highly recommended to set the correct {@link DbiFlags} to allow + * the native library to correctly order the intended keys. + *

A default comparator will be provided if null is passed as the comparator. + * If a custom comparator is provided, it must strictly match the lexicographical order of + * keys in the native LMDB database. + *

This method (and its overloaded convenience variants) must not be called from concurrent + * threads. */ @Deprecated() public Dbi openDbi( @@ -499,9 +513,7 @@ public Dbi openDbi( * @return an immutable statistics object. */ public Stat stat() { - if (closed) { - throw new AlreadyClosedException(); - } + checkNotClosed(); final MDB_stat stat = new MDB_stat(RUNTIME); checkRc(LIB.mdb_env_stat(ptr, stat)); return new Stat( @@ -517,26 +529,23 @@ public Stat stat() { * Flushes the data buffers to disk. * * @param force force a synchronous flush (otherwise if the environment has the MDB_NOSYNC flag - * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) + * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) */ public void sync(final boolean force) { - if (closed) { - throw new AlreadyClosedException(); - } + checkNotClosed(); final int f = force ? 1 : 0; checkRc(LIB.mdb_env_sync(ptr, f)); } /** * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (eg for a reusable, read-only transaction) + * @param flags applicable flags (eg for a reusable, read-only transaction) * @return a transaction (never null) * @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)} - *

Obtain a transaction with the requested parent and flags. + *

Obtain a transaction with the requested parent and flags. */ @Deprecated public Txn txn(final Txn parent, final TxnFlags... flags) { - checkNotClosed(); return new Txn<>(this, parent, proxy, TxnFlagSet.of(flags)); } @@ -547,7 +556,6 @@ public Txn txn(final Txn parent, final TxnFlags... flags) { * @return a transaction (never null) */ public Txn txn(final Txn parent) { - checkNotClosed(); return new Txn<>(this, parent, proxy, TxnFlagSet.EMPTY); } @@ -555,13 +563,12 @@ public Txn txn(final Txn parent) { * Obtain a transaction with the requested parent and flags. * * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags - * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} - * for re-use. + * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags + * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} + * for re-use. * @return a transaction (never null) */ public Txn txn(final Txn parent, final TxnFlagSet flags) { - checkNotClosed(); return new Txn<>(this, parent, proxy, flags); } @@ -571,7 +578,6 @@ public Txn txn(final Txn parent, final TxnFlagSet flags) { * @return a read-only transaction */ public Txn txnRead() { - checkNotClosed(); return new Txn<>(this, null, proxy, TxnFlags.MDB_RDONLY_TXN); } @@ -581,7 +587,6 @@ public Txn txnRead() { * @return a read-write transaction */ public Txn txnWrite() { - checkNotClosed(); return new Txn<>(this, null, proxy, TxnFlagSet.EMPTY); } @@ -590,9 +595,7 @@ Pointer pointer() { } void checkNotClosed() { - if (closed) { - throw new AlreadyClosedException(); - } + refCounter.checkNotClosed(); } private void validateDirectoryEmpty(final Path path) { @@ -629,7 +632,13 @@ public int readerCheck() { return resultPtr.intValue(); } - /** For testing use. */ + RefCounter.RefCounterReleaser acquire() { + return refCounter.acquire(); + } + + /** + * For testing use. + */ EnvFlagSet getEnvFlagSet() { return envFlagSet; } @@ -638,7 +647,7 @@ EnvFlagSet getEnvFlagSet() { public String toString() { return "Env{" + "closed=" - + closed + + refCounter.isClosed() + ", maxKeySize=" + maxKeySize + ", noSubDir=" @@ -649,26 +658,52 @@ public String toString() { + path + ", envFlagSet=" + envFlagSet + + ", singleThreaded=" + + isSingleThreaded + '}'; } - /** Object has already been closed and the operation is therefore prohibited. */ + public static final class EnvInUseException extends LmdbException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance. + */ + public EnvInUseException() { + super("Environment has open transactions/cursors so cannot be closed."); + } + + public EnvInUseException(final int count) { + super("Environment has open " + count + " transactions/cursors so cannot be closed."); + } + } + + /** + * Object has already been closed and the operation is therefore prohibited. + */ public static final class AlreadyClosedException extends LmdbException { private static final long serialVersionUID = 1L; - /** Creates a new instance. */ + /** + * Creates a new instance. + */ public AlreadyClosedException() { super("Environment has already been closed"); } } - /** Object has already been opened and the operation is therefore prohibited. */ + /** + * Object has already been opened and the operation is therefore prohibited. + */ public static final class AlreadyOpenException extends LmdbException { private static final long serialVersionUID = 1L; - /** Creates a new instance. */ + /** + * Creates a new instance. + */ public AlreadyOpenException() { super("Environment has already been opened"); } @@ -691,6 +726,7 @@ public static final class Builder { private boolean opened; private final BufferProxy proxy; private int mode = POSIX_MODE_DEFAULT; + private boolean singleThreaded = false; private final AbstractFlagSet.Builder flagSetBuilder = EnvFlagSet.builder(); @@ -702,12 +738,12 @@ public static final class Builder { /** * Opens the environment. * - * @param path file system destination - * @param mode Unix permissions to set on created files and semaphores + * @param path file system destination + * @param mode Unix permissions to set on created files and semaphores * @param flags the flags for this new environment * @return an environment ready for use * @deprecated Instead use {@link Builder#open(Path)}, {@link Builder#setFilePermissions(int)} - * and {@link Builder#setEnvFlags(EnvFlags...)}. + * and {@link Builder#setEnvFlags(EnvFlags...)}. */ @Deprecated public Env open(final File path, final int mode, final EnvFlags... flags) { @@ -731,11 +767,11 @@ public Env open(final File path) { /** * Opens the environment with 0664 mode. * - * @param path file system destination + * @param path file system destination * @param flags the flags for this new environment * @return an environment ready for use * @deprecated Instead use {@link Builder#open(Path)} and {@link - * Builder#setEnvFlags(EnvFlags...)}. + * Builder#setEnvFlags(EnvFlags...)}. */ @Deprecated public Env open(final File path, final EnvFlags... flags) { @@ -766,7 +802,7 @@ public Env open(final Path path) { final boolean readOnly = flags.isSet(MDB_RDONLY_ENV); final boolean noSubDir = flags.isSet(MDB_NOSUBDIR); checkRc(LIB.mdb_env_open(ptr, path.toAbsolutePath().toString(), flags.getMask(), mode)); - return new Env<>(proxy, ptr, readOnly, noSubDir, path, flags); + return new Env<>(proxy, ptr, readOnly, noSubDir, path, flags, singleThreaded); } catch (final LmdbNativeException e) { LIB.mdb_env_close(ptr); throw e; @@ -793,7 +829,7 @@ public Builder setMapSize(final long mapSize) { /** * Sets the map size in the supplied unit. * - * @param mapSize new map size in the units of byteUnit. + * @param mapSize new map size in the units of byteUnit. * @param byteUnit The unit that mapSize is in. * @return the builder */ @@ -852,7 +888,7 @@ public Builder setFilePermissions(final int mode) { * Sets all the flags used to open this {@link Env}. * * @param envFlags The flags to use. Clears any existing flags. A null value results in no flags - * being set. + * being set. * @return this builder instance. */ public Builder setEnvFlags(final Collection envFlags) { @@ -867,7 +903,7 @@ public Builder setEnvFlags(final Collection envFlags) { * Sets all the flags used to open this {@link Env}. * * @param envFlags The flags to use. Clears any existing flags. A null value results in no flags - * being set. + * being set. * @return this builder instance. */ public Builder setEnvFlags(final EnvFlags... envFlags) { @@ -882,7 +918,7 @@ public Builder setEnvFlags(final EnvFlags... envFlags) { * Sets all the flags used to open this {@link Env}. * * @param envFlagSet The flags to use. Clears any existing flags. A null value results in no - * flags being set. + * flags being set. * @return this builder instance. */ public Builder setEnvFlags(final EnvFlagSet envFlagSet) { @@ -921,7 +957,7 @@ public Builder addEnvFlags(final EnvFlagSet envFlagSet) { * Adds a {@link Collection} of {@link EnvFlags} to any existing flags. * * @param envFlags The {@link Collection} of flags to add to any existing flags. A null value is - * a no-op. + * a no-op. * @return this builder instance. */ public Builder addEnvFlags(final Collection envFlags) { @@ -930,9 +966,23 @@ public Builder addEnvFlags(final Collection envFlags) { } return this; } + + /** + * If set the the {@link Env} will only be used by the same thread for its entire life. + * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. + * using primitives rather than thread-safe objects. + * By default, an Env is considered thread-safe. + * @return this builder instance. + */ + public Builder singleThreaded() { + singleThreaded = true; + return this; + } } - /** File is not a valid LMDB file. */ + /** + * File is not a valid LMDB file. + */ public static final class FileInvalidException extends LmdbNativeException { static final int MDB_INVALID = -30_793; @@ -943,7 +993,9 @@ public static final class FileInvalidException extends LmdbNativeException { } } - /** The specified copy destination is invalid. */ + /** + * The specified copy destination is invalid. + */ public static final class InvalidCopyDestination extends LmdbException { private static final long serialVersionUID = 1L; @@ -958,7 +1010,9 @@ public InvalidCopyDestination(final String message) { } } - /** Environment mapsize reached. */ + /** + * Environment mapsize reached. + */ public static final class MapFullException extends LmdbNativeException { static final int MDB_MAP_FULL = -30_792; @@ -969,7 +1023,9 @@ public static final class MapFullException extends LmdbNativeException { } } - /** Environment maxreaders reached. */ + /** + * Environment maxreaders reached. + */ public static final class ReadersFullException extends LmdbNativeException { static final int MDB_READERS_FULL = -30_790; @@ -980,7 +1036,9 @@ public static final class ReadersFullException extends LmdbNativeException { } } - /** Environment version mismatch. */ + /** + * Environment version mismatch. + */ public static final class VersionMismatchException extends LmdbNativeException { static final int MDB_VERSION_MISMATCH = -30_794; diff --git a/src/main/java/org/lmdbjava/EnvState.java b/src/main/java/org/lmdbjava/EnvState.java new file mode 100644 index 00000000..9db52d15 --- /dev/null +++ b/src/main/java/org/lmdbjava/EnvState.java @@ -0,0 +1,9 @@ +package org.lmdbjava; + + +public enum EnvState { + OPEN, + CLOSING, + CLOSED, + ; +} diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java new file mode 100644 index 00000000..b3cb1fd5 --- /dev/null +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -0,0 +1,40 @@ +package org.lmdbjava; + + +public class NoOpRefCounter implements RefCounter { + + @Override + public RefCounterReleaser acquire() { + return RefCounterReleaser.NO_OP_RELEASER; + } + + @Override + public void use(Runnable runnable) { + runnable.run(); + } + + @Override + public void close() { + // no-op + } + +// @Override +// public void close(long duration, TimeUnit timeUnit) { +// // no-op +// } + + @Override + public boolean isClosed() { + return false; + } + + @Override + public EnvState getState() { + return null; + } + + @Override + public void checkNotClosed() { + // no-op + } +} diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java new file mode 100644 index 00000000..b189dfda --- /dev/null +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -0,0 +1,55 @@ +package org.lmdbjava; + + +/** + * Used to prevent the closure of a thing while other threads are actively + * using that thing. + */ +interface RefCounter { + + /** + * Call this before using the {@link RefCounter} controlled object. + * @return A {@link RefCounterReleaser} to release once the work is complete + */ + RefCounterReleaser acquire(); + + /** + * Calls {@link RefCounter#acquire()}, runs runnable, then calls {@link RefCounterReleaser#release()} + */ + void use(final Runnable runnable); + + /** + * @param counterIdx + */ +// void release(int counterIdx); + + /** + * Closes the {@link RefCounter} controlled item, but only after ensuring all active users of + * it have released. Once {@link RefCounter#close()} is called, all subsequent calls to + * {@link RefCounter#acquire()} or {@link RefCounter#use(Runnable)} will throw an + * {@link org.lmdbjava.Env.AlreadyClosedException} + * @throws org.lmdbjava.Env.EnvInUseException If the {@link Env} has open transactions/cursors. + */ + void close(); + +// void close(final long duration, final TimeUnit timeUnit); + + /** + * @return True if {@link RefCounter#close()} has been called. The actual close may not have completed though. + */ + boolean isClosed(); + + EnvState getState(); + + void checkNotClosed(); + + @FunctionalInterface + interface RefCounterReleaser { + + RefCounterReleaser NO_OP_RELEASER = () -> { + // No-op + }; + + void release(); + } +} diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java new file mode 100644 index 00000000..e131a9cc --- /dev/null +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -0,0 +1,98 @@ +package org.lmdbjava; + + +import java.util.Objects; + +public class SingleThreadedRefCounter implements RefCounter { + + private final Runnable onClose; + private boolean closeCalled = false; + private boolean onCloseCompleted = false; + private int refCount; + private EnvState envState; + + public SingleThreadedRefCounter(final Runnable onClose) { + this.onClose = Objects.requireNonNull(onClose); + this.envState = EnvState.OPEN; + } + + @Override + public RefCounterReleaser acquire() { + if (envState != EnvState.OPEN) { + throw new Env.AlreadyClosedException(); + } + return new SingleThreadedReleaser(this); + } + + private void release() { + if (refCount == 0) { + throw new IllegalStateException("Attempt to release with a refCount of zero"); + } + refCount--; + } + + @Override + public void use(Runnable runnable) { + final RefCounterReleaser releaser = acquire(); + try { + runnable.run(); + } finally { + releaser.release(); + } + } + + @Override + public void close() { + envState = EnvState.CLOSING; +// closeCalled = true; + if (refCount > 0) { + throw new Env.EnvInUseException(); + } +// if (!onCloseCompleted) { + if (envState == EnvState.CLOSING) { + onClose.run(); + envState = EnvState.CLOSED; +// onCloseCompleted = true; + } + } + +// @Override +// public void close(long duration, TimeUnit timeUnit) { +// throw new UnsupportedOperationException("Method not supported for single threaded use."); +// } + + @Override + public boolean isClosed() { + return envState == EnvState.CLOSED; + } + + @Override + public EnvState getState() { + return envState; + } + + @Override + public void checkNotClosed() { + if (envState != EnvState.OPEN) { + throw new Env.AlreadyClosedException(); + } + } + + private static class SingleThreadedReleaser implements RefCounterReleaser { + + private final SingleThreadedRefCounter singleThreadedRefCounter; + private boolean released = false; + + private SingleThreadedReleaser(final SingleThreadedRefCounter singleThreadedRefCounter) { + this.singleThreadedRefCounter = singleThreadedRefCounter; + } + + @Override + public void release() { + if (!released) { + released = true; + singleThreadedRefCounter.release(); + } + } + } +} diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java new file mode 100644 index 00000000..2ef4b867 --- /dev/null +++ b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java @@ -0,0 +1,375 @@ +package org.lmdbjava; + + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +class StripedRefCounterImpl implements RefCounter { + private static final int CLOSED_COUNT = Integer.MIN_VALUE; + + /** + * Number of stripes used to improve the concurrency + */ + private final int stripes; + private final StripeState[] stripeStates; + /** + * Flag to indicate if {@link RefCounter#close()} has been called. + * Once set, it means that all subsequent {@link RefCounter#acquire()} will throw. + */ +// private final AtomicBoolean closeCalled = new AtomicBoolean(false); + private final Runnable onClose; +// private final AtomicBoolean onCloseCompleted = new AtomicBoolean(false); + private final AtomicReference stateRef; + + StripedRefCounterImpl(Runnable onClose) { + // Default to 1 stripe per processor for max concurrency + this(Runtime.getRuntime().availableProcessors(), onClose); + } + + StripedRefCounterImpl(int stripes, Runnable onClose) { + this.stripes = stripes; + if (stripes <= 0) { + throw new IllegalArgumentException("stripes must be positive"); + } + this.stripeStates = new StripeState[stripes]; + this.onClose = requireNonNull(onClose); + this.stateRef = new AtomicReference<>(EnvState.OPEN); + for (int stripeIdx = 0; stripeIdx < stripes; stripeIdx++) { + stripeStates[stripeIdx] = new StripeState(stripeIdx); + } + } + + @Override + public RefCounterReleaser acquire() { +// return doWithOptionalLocking(() -> { + if (stateRef.get() != EnvState.OPEN) { +// if (closeCalled.get()) { + // Close has been initiated, so acquire() is no longer allowed +// System.out.println("Throwing AlreadyClosedException 1"); + throw new Env.AlreadyClosedException(); + } + + // close() may be called after we have checked closeCalled, but the updateAndGet + // will ensure that we cannot increment the count if close() has been called. + + final StripeState stripeState = stripeStates[getStripeIdx()]; + // If we increment the count just before counter is made negative, then close() + // will have to wait for us to release. + final int count = stripeState.counter.updateAndGet(currVal -> { + final int newVal; + if (currVal < 0) { + // Negative means it is in a closed state +// System.out.println("Throwing AlreadyClosedException 2"); +// throw new Env.AlreadyClosedException(); + newVal = currVal; + } else { + newVal = currVal + 1; +// System.out.printf("%s - acquire() called, currVal: %s, newVal: %s%n", +// Thread.currentThread(), currVal, newVal); + if (newVal == Integer.MAX_VALUE) { + // MAX_VALUE is not allowed as that would become CLOSED_COUNT when made negative + throw new IllegalStateException("Too many concurrent acquire calls"); + } + } + return newVal; + }); + + if (count < 0) { + throw new Env.AlreadyClosedException(); + } + // Return the counter index, so the release call can use it + return new RefCounterReleaserImpl(this, stripeState); +// }); + } + + @Override + public void use(final Runnable runnable) { + if (runnable != null) { + final RefCounterReleaser releaser = acquire(); + try { + runnable.run(); + } finally { + releaser.release(); + } + } + } + + private void release(final StripeState stripeState) { +// doWithOptionalLocking(() -> { + stripeState.counter.updateAndGet(currVal -> { + int newVal; + if (currVal > 0) { + // Positive count, so in an open state, therefore -1 back down towards zero + newVal = currVal - 1; +// System.out.printf("%s - release() called, currVal: %s, newVal: %s%n", +// Thread.currentThread(), currVal, newVal); + } else if (currVal == CLOSED_COUNT) { + // CLOSED_COUNT is only set if the value is zero on close() + throw new IllegalStateException("currVal should never be CLOSED_COUNT on release()"); + } else if (currVal < 0) { + // Negative count, so in a closed state + // +1 to take the count back up towards zero + newVal = currVal + 1; +// System.out.printf("%s - release() called, currVal: %s, newVal: %s%n", +// Thread.currentThread(), currVal, newVal); + if (newVal == 0) { + // Reached zero, so set to the magic number + newVal = CLOSED_COUNT; + // We have reached zero, so count down the latch so that close() can stop blocking +// stripeState.countDownLatch.countDown(); + } + } else { + // currVal == 0 + throw new IllegalStateException("currVal should never be zero on release()"); + } + return newVal; + }); +// return null; +// }); + } + +// @Override +// public void release(final int counterIdx) { +// final StripeState stripeState = stripeStates[counterIdx]; +// release(stripeState); +// } + + private void markClosing() { + // Only want to do this once + if (stateRef.compareAndSet(EnvState.OPEN, EnvState.CLOSING)) { +// if (closeCalled.compareAndSet(false, true)) { +// System.out.println("close() called"); + // Place each stripe into a closed state + for (int stripe = 0; stripe < stripes; stripe++) { + final StripeState stripeState = stripeStates[stripe]; + stripeState.counter.updateAndGet(currVal -> { + if (currVal == 0) { + // Count is already at zero so there will be nothing to wait for. +// stripeState.countDownLatch.countDown(); + // Ensures any thread that tries to increment will see it as closed + return CLOSED_COUNT; + } else if (currVal > 0) { + // Make it negative to indicate the closed state but maintain the ref count + // (albeit as a negative number) + final int newVal = currVal * -1; +// System.out.printf("%s - close() called, currVal: %s, newVal: %s%n", +// Thread.currentThread(), currVal, newVal); + return newVal; + } else { + throw new IllegalStateException("currVal should not be zero on close()"); + } + }); + } + } + } + + @Override + public void close() { + markClosing(); + +// if (!onCloseCompleted.get()) { + final EnvState envState = stateRef.get(); + if (envState == EnvState.CLOSING) { + // TODO This will mark as closed, but then throw if it is still in use, which is not + // ideal. It ought to throw before marking closed if in use. + + for (int stripe = 0; stripe < stripes; stripe++) { + final StripeState stripeState = stripeStates[stripe]; + final int count = stripeState.counter.get(); + if (count < 0 && count != CLOSED_COUNT) { + throw new Env.EnvInUseException(getTotalCount()); + } + } + + onClose.run(); + stateRef.set(EnvState.CLOSED); + } else if (envState == EnvState.OPEN) { + throw new IllegalStateException("EnvState should not be OPEN at this point"); + } + } + +// @Override +// public void close(long duration, TimeUnit timeUnit) { +// markClosing(); +// +//// if (!onCloseCompleted.get()) { +// final EnvState envState = stateRef.get(); +// if (envState == EnvState.CLOSING) { +// Duration totalWaitTime = Duration.ZERO; +// // Now wait for all active threads to finish +// for (int stripe = 0; stripe < stripes; stripe++) { +// final Instant stripeStartTime = Instant.now(); +// final StripeState stripeState = stripeStates[stripe]; +// final AtomicInteger counter = stripeState.counter; +// final CountDownLatch latch = stripeState.countDownLatch; +// +// // By this point all counters will be negative, so we need to wait for them ALL to reach 0, +// // except the ones with the magic value of CLOSED_COUNT +// +// int count = counter.get(); +// if (count < 0 && count != CLOSED_COUNT) { +// // Non-zero count so we must wait for it to hit 0 +//// System.out.printf("%s - Waiting for closure, stripe: %s, count: %d, latch: %s%n", +//// Thread.currentThread(), stripe, count, latch.getCount()); +// try { +// // Release will count down when it hits zero +// final long latchCount = latch.getCount(); +// Instant now = Instant.now(); +// final boolean didCountDown = latch.await(duration, timeUnit); +// if (!didCountDown) { +// throw new Env.EnvInUseException(); +// } +// count = counter.get(); +// if (counter.get() != 0) { +// throw new IllegalStateException("count " + count +// + " should be zero at this point, latchCount: " + latchCount + ", new latchCount: " + latch.getCount() +// + ", waited: " + Duration.between(now, Instant.now())); +// } +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// // Swallow +// } +// } else if (count > 0) { +// throw new IllegalStateException("count " + count + " should not positive"); +// } +// +// totalWaitTime = totalWaitTime.plus(Duration.between(stripeStartTime, Instant.now())); +// } +//// System.out.printf("%s - Wait complete, waited %s, count: %s%n", +//// Thread.currentThread(), totalWaitTime, getTotalCount()); +// +// // All counters returned to zero +// onClose.run(); +// stateRef.set(EnvState.CLOSED); +// onCloseCompleted.set(true); +//// System.out.printf("%s - onClose completed, waited %s, count: %s%n", +//// Thread.currentThread(), totalWaitTime, getTotalCount()); +// } else if (envState == EnvState.OPEN) { +// throw new IllegalStateException("EnvState should not be OPEN at this point"); +// } +// } + + private int getStripeIdx() { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + final long idx = Thread.currentThread().getId() % stripes; + return (int) idx; + } + + @Override + public boolean isClosed() { + // TODO May need a three state approach, i.e. + // OPEN - Open and ready for use. + // CLOSING - Not yet closed, but + // CLOSED - + // close() has been called, but it may not be fully closed yet, i.e. the close + // action may not have run/finished. +// return closeCalled.get(); + return stateRef.get() == EnvState.CLOSED; + } + + @Override + public EnvState getState() { + return stateRef.get(); + } + + @Override + public void checkNotClosed() { +// if (closeCalled.get()) { + if (stateRef.get() != EnvState.OPEN) { + throw new Env.AlreadyClosedException(); + } + } + + /** + * @return The total number of active users. Not atomic. + */ + int getTotalCount() { + return Arrays.stream(stripeStates) + .map(StripeState::getCounter) + .mapToInt(AtomicInteger::get) + .filter(i -> i != CLOSED_COUNT) + .map(Math::abs) // Count could be +ve/-ve so take abs value + .sum(); + } + + private static class StripeState { + + private final int index; + private final AtomicInteger counter; + /** + * One latch per stripe. Each will start with a value of 1 + */ +// private final CountDownLatch countDownLatch; + + private StripeState(final int index) { + this.index = index; + this.counter = new AtomicInteger(0); +// this.countDownLatch = new CountDownLatch(1); + } + + int getIndex() { + return index; + } + + AtomicInteger getCounter() { + return counter; + } + +// CountDownLatch getCountDownLatch() { +// return countDownLatch; +// } + + @Override + public String toString() { + return "Stripe{" + + "index=" + index + + ", counter=" + counter + +// ", countDownLatch=" + countDownLatch + + '}'; + } + } + +// private T doWithOptionalLocking(final Supplier supplier) { +// final State state = stateRef.get(); +// if (state == State.CLOSED) { +// throw new Env.AlreadyClosedException(); +// } else if (state == State.OPEN) { +// return supplier.get(); +// } else { +// synchronized (this) { +// final State state2 = stateRef.get(); +// if (state2 == State.CLOSED) { +// throw new Env.AlreadyClosedException(); +// } else if (state2 == State.OPEN) { +// return supplier.get(); +// } else { +// throw new IllegalStateException("Should not be in a sate of CLOSING with the lock held"); +// } +// } +// } +// } + + private static class RefCounterReleaserImpl implements RefCounterReleaser { + + private final AtomicReference refCounterRef; + private final StripeState stripeState; + + private RefCounterReleaserImpl(final StripedRefCounterImpl refCounter, + final StripeState stripeState) { + this.refCounterRef = new AtomicReference<>(refCounter); + this.stripeState = stripeState; + } + + @Override + public void release() { + // Prevent duplicate release calls + final StripedRefCounterImpl refCounter = refCounterRef.getAndSet(null); + if (refCounter != null) { + refCounter.release(stripeState); + } + } + } +} diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 7e9aacf9..3985a9a3 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -44,8 +44,13 @@ public final class Txn implements AutoCloseable { private final boolean readOnly; private final Env env; private State state; + private RefCounter.RefCounterReleaser refCounterReleaser; + + Txn(final Env env, + final Txn parent, + final BufferProxy proxy, + final TxnFlagSet flags) { - Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlagSet flags) { if (SHOULD_CHECK) { Objects.requireNonNull(flags); } @@ -66,6 +71,8 @@ public final class Txn implements AutoCloseable { ptr = txnPtr.getPointer(0); state = READY; + System.out.println("Acquiring for txn"); + this.refCounterReleaser = env.acquire(); } /** Aborts this transaction. */ @@ -97,6 +104,9 @@ public void close() { } keyVal.close(); state = RELEASED; + + System.out.println("Closing Txn"); + release(); } /** Commits this transaction. */ @@ -139,6 +149,15 @@ public boolean isReadOnly() { return readOnly; } + /** + * Whether this transaction is writable (i.e. not read-only). + * + * @return if writable + */ + public boolean isWritable() { + return !readOnly; + } + /** * Fetch the buffer which holds a read-only view of the LMDI allocated memory. Any use of this * buffer must comply with the standard LMDB C "mdb_get" contract (ie do not modify, do not @@ -204,6 +223,10 @@ void checkReady() { } } + boolean isReady() { + return state == READY; + } + void checkWritesAllowed() { if (readOnly) { throw new ReadWriteRequiredException(); @@ -231,6 +254,11 @@ Pointer pointer() { return ptr; } + void release() { + System.out.printf("%s - Txn.release() called%n", Thread.currentThread()); + refCounterReleaser.release(); + } + /** Transaction must abort, has a child, or is invalid. */ public static final class BadException extends LmdbNativeException { diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index d90c3c23..43663d40 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -115,16 +115,21 @@ private void populateTestDataList() { } private void populateDatabase(final Dbi dbi) { - try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bb(2), bb(3), MDB_NOOVERWRITE); - c.put(bb(4), bb(5)); - c.put(bb(6), bb(7)); - c.put(bb(8), bb(9)); + try (Txn txn = env.txnWrite(); + final Cursor cursor = dbi.openCursor(txn)) { + cursor.put(bb(2), bb(3), MDB_NOOVERWRITE); + cursor.put(bb(4), bb(5)); + cursor.put(bb(6), bb(7)); + cursor.put(bb(8), bb(9)); txn.commit(); } } + @Test + void testPopulate() { + final Dbi db = getDb(); + } + @Test void allBackwardTest() { verify(allBackward(), 8, 6, 4, 2); diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index deb75622..ae0b6aee 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -44,9 +44,9 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.Cursor.ClosedException; -import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.ReadOnlyRequiredException; @@ -100,7 +100,7 @@ void closedEnvRejectsSeekFirstCall() { () -> { doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -109,7 +109,7 @@ void closedEnvRejectsSeekLastCall() { () -> { doEnvClosedTest(null, c -> c.seek(MDB_LAST)); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -118,7 +118,7 @@ void closedEnvRejectsSeekNextCall() { () -> { doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -127,7 +127,7 @@ void closedEnvRejectsCloseCall() { () -> { doEnvClosedTest(null, Cursor::close); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -136,7 +136,7 @@ void closedEnvRejectsFirstCall() { () -> { doEnvClosedTest(null, Cursor::first); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -145,7 +145,7 @@ void closedEnvRejectsLastCall() { () -> { doEnvClosedTest(null, Cursor::last); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -161,7 +161,7 @@ void closedEnvRejectsPrevCall() { }, Cursor::prev); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -176,7 +176,7 @@ void closedEnvRejectsDeleteCall() { }, Cursor::delete); }) - .isInstanceOf(AlreadyClosedException.class); + .isInstanceOf(Env.EnvInUseException.class); } @Test @@ -224,6 +224,7 @@ void countWithoutDupsort() { } } + @Disabled // close() method changed to only do the mdb_cursor_close call if in the right txn state @Test void cursorCannotCloseIfTransactionCommitted() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java new file mode 100644 index 00000000..2a975bc6 --- /dev/null +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -0,0 +1,175 @@ +package org.lmdbjava; + + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Objects; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +public class RefCounterTest { + + private final int iterations = 1_000_000; + private final int threadCount = Runtime.getRuntime().availableProcessors(); + private volatile Object env = new Object(); + + @Test + public void perfTest() { + IntStream.of(1, 2, 5, 10, 12, 14, 16, 18, 20, 22, threadCount, threadCount * 2, threadCount * 4) + .forEach(this::runTest); + } + + private void runTest(final int stripes) { +// System.out.println("Running test for " + stripes + " stripes"); + + final AtomicReference startTime = new AtomicReference<>(null); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(stripes, this::onClose); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + for (int i = 0; i < threadCount; i++) { + futures[i] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + + // Capture the start time + startTime.updateAndGet(currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + try { + // Make sure we have an env that is not 'closed' + Objects.requireNonNull(env); + } finally { + releaser.release(); + } + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + CompletableFuture.allOf(futures).join(); + + if (refCounter.getTotalCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getTotalCount()); + } + + final Duration duration = Duration.between(startTime.get(), Instant.now()); + final double iterationsPerSec = (double) iterations / duration.toMillis() * 1000; + + System.out.println("All Finished" + + ", stripes: " + stripes + + ", threads: " + threadCount + + ", duration: " + duration + + ", iterationsPerSec: " + iterationsPerSec); + } + + @Test + void testBehaviour() throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int rounds = 50; + final int iterations = 100_000_000; + + for (int k = 0; k < rounds; k++) { + final int round = k; + System.out.printf("Round %s ----------------------------------------%n", round); + + // Reset the env + env = new Object(); + final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final long[] counts = new long[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); +// System.out.println(Thread.currentThread() + " - Starting"); + + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx]++; + } catch (Env.AlreadyClosedException e) { +// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + Thread.sleep(random.nextInt(1)); + // env is null after closure + Objects.requireNonNull(env, "Attempt to use a null env"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + releaser.release(); + } + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + Thread.sleep(100 + random.nextInt(200)); + + while (true) { + try { + refCounter.close(); + break; + } catch (Env.EnvInUseException e) { + Thread.sleep(100); + } + } + + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); + + if (refCounter.getTotalCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getTotalCount()); + } + + if (!refCounter.isClosed()) { + throw new IllegalStateException("Env not closed"); + } + } + } + + private void countDownThenAwait(final CountDownLatch latch) { + latch.countDown(); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + private void onClose() { + System.out.println(Thread.currentThread() + " - Starting onClose runnable"); + env = null; + System.out.println(Thread.currentThread() + " - Finishing onClose runnable"); + } + +} From 2a0a6c030514938ffc12930ba6dc191714485f29 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:25:25 +0000 Subject: [PATCH 02/61] Tidy code --- .../org/lmdbjava/StripedRefCounterImpl.java | 142 ++---------------- 1 file changed, 13 insertions(+), 129 deletions(-) diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java index 2ef4b867..3e9f0b6d 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java @@ -19,9 +19,7 @@ class StripedRefCounterImpl implements RefCounter { * Flag to indicate if {@link RefCounter#close()} has been called. * Once set, it means that all subsequent {@link RefCounter#acquire()} will throw. */ -// private final AtomicBoolean closeCalled = new AtomicBoolean(false); private final Runnable onClose; -// private final AtomicBoolean onCloseCompleted = new AtomicBoolean(false); private final AtomicReference stateRef; StripedRefCounterImpl(Runnable onClose) { @@ -44,9 +42,7 @@ class StripedRefCounterImpl implements RefCounter { @Override public RefCounterReleaser acquire() { -// return doWithOptionalLocking(() -> { if (stateRef.get() != EnvState.OPEN) { -// if (closeCalled.get()) { // Close has been initiated, so acquire() is no longer allowed // System.out.println("Throwing AlreadyClosedException 1"); throw new Env.AlreadyClosedException(); @@ -58,14 +54,9 @@ public RefCounterReleaser acquire() { final StripeState stripeState = stripeStates[getStripeIdx()]; // If we increment the count just before counter is made negative, then close() // will have to wait for us to release. - final int count = stripeState.counter.updateAndGet(currVal -> { - final int newVal; - if (currVal < 0) { - // Negative means it is in a closed state -// System.out.println("Throwing AlreadyClosedException 2"); -// throw new Env.AlreadyClosedException(); - newVal = currVal; - } else { + final int newCount = stripeState.counter.updateAndGet(currVal -> { + int newVal = currVal; + if (currVal >= 0) { newVal = currVal + 1; // System.out.printf("%s - acquire() called, currVal: %s, newVal: %s%n", // Thread.currentThread(), currVal, newVal); @@ -77,12 +68,11 @@ public RefCounterReleaser acquire() { return newVal; }); - if (count < 0) { + if (newCount < 0) { throw new Env.AlreadyClosedException(); } - // Return the counter index, so the release call can use it + // Return the releaser than knows which stripe to release back to return new RefCounterReleaserImpl(this, stripeState); -// }); } @Override @@ -98,7 +88,6 @@ public void use(final Runnable runnable) { } private void release(final StripeState stripeState) { -// doWithOptionalLocking(() -> { stripeState.counter.updateAndGet(currVal -> { int newVal; if (currVal > 0) { @@ -116,31 +105,19 @@ private void release(final StripeState stripeState) { // System.out.printf("%s - release() called, currVal: %s, newVal: %s%n", // Thread.currentThread(), currVal, newVal); if (newVal == 0) { - // Reached zero, so set to the magic number + // Reached zero, so set to the magic number, so counter stays negative, i.e. closed newVal = CLOSED_COUNT; - // We have reached zero, so count down the latch so that close() can stop blocking -// stripeState.countDownLatch.countDown(); } } else { - // currVal == 0 throw new IllegalStateException("currVal should never be zero on release()"); } return newVal; }); -// return null; -// }); } -// @Override -// public void release(final int counterIdx) { -// final StripeState stripeState = stripeStates[counterIdx]; -// release(stripeState); -// } - - private void markClosing() { + private void setCountersInClosingState() { // Only want to do this once if (stateRef.compareAndSet(EnvState.OPEN, EnvState.CLOSING)) { -// if (closeCalled.compareAndSet(false, true)) { // System.out.println("close() called"); // Place each stripe into a closed state for (int stripe = 0; stripe < stripes; stripe++) { @@ -148,7 +125,6 @@ private void markClosing() { stripeState.counter.updateAndGet(currVal -> { if (currVal == 0) { // Count is already at zero so there will be nothing to wait for. -// stripeState.countDownLatch.countDown(); // Ensures any thread that tries to increment will see it as closed return CLOSED_COUNT; } else if (currVal > 0) { @@ -168,14 +144,14 @@ private void markClosing() { @Override public void close() { - markClosing(); + // First ensure all counters are marked as closing to stop any new acquire calls + setCountersInClosingState(); -// if (!onCloseCompleted.get()) { + // At this point, no new acquire calls are possible final EnvState envState = stateRef.get(); if (envState == EnvState.CLOSING) { - // TODO This will mark as closed, but then throw if it is still in use, which is not - // ideal. It ought to throw before marking closed if in use. + // If any counter is negative then there are still release() calls outstanding for (int stripe = 0; stripe < stripes; stripe++) { final StripeState stripeState = stripeStates[stripe]; final int count = stripeState.counter.get(); @@ -191,67 +167,6 @@ public void close() { } } -// @Override -// public void close(long duration, TimeUnit timeUnit) { -// markClosing(); -// -//// if (!onCloseCompleted.get()) { -// final EnvState envState = stateRef.get(); -// if (envState == EnvState.CLOSING) { -// Duration totalWaitTime = Duration.ZERO; -// // Now wait for all active threads to finish -// for (int stripe = 0; stripe < stripes; stripe++) { -// final Instant stripeStartTime = Instant.now(); -// final StripeState stripeState = stripeStates[stripe]; -// final AtomicInteger counter = stripeState.counter; -// final CountDownLatch latch = stripeState.countDownLatch; -// -// // By this point all counters will be negative, so we need to wait for them ALL to reach 0, -// // except the ones with the magic value of CLOSED_COUNT -// -// int count = counter.get(); -// if (count < 0 && count != CLOSED_COUNT) { -// // Non-zero count so we must wait for it to hit 0 -//// System.out.printf("%s - Waiting for closure, stripe: %s, count: %d, latch: %s%n", -//// Thread.currentThread(), stripe, count, latch.getCount()); -// try { -// // Release will count down when it hits zero -// final long latchCount = latch.getCount(); -// Instant now = Instant.now(); -// final boolean didCountDown = latch.await(duration, timeUnit); -// if (!didCountDown) { -// throw new Env.EnvInUseException(); -// } -// count = counter.get(); -// if (counter.get() != 0) { -// throw new IllegalStateException("count " + count -// + " should be zero at this point, latchCount: " + latchCount + ", new latchCount: " + latch.getCount() -// + ", waited: " + Duration.between(now, Instant.now())); -// } -// } catch (InterruptedException e) { -// Thread.currentThread().interrupt(); -// // Swallow -// } -// } else if (count > 0) { -// throw new IllegalStateException("count " + count + " should not positive"); -// } -// -// totalWaitTime = totalWaitTime.plus(Duration.between(stripeStartTime, Instant.now())); -// } -//// System.out.printf("%s - Wait complete, waited %s, count: %s%n", -//// Thread.currentThread(), totalWaitTime, getTotalCount()); -// -// // All counters returned to zero -// onClose.run(); -// stateRef.set(EnvState.CLOSED); -// onCloseCompleted.set(true); -//// System.out.printf("%s - onClose completed, waited %s, count: %s%n", -//// Thread.currentThread(), totalWaitTime, getTotalCount()); -// } else if (envState == EnvState.OPEN) { -// throw new IllegalStateException("EnvState should not be OPEN at this point"); -// } -// } - private int getStripeIdx() { // TODO In >= Java19, getId() is deprecated, so change to .threadId() final long idx = Thread.currentThread().getId() % stripes; @@ -260,13 +175,9 @@ private int getStripeIdx() { @Override public boolean isClosed() { - // TODO May need a three state approach, i.e. - // OPEN - Open and ready for use. - // CLOSING - Not yet closed, but - // CLOSED - // close() has been called, but it may not be fully closed yet, i.e. the close // action may not have run/finished. -// return closeCalled.get(); + // TODO should it return ==CLOSED or !=OPEN ? return stateRef.get() == EnvState.CLOSED; } @@ -277,7 +188,7 @@ public EnvState getState() { @Override public void checkNotClosed() { -// if (closeCalled.get()) { + // TODO should it return ==CLOSED or !=OPEN ? if (stateRef.get() != EnvState.OPEN) { throw new Env.AlreadyClosedException(); } @@ -302,12 +213,10 @@ private static class StripeState { /** * One latch per stripe. Each will start with a value of 1 */ -// private final CountDownLatch countDownLatch; private StripeState(final int index) { this.index = index; this.counter = new AtomicInteger(0); -// this.countDownLatch = new CountDownLatch(1); } int getIndex() { @@ -318,40 +227,15 @@ AtomicInteger getCounter() { return counter; } -// CountDownLatch getCountDownLatch() { -// return countDownLatch; -// } - @Override public String toString() { return "Stripe{" + "index=" + index + ", counter=" + counter + -// ", countDownLatch=" + countDownLatch + '}'; } } -// private T doWithOptionalLocking(final Supplier supplier) { -// final State state = stateRef.get(); -// if (state == State.CLOSED) { -// throw new Env.AlreadyClosedException(); -// } else if (state == State.OPEN) { -// return supplier.get(); -// } else { -// synchronized (this) { -// final State state2 = stateRef.get(); -// if (state2 == State.CLOSED) { -// throw new Env.AlreadyClosedException(); -// } else if (state2 == State.OPEN) { -// return supplier.get(); -// } else { -// throw new IllegalStateException("Should not be in a sate of CLOSING with the lock held"); -// } -// } -// } -// } - private static class RefCounterReleaserImpl implements RefCounterReleaser { private final AtomicReference refCounterRef; From 81e8ae7e6b6f411bd93f10ed11d3237b883e91a9 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:05:08 +0000 Subject: [PATCH 03/61] Change checkNotClosed to allow things to close down in CLOSING state --- src/main/java/org/lmdbjava/Env.java | 4 ++++ .../java/org/lmdbjava/NoOpRefCounter.java | 10 ++++---- src/main/java/org/lmdbjava/RefCounter.java | 18 +++++++-------- .../lmdbjava/SingleThreadedRefCounter.java | 23 ++++++++++--------- .../org/lmdbjava/StripedRefCounterImpl.java | 12 ++++++---- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 4f80b8c3..e08e8ed3 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -598,6 +598,10 @@ void checkNotClosed() { refCounter.checkNotClosed(); } + void checkOpen() { + refCounter.checkOpen(); + } + private void validateDirectoryEmpty(final Path path) { if (!Files.exists(path)) { throw new InvalidCopyDestination("Path does not exist"); diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index b3cb1fd5..6f6bb31e 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -18,11 +18,6 @@ public void close() { // no-op } -// @Override -// public void close(long duration, TimeUnit timeUnit) { -// // no-op -// } - @Override public boolean isClosed() { return false; @@ -37,4 +32,9 @@ public EnvState getState() { public void checkNotClosed() { // no-op } + + @Override + public void checkOpen() { + // no-op + } } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index b189dfda..d38cdd14 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -1,6 +1,5 @@ package org.lmdbjava; - /** * Used to prevent the closure of a thing while other threads are actively * using that thing. @@ -18,11 +17,6 @@ interface RefCounter { */ void use(final Runnable runnable); - /** - * @param counterIdx - */ -// void release(int counterIdx); - /** * Closes the {@link RefCounter} controlled item, but only after ensuring all active users of * it have released. Once {@link RefCounter#close()} is called, all subsequent calls to @@ -32,17 +26,23 @@ interface RefCounter { */ void close(); -// void close(final long duration, final TimeUnit timeUnit); - /** - * @return True if {@link RefCounter#close()} has been called. The actual close may not have completed though. + * @return True if {@link RefCounter} is in a state of {@link EnvState#CLOSED} */ boolean isClosed(); EnvState getState(); + /** + * If it is in a CLOSED state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} + */ void checkNotClosed(); + /** + * If it is not in an OPEN state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} + */ + void checkOpen(); + @FunctionalInterface interface RefCounterReleaser { diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index e131a9cc..a2e69203 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -6,8 +6,6 @@ public class SingleThreadedRefCounter implements RefCounter { private final Runnable onClose; - private boolean closeCalled = false; - private boolean onCloseCompleted = false; private int refCount; private EnvState envState; @@ -43,24 +41,20 @@ public void use(Runnable runnable) { @Override public void close() { - envState = EnvState.CLOSING; -// closeCalled = true; + if (envState == EnvState.OPEN) { + envState = EnvState.CLOSING; + } + if (refCount > 0) { throw new Env.EnvInUseException(); } -// if (!onCloseCompleted) { + if (envState == EnvState.CLOSING) { onClose.run(); envState = EnvState.CLOSED; -// onCloseCompleted = true; } } -// @Override -// public void close(long duration, TimeUnit timeUnit) { -// throw new UnsupportedOperationException("Method not supported for single threaded use."); -// } - @Override public boolean isClosed() { return envState == EnvState.CLOSED; @@ -73,6 +67,13 @@ public EnvState getState() { @Override public void checkNotClosed() { + if (envState == EnvState.CLOSED) { + throw new Env.AlreadyClosedException(); + } + } + + @Override + public void checkOpen() { if (envState != EnvState.OPEN) { throw new Env.AlreadyClosedException(); } diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java index 3e9f0b6d..f2b7c6a5 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java @@ -175,9 +175,6 @@ private int getStripeIdx() { @Override public boolean isClosed() { - // close() has been called, but it may not be fully closed yet, i.e. the close - // action may not have run/finished. - // TODO should it return ==CLOSED or !=OPEN ? return stateRef.get() == EnvState.CLOSED; } @@ -189,7 +186,14 @@ public EnvState getState() { @Override public void checkNotClosed() { // TODO should it return ==CLOSED or !=OPEN ? - if (stateRef.get() != EnvState.OPEN) { + if (stateRef.get() == EnvState.CLOSED) { + throw new Env.AlreadyClosedException(); + } + } + + @Override + public void checkOpen() { + if (stateRef.get() != EnvState.OPEN) { throw new Env.AlreadyClosedException(); } } From 5a4585e652ff933e02b4ec2f79299645bc8993c3 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:15:10 +0000 Subject: [PATCH 04/61] Fix acquire behaviour in Txn/Cursor ctors --- src/main/java/org/lmdbjava/Cursor.java | 8 +++++++- src/main/java/org/lmdbjava/Txn.java | 16 +++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 1471ab6f..4036d66b 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -59,9 +59,15 @@ public final class Cursor implements AutoCloseable { // The env needs to track open cursors to prevent env closure before the cursors are closed System.out.println("Acquiring for cursor"); this.refCounterReleaser = env.acquire(); - this.kv = txn.newKeyVal(); this.env = env; this.closed = new AtomicBoolean(false); + try { + this.kv = txn.newKeyVal(); + } catch (final Exception e) { + this.refCounterReleaser.release(); + closed.set(false); + throw e; + } } /** diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 3985a9a3..b3a1eeb2 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -65,14 +65,20 @@ public final class Txn implements AutoCloseable { if (parent != null && parent.isReadOnly() != this.readOnly) { throw new IncompatibleParent(); } - final Pointer txnPtr = allocateDirect(RUNTIME, ADDRESS); - final Pointer txnParentPtr = parent == null ? null : parent.ptr; - checkRc(LIB.mdb_txn_begin(env.pointer(), txnParentPtr, flags.getMask(), txnPtr)); - ptr = txnPtr.getPointer(0); - state = READY; System.out.println("Acquiring for txn"); this.refCounterReleaser = env.acquire(); + try { + final Pointer txnPtr = allocateDirect(RUNTIME, ADDRESS); + final Pointer txnParentPtr = parent == null ? null : parent.ptr; + checkRc(LIB.mdb_txn_begin(env.pointer(), txnParentPtr, flags.getMask(), txnPtr)); + ptr = txnPtr.getPointer(0); + + state = READY; + } catch (final Exception e) { + this.refCounterReleaser.release(); + throw e; + } } /** Aborts this transaction. */ From a7a99c9df85c9f42fd21a3747d9fe1a6bcc26c54 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:29:03 +0000 Subject: [PATCH 05/61] Add onClose to NoOpRefCounter --- src/main/java/org/lmdbjava/Env.java | 2 +- .../java/org/lmdbjava/NoOpRefCounter.java | 10 +++- .../java/org/lmdbjava/RefCounterTest.java | 57 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index e08e8ed3..8263ebd9 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -103,7 +103,7 @@ private Env( this.refCounter = new StripedRefCounterImpl(this::closeMdbEnv); } } else { - this.refCounter = new NoOpRefCounter(); + this.refCounter = new NoOpRefCounter(this::closeMdbEnv); } } diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index 6f6bb31e..dea888e6 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -1,8 +1,16 @@ package org.lmdbjava; +import java.util.Objects; + public class NoOpRefCounter implements RefCounter { + private final Runnable onClose; + + public NoOpRefCounter(final Runnable onClose) { + this.onClose = Objects.requireNonNull(onClose); + } + @Override public RefCounterReleaser acquire() { return RefCounterReleaser.NO_OP_RELEASER; @@ -15,7 +23,7 @@ public void use(Runnable runnable) { @Override public void close() { - // no-op + onClose.run(); } @Override diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 2a975bc6..2abc1ccb 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -26,6 +26,63 @@ public void perfTest() { .forEach(this::runTest); } + @Test + public void noOpRefCounter() { + System.setProperty(Env.DISABLE_CHECKS_PROP, "true"); + try { + for (int i = 0; i < 20; i++) { + doNoOpRefCounter(); + } + } finally { + System.clearProperty(Env.DISABLE_CHECKS_PROP); + } + } + + private void doNoOpRefCounter() { +// System.out.println("Running test for " + stripes + " stripes"); + + final AtomicReference startTime = new AtomicReference<>(null); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final NoOpRefCounter refCounter = new NoOpRefCounter(this::onClose); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + for (int i = 0; i < threadCount; i++) { + futures[i] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + + // Capture the start time + startTime.updateAndGet(currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + try { + // Make sure we have an env that is not 'closed' + Objects.requireNonNull(env); + } finally { + releaser.release(); + } + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + CompletableFuture.allOf(futures).join(); + + final Duration duration = Duration.between(startTime.get(), Instant.now()); + final double iterationsPerSec = (double) iterations / duration.toMillis() * 1000; + + System.out.println("All Finished" + + ", threads: " + threadCount + + ", duration: " + duration + + ", iterationsPerSec: " + iterationsPerSec); + } + private void runTest(final int stripes) { // System.out.println("Running test for " + stripes + " stripes"); From d02f01baee22b794b426aff8381e66fbe4533d9e Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:28:30 +0000 Subject: [PATCH 06/61] Tweak perf test --- src/test/java/org/lmdbjava/RefCounterTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 2abc1ccb..57d65ac0 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -22,19 +22,19 @@ public class RefCounterTest { @Test public void perfTest() { - IntStream.of(1, 2, 5, 10, 12, 14, 16, 18, 20, 22, threadCount, threadCount * 2, threadCount * 4) - .forEach(this::runTest); + // Do multiple rounds to let it warm up + for (int i = 1; i <= 3; i++) { + System.out.println("Round: " + i); + IntStream.of(1, 2, 5, 10, 12, 14, 16, 18, 20, 22, threadCount, threadCount * 2, threadCount * 4) + .forEach(this::runTest); + } } @Test public void noOpRefCounter() { - System.setProperty(Env.DISABLE_CHECKS_PROP, "true"); - try { - for (int i = 0; i < 20; i++) { - doNoOpRefCounter(); - } - } finally { - System.clearProperty(Env.DISABLE_CHECKS_PROP); + // Do multiple rounds to let it warm up + for (int i = 0; i < 20; i++) { + doNoOpRefCounter(); } } From 7b83648099c875c7a16b52afd7b9dcccbe0ddd6e Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:20:08 +0000 Subject: [PATCH 07/61] Improve stripe hashing with golden ratio --- src/main/java/org/lmdbjava/StripedRefCounterImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java index f2b7c6a5..5ed9d18a 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java @@ -9,6 +9,9 @@ class StripedRefCounterImpl implements RefCounter { private static final int CLOSED_COUNT = Integer.MIN_VALUE; + // Golden Ratio constant used for better hash scattering + // See https://softwareengineering.stackexchange.com/a/402543 + private static final long GOLDEN_RATIO = 0x9e3779b9L; /** * Number of stripes used to improve the concurrency @@ -169,7 +172,7 @@ public void close() { private int getStripeIdx() { // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long idx = Thread.currentThread().getId() % stripes; + final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; return (int) idx; } From 301df13793eb06415b47669c78e5b8e838b1a233 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:15:16 +0000 Subject: [PATCH 08/61] Add StampedLockRefCounterImpl WIP --- src/main/java/org/lmdbjava/AtomicBitSet.java | 138 ++++++++++++++++++ src/main/java/org/lmdbjava/Guard.java | 94 ++++++++++++ .../java/org/lmdbjava/NoOpRefCounter.java | 5 + src/main/java/org/lmdbjava/RefCounter.java | 2 + .../lmdbjava/SingleThreadedRefCounter.java | 5 + .../lmdbjava/StampedLockRefCounterImpl.java | 135 +++++++++++++++++ .../org/lmdbjava/StripedRefCounterImpl.java | 58 ++++++-- .../java/org/lmdbjava/RefCounterTest.java | 117 +++++++++++++-- 8 files changed, 531 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/lmdbjava/AtomicBitSet.java create mode 100644 src/main/java/org/lmdbjava/Guard.java create mode 100644 src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java diff --git a/src/main/java/org/lmdbjava/AtomicBitSet.java b/src/main/java/org/lmdbjava/AtomicBitSet.java new file mode 100644 index 00000000..daf82508 --- /dev/null +++ b/src/main/java/org/lmdbjava/AtomicBitSet.java @@ -0,0 +1,138 @@ +package org.lmdbjava; + + +import java.util.concurrent.atomic.AtomicLong; + +public class AtomicBitSet { + private static final int MAX_SIZE = (Long.BYTES * 8); + + private final int size; + private final int maxIdx; + private final AtomicLong atomicLong = new AtomicLong(0); + + public AtomicBitSet() { + this(MAX_SIZE); + } + + public AtomicBitSet(int size) { + if (size < 0 || size > MAX_SIZE) { + throw new IllegalArgumentException("size must be between 0 and " + MAX_SIZE + " (inclusive)"); + } + this.maxIdx = size - 1; + this.size = size; + } + + public boolean flip(int idx) { + checkIdx(idx); + final long newVal = atomicLong.accumulateAndGet(idx, (currVal, idx2) -> + currVal ^ (1L << idx2)); + return isSetWithNoCheck(newVal, idx); + } + + /** + * Set the bit at position idx and return the resulting bit set as a long. + */ + public long setAndGet(int idx) { + checkIdx(idx); + return atomicLong.accumulateAndGet(idx, (currVal, idx2) -> + currVal | (1L << idx2)); + } + + /** + * Sets the bit at position idx + * + * @return The previous value of the set as a long. + */ + public long getAndSet(int idx) { + checkIdx(idx); + return atomicLong.getAndAccumulate(idx, (currVal, idx2) -> + currVal | (1L << idx2)); + } + + /** + * Un-set the bit at position idx and return the resulting bit set as a long. + */ + public long unset(int idx) { + checkIdx(idx); + return atomicLong.updateAndGet(currVal -> + currVal & ~(1L << idx)); + } + + /** + * Set/un-set the bit at position idx, according to the value of isSet, + * and return the resulting bit set as a long. + */ + public long setAndGet(int idx, final boolean isSet) { + return isSet + ? setAndGet(idx) + : unset(idx); + } + + /** + * @return True if the bit at position idx is set. + */ + public boolean isSet(final int idx) { + checkIdx(idx); + return isSetWithNoCheck(atomicLong.get(), idx); + } + + /** + * @return The number of bits that have been set. + */ + public int countSet() { + return Long.bitCount(atomicLong.get()); + } + + public int countSet(final long val) { + return Long.bitCount(val); + } + + /** + * @return The number of bits that are un-set. + */ + public int countUnSet() { + return size - Long.bitCount(atomicLong.get()); + } + + public int countUnSet(final long val) { + return size - Long.bitCount(val); + } + + public void unSetAll() { + atomicLong.set(0L); + } + + public void setAll() { + if (size == MAX_SIZE) { + atomicLong.set(-1L); + } else { + for (int i = 0; i < size; i++) { + setAndGet(i); + } + } + } + + public long asLong() { + return atomicLong.get(); + } + + public boolean isSet(final long val, final int idx) { + checkIdx(idx); + return isSetWithNoCheck(val, idx); + } + + private static boolean isSetWithNoCheck(final long val, final int idx) { + return ((val >> idx) & 1L) != 0L; + } + + private void checkIdx(final int idx) { + if (idx < 0 || idx > maxIdx) { + throw new IllegalArgumentException("idx must be between 0 and " + maxIdx + " (inclusive)"); + } + } + + @Override + public String toString() { + return Long.toBinaryString(atomicLong.get()); + } +} diff --git a/src/main/java/org/lmdbjava/Guard.java b/src/main/java/org/lmdbjava/Guard.java new file mode 100644 index 00000000..82fdd8c6 --- /dev/null +++ b/src/main/java/org/lmdbjava/Guard.java @@ -0,0 +1,94 @@ +package org.lmdbjava; + + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +class Guard { + private static final long GOLDEN_RATIO = 0x9e3779b9L; + + private final int stripes; + private final AtomicInteger[] inUseCounts; + private final AtomicBoolean destroy = new AtomicBoolean(); + // private final AtomicBoolean destroyed = new AtomicBoolean(); + private final AtomicBitSet bitSet; + private final Runnable destroyRunnable; + + public Guard(final Runnable destroyRunnable, final int stripes) { + this.stripes = stripes; + this.destroyRunnable = destroyRunnable; + this.inUseCounts = new AtomicInteger[stripes]; + this.bitSet = new AtomicBitSet(stripes); + for (int i = 0; i < stripes; i++) { + inUseCounts[i] = new AtomicInteger(1); + } + } + + private int getStripeIdx() { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; + return (int) idx; + } + + public R acquire(final Supplier supplier) { + // Increment if greater than 0. + final int c = inUseCounts[getStripeIdx()].updateAndGet(count -> count > 0 + ? count + 1 + : count); + if (c <= 0) { + // The destroy flag may not have been set when we entered this method but count == 0 means destruction + // has been triggered since then. + throw new RuntimeException("Try again"); + } + + try { + return supplier.get(); + } finally { + release(); + } + } + + private void release() { + // Decrement but don't go lower than 0. + final int stripeIdx = getStripeIdx(); + release(stripeIdx); + } + + private void release(final int stripeIdx) { + // Decrement but don't go lower than 0. + final int newCount = inUseCounts[stripeIdx].updateAndGet(count -> { + if (count > 0) { + return count - 1; + } else if (count < 0) { + return count + 1; + } else { + return count; + } + }); + + if (newCount == 0) { + if (!bitSet.isSet(stripeIdx)) { + final long prevVal = bitSet.getAndSet(stripeIdx); + final boolean didChange = !bitSet.isSet(prevVal, stripeIdx); + if (didChange) { + if (bitSet.countUnSet(prevVal) == 1) { + destroyRunnable.run(); + } + } + } + } + } + + public void destroy() { + if (destroy.compareAndSet(false, true)) { + // Perform final decrement. Close is either performed now if the guard is not acquired or will be + // performed by the final thread that releases the acquisition. + for (int stripeIdx = 0; stripeIdx < stripes; stripeIdx++) { + release(stripeIdx); + } + } else { +// LOGGER.debug("Guard already destroyed"); + } + } +} diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index dea888e6..a36b94f2 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -45,4 +45,9 @@ public void checkNotClosed() { public void checkOpen() { // no-op } + + @Override + public int getCount() { + return 0; + } } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index d38cdd14..e3197868 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -43,6 +43,8 @@ interface RefCounter { */ void checkOpen(); + int getCount(); + @FunctionalInterface interface RefCounterReleaser { diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index a2e69203..39509037 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -79,6 +79,11 @@ public void checkOpen() { } } + @Override + public int getCount() { + return refCount; + } + private static class SingleThreadedReleaser implements RefCounterReleaser { private final SingleThreadedRefCounter singleThreadedRefCounter; diff --git a/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java b/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java new file mode 100644 index 00000000..604d0a50 --- /dev/null +++ b/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java @@ -0,0 +1,135 @@ +package org.lmdbjava; + + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Supplier; + +class StampedLockRefCounterImpl implements RefCounter { + private static final long GOLDEN_RATIO = 0x9e3779b9L; + + private final StampedLock stampedLock; + private final int stripes; + private final AtomicInteger[] counters; + + public StampedLockRefCounterImpl(final int stripes) { + this.stampedLock = new StampedLock(); + this.stripes = stripes; + this.counters = new AtomicInteger[stripes]; + for (int i = 0; i < stripes; i++) { + counters[i] = new AtomicInteger(0); + } + } + + private int getStripeIdx() { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; + return (int) idx; + } + + @Override + public void use(Runnable runnable) { + acquire(); + try { + runnable.run(); + } finally { + release(); + } + } + + @Override + public void close() { + + } + + @Override + public boolean isClosed() { + return false; + } + + @Override + public EnvState getState() { + return null; + } + + @Override + public void checkNotClosed() { + + } + + @Override + public void checkOpen() { + + } + + public R acquire(final Supplier supplier) { + acquire(); + try { + return supplier.get(); + } finally { + release(); + } + } + + private void release() { + final AtomicInteger counter = counters[getStripeIdx()]; + release(counter); + } + + public RefCounterReleaser acquire() { + final long optimisticLockStamp = stampedLock.tryOptimisticRead(); + final int stripeIdx = getStripeIdx(); + // Increment if greater than 0. + final AtomicInteger counter = counters[stripeIdx]; + counter.incrementAndGet(); + + final boolean success = stampedLock.validate(optimisticLockStamp); + if (!success) { + // Undo incrementAndGet + counter.decrementAndGet(); + + // Now repeat under lock + final long readLockStamp = stampedLock.readLock(); + try { + counter.incrementAndGet(); + } finally { + stampedLock.unlockRead(readLockStamp); + } + } + return () -> release(counter); + } + + private void release(final AtomicInteger counter) { + final long optimisticLockStamp = stampedLock.tryOptimisticRead(); + + // Increment if greater than 0. + counter.decrementAndGet(); + + final boolean success = stampedLock.validate(optimisticLockStamp); + if (!success) { + // Undo decrementAndGet + counter.incrementAndGet(); + + // Now repeat under lock + final long readLockStamp = stampedLock.readLock(); + try { + counter.decrementAndGet(); + } finally { + stampedLock.unlockRead(readLockStamp); + } + } + } + + public int getCount() { + final long writeLockStamp = stampedLock.writeLock(); + try { + int count = 0; + for (int i = 0; i < stripes; i++) { + count += counters[i].get(); + } + return count; + } finally { + stampedLock.unlockWrite(writeLockStamp); + } + } +} diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java index 5ed9d18a..1bd1c304 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java @@ -18,6 +18,7 @@ class StripedRefCounterImpl implements RefCounter { */ private final int stripes; private final StripeState[] stripeStates; + private final AtomicBitSet closedStripesBitSet; /** * Flag to indicate if {@link RefCounter#close()} has been called. * Once set, it means that all subsequent {@link RefCounter#acquire()} will throw. @@ -37,7 +38,8 @@ class StripedRefCounterImpl implements RefCounter { } this.stripeStates = new StripeState[stripes]; this.onClose = requireNonNull(onClose); - this.stateRef = new AtomicReference<>(EnvState.OPEN); + this.stateRef = new AtomicReference<>(EnvState.OPEN); + this.closedStripesBitSet = new AtomicBitSet(stripes); for (int stripeIdx = 0; stripeIdx < stripes; stripeIdx++) { stripeStates[stripeIdx] = new StripeState(stripeIdx); } @@ -72,7 +74,7 @@ public RefCounterReleaser acquire() { }); if (newCount < 0) { - throw new Env.AlreadyClosedException(); + throw new Env.AlreadyClosedException(); } // Return the releaser than knows which stripe to release back to return new RefCounterReleaserImpl(this, stripeState); @@ -91,7 +93,7 @@ public void use(final Runnable runnable) { } private void release(final StripeState stripeState) { - stripeState.counter.updateAndGet(currVal -> { + final int count = stripeState.counter.updateAndGet(currVal -> { int newVal; if (currVal > 0) { // Positive count, so in an open state, therefore -1 back down towards zero @@ -116,16 +118,33 @@ private void release(final StripeState stripeState) { } return newVal; }); + +// if (count == CLOSED_COUNT) { +// markStripeAsClosed(stripeState); +// } + } + + private void markStripeAsClosed(final StripeState stripeState) { + // Mark this stripe as closed + final int idx = stripeState.index; + final long prevVal = closedStripesBitSet.getAndSet(idx); + final boolean didChange = !closedStripesBitSet.isSet(prevVal, idx); + if (didChange) { + if (closedStripesBitSet.countUnSet(prevVal) == 1) { + // We closed the last one + } + } } - private void setCountersInClosingState() { + private boolean setCountersInClosingState() { // Only want to do this once - if (stateRef.compareAndSet(EnvState.OPEN, EnvState.CLOSING)) { + final boolean didChange = stateRef.compareAndSet(EnvState.OPEN, EnvState.CLOSING); + if (didChange) { // System.out.println("close() called"); // Place each stripe into a closed state for (int stripe = 0; stripe < stripes; stripe++) { final StripeState stripeState = stripeStates[stripe]; - stripeState.counter.updateAndGet(currVal -> { + final int count = stripeState.counter.updateAndGet(currVal -> { if (currVal == 0) { // Count is already at zero so there will be nothing to wait for. // Ensures any thread that tries to increment will see it as closed @@ -141,32 +160,40 @@ private void setCountersInClosingState() { throw new IllegalStateException("currVal should not be zero on close()"); } }); + +// if (count == CLOSED_COUNT) { +// markStripeAsClosed(stripeState); +// } } } + return didChange; } @Override public void close() { // First ensure all counters are marked as closing to stop any new acquire calls - setCountersInClosingState(); + final boolean didChange = setCountersInClosingState(); // At this point, no new acquire calls are possible - final EnvState envState = stateRef.get(); - if (envState == EnvState.CLOSING) { + if (didChange) { +// closedStripesArray. // If any counter is negative then there are still release() calls outstanding for (int stripe = 0; stripe < stripes; stripe++) { final StripeState stripeState = stripeStates[stripe]; final int count = stripeState.counter.get(); if (count < 0 && count != CLOSED_COUNT) { - throw new Env.EnvInUseException(getTotalCount()); + throw new Env.EnvInUseException(getCount()); } } onClose.run(); stateRef.set(EnvState.CLOSED); - } else if (envState == EnvState.OPEN) { - throw new IllegalStateException("EnvState should not be OPEN at this point"); + } else { + final EnvState envState = stateRef.get(); + if (envState == EnvState.OPEN) { + throw new IllegalStateException("EnvState should not be OPEN at this point"); + } } } @@ -204,7 +231,8 @@ public void checkOpen() { /** * @return The total number of active users. Not atomic. */ - int getTotalCount() { + @Override + public int getCount() { return Arrays.stream(stripeStates) .map(StripeState::getCounter) .mapToInt(AtomicInteger::get) @@ -217,6 +245,7 @@ private static class StripeState { private final int index; private final AtomicInteger counter; + /** * One latch per stripe. Each will start with a value of 1 */ @@ -259,8 +288,9 @@ public void release() { // Prevent duplicate release calls final StripedRefCounterImpl refCounter = refCounterRef.getAndSet(null); if (refCounter != null) { - refCounter.release(stripeState); + refCounter.release(stripeState); } } } + } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 57d65ac0..f1a4927d 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,6 +1,8 @@ package org.lmdbjava; +import static org.assertj.core.api.Assertions.assertThat; + import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -24,9 +26,14 @@ public class RefCounterTest { public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { - System.out.println("Round: " + i); - IntStream.of(1, 2, 5, 10, 12, 14, 16, 18, 20, 22, threadCount, threadCount * 2, threadCount * 4) - .forEach(this::runTest); + System.out.println("Round: " + i + " StripedRefCounterImpl"); + + IntStream.of(1, 2, 4, 8, threadCount, threadCount * 2) + .forEach(stripes -> runTest(stripes, new StripedRefCounterImpl(stripes, this::onClose))); + + System.out.println("Round: " + i + " StripedCounter"); + IntStream.of(1, 2, 4, 8, threadCount, threadCount * 2) + .forEach(stripes -> runTest(stripes, new StampedLockRefCounterImpl(stripes))); } } @@ -83,12 +90,12 @@ private void doNoOpRefCounter() { + ", iterationsPerSec: " + iterationsPerSec); } - private void runTest(final int stripes) { + private void runTest(int stripes, final RefCounter refCounter) { // System.out.println("Running test for " + stripes + " stripes"); final AtomicReference startTime = new AtomicReference<>(null); final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(stripes, this::onClose); +// final RefCounter refCounter = new StripedRefCounterImpl(stripes, this::onClose); final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { @@ -119,8 +126,8 @@ private void runTest(final int stripes) { } CompletableFuture.allOf(futures).join(); - if (refCounter.getTotalCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getTotalCount()); + if (refCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getCount()); } final Duration duration = Duration.between(startTime.get(), Instant.now()); @@ -203,8 +210,8 @@ void testBehaviour() throws InterruptedException { System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); - if (refCounter.getTotalCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getTotalCount()); + if (refCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getCount()); } if (!refCounter.isClosed()) { @@ -229,4 +236,96 @@ private void onClose() { System.out.println(Thread.currentThread() + " - Finishing onClose runnable"); } + @Test + void testBits() { + long val = 0; + val = val | (1L << 0); + val = val | (1L << 3); + val = val | (1L << 63); + + System.out.println("val: " + val + ", bits: " + Long.toBinaryString(val)); + + for (int i = 0; i < 64; i++) { + System.out.println("i: " + i + ", bit: " + getBit(val, i)); + } + } + + @Test + void testAtomicBitSet() { + final int size = 16; + final AtomicBitSet bitSet = new AtomicBitSet(16); + + assertThat(bitSet.isSet(3)) + .isEqualTo(false); + assertThat(bitSet.countSet()) + .isEqualTo(0); + assertThat(bitSet.flip(3)) + .isEqualTo(true); + assertThat(bitSet.countSet()) + .isEqualTo(1); + assertThat(bitSet.flip(3)) + .isEqualTo(false); + assertThat(bitSet.countSet()) + .isEqualTo(0); + + bitSet.setAndGet(3); + bitSet.setAndGet(10); + assertThat(bitSet.countSet()) + .isEqualTo(2); + bitSet.setAndGet(10); + assertThat(bitSet.countSet()) + .isEqualTo(2); + bitSet.unset(10); + assertThat(bitSet.countSet()) + .isEqualTo(1); + bitSet.unset(10); + assertThat(bitSet.countSet()) + .isEqualTo(1); + + bitSet.unSetAll(); + assertThat(bitSet.countSet()) + .isEqualTo(0); + + System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); + for (int i = 0; i < size; i++) { + bitSet.setAndGet(i); + } + System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); + + bitSet.setAll(); + assertThat(bitSet.countSet()) + .isEqualTo(size); + System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); + + bitSet.unSetAll(); + long asLong = bitSet.asLong(); + assertThat(bitSet.getAndSet(5)) + .isEqualTo(asLong); + assertThat(bitSet.getAndSet(5)) + .isNotEqualTo(asLong); + + bitSet.unSetAll(); + assertThat(bitSet.countSet()) + .isEqualTo(0); + assertThat(bitSet.countUnSet()) + .isEqualTo(size); + + bitSet.unSetAll(); + bitSet.setAndGet(3); + bitSet.setAndGet(10); + assertThat(bitSet.countSet()) + .isEqualTo(2); + assertThat(bitSet.countUnSet()) + .isEqualTo(size - 2); + } + + private String longAsBits(long val) { + return Long.toBinaryString(val); + } + + private long getBit(final long val, final long idx) { + return (val >> idx) & 1; + } + + } From e40ee221fd8e7d7cc480eb58c1d71bcb68142028 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:23:00 +0000 Subject: [PATCH 09/61] Add more ref counters, improve tests --- src/main/java/org/lmdbjava/AtomicBitSet.java | 3 + src/main/java/org/lmdbjava/RefCounter.java | 4 + .../org/lmdbjava/SimpleRefCounterImpl.java | 101 ++++++++++++ .../lmdbjava/SingleThreadedRefCounter.java | 1 + .../lmdbjava/StampedLockRefCounterImpl.java | 147 +++++++++++++++--- .../java/org/lmdbjava/RefCounterTest.java | 116 +++++++++++--- 6 files changed, 331 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/lmdbjava/SimpleRefCounterImpl.java diff --git a/src/main/java/org/lmdbjava/AtomicBitSet.java b/src/main/java/org/lmdbjava/AtomicBitSet.java index daf82508..12dbcb76 100644 --- a/src/main/java/org/lmdbjava/AtomicBitSet.java +++ b/src/main/java/org/lmdbjava/AtomicBitSet.java @@ -3,6 +3,9 @@ import java.util.concurrent.atomic.AtomicLong; +/** + * A bit set that can be mutated + */ public class AtomicBitSet { private static final int MAX_SIZE = (Long.BYTES * 8); diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index e3197868..a4c0ef1a 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -26,6 +26,10 @@ interface RefCounter { */ void close(); + default void doWhenIdle(final Runnable runnable) { + + } + /** * @return True if {@link RefCounter} is in a state of {@link EnvState#CLOSED} */ diff --git a/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java b/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java new file mode 100644 index 00000000..721769a3 --- /dev/null +++ b/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java @@ -0,0 +1,101 @@ +package org.lmdbjava; + + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +class SimpleRefCounterImpl implements RefCounter { + private final AtomicInteger counter; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicBoolean preventAcquire = new AtomicBoolean(false); + + public SimpleRefCounterImpl() { + this.counter = new AtomicInteger(0); + } + + @Override + public void use(Runnable runnable) { + acquire(); + try { + runnable.run(); + } finally { + release(); + } + } + + @Override + public void close() { + while (true) { + final int count = getCount(); + if (count == 0) { + break; + } + } + } + + @Override + public void doWhenIdle(final Runnable runnable) { + while (true) { + final int count = getCount(); +// System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", +// Thread.currentThread(), count); + if (count == 0) { + System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", + Thread.currentThread(), count); + if (closed.compareAndSet(false, true)) { + runnable.run(); + } + break; + } else { + preventAcquire.compareAndSet(false, true); + } + } + } + + @Override + public boolean isClosed() { + return closed.get(); + } + + @Override + public EnvState getState() { + return null; + } + + @Override + public void checkNotClosed() { + + } + + @Override + public void checkOpen() { + + } + + public R acquire(final Supplier supplier) { + acquire(); + try { + return supplier.get(); + } finally { + release(); + } + } + + public RefCounterReleaser acquire() { + if (preventAcquire.get()) { + throw new Env.AlreadyClosedException(); + } + counter.incrementAndGet(); + return this::release; + } + + private void release() { + // Increment if greater than 0. + counter.decrementAndGet(); + } + + public int getCount() { + return counter.get(); + } +} diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 39509037..3006db01 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -19,6 +19,7 @@ public RefCounterReleaser acquire() { if (envState != EnvState.OPEN) { throw new Env.AlreadyClosedException(); } + refCount++; return new SingleThreadedReleaser(this); } diff --git a/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java b/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java index 604d0a50..10a309a1 100644 --- a/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java @@ -1,30 +1,94 @@ package org.lmdbjava; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.StampedLock; import java.util.function.Supplier; class StampedLockRefCounterImpl implements RefCounter { - private static final long GOLDEN_RATIO = 0x9e3779b9L; + private static final int DEFAULT_STRIPES = 32; + private static final int MAX_STRIPES = 256; private final StampedLock stampedLock; private final int stripes; private final AtomicInteger[] counters; + private final AtomicBoolean closed = new AtomicBoolean(false); +// private final AtomicBoolean preventAcquire = new AtomicBoolean(false); + /** + * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). + * Used with bitwise AND for O(1) hashing with no modulo operation. + */ + private final int stripeMask; - public StampedLockRefCounterImpl(final int stripes) { + + public StampedLockRefCounterImpl() { + this(DEFAULT_STRIPES); + } + + public StampedLockRefCounterImpl(final int stripeCount) { this.stampedLock = new StampedLock(); - this.stripes = stripes; - this.counters = new AtomicInteger[stripes]; - for (int i = 0; i < stripes; i++) { + this.stripes = validateStripes(stripeCount); + this.stripeMask = stripeCount - 1; + this.counters = new AtomicInteger[stripeCount]; + for (int i = 0; i < stripeCount; i++) { counters[i] = new AtomicInteger(0); } } + private int validateStripes(final int stripeCount) { + if (stripeCount <= 0) { + throw new IllegalArgumentException( + "Stripe count must be positive, got: " + stripeCount); + } + if (stripeCount > MAX_STRIPES) { + throw new IllegalArgumentException( + "Stripe count exceeds maximum. Got: " + stripeCount + + ", max: " + MAX_STRIPES); + } + if ((stripeCount & (stripeCount - 1)) != 0) { + throw new IllegalArgumentException( + "Stripe count must be power of 2, got: " + stripeCount); + } + return stripeCount; + } + + /** + * Computes the stripe index for the current thread using XOR-fold hashing. + *

+ * This method combines the upper and lower 32 bits of the thread ID using XOR, + * then masks to the stripe count using bitwise AND. This provides: + *

    + *
  • Even distribution across stripes
  • + *
  • Same thread always maps to same stripe
  • + *
  • Guaranteed non-negative result
  • + *
  • O(1) performance (~3ns)
  • + *
+ * + * @return stripe index in range [0, stripeCount), guaranteed non-negative + */ private int getStripeIdx() { // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; - return (int) idx; + final long threadId = Thread.currentThread().getId(); + + // TODO In >= Java17 might as well replace with a JDK method + // final int mixedThreadId = (int) RandomSupport.mixStafford13(threadId); + // bit-mixer/finalizer for improving the entropy of the threadId + final int mixedThreadId = mixBits(threadId); + return mixedThreadId & stripeMask; + } + + + /** + * Returns the 32 high bits of David Stafford's variant 4 mix64 function as int. + * + * better-bit-mixing-improving-on + * + * An evolution of the MurmurHash3 finalizer. + */ + private static int mixBits(long val) { + val = (val ^ (val >>> 33)) * 0x62a9d9ed799705f5L; + return (int)(((val ^ (val >>> 28)) * 0xcb24d0a5c88c35b3L) >>> 32); } @Override @@ -39,12 +103,46 @@ public void use(Runnable runnable) { @Override public void close() { + while (true) { + final long writeLockStamp = stampedLock.writeLock(); + try { + final int count = getCountInternal(); + if (count == 0) { + break; + } + } finally { + stampedLock.unlockWrite(writeLockStamp); + } + } + } + @Override + public void doWhenIdle(final Runnable runnable) { + while (true) { + final long writeLockStamp = stampedLock.writeLock(); + try { + final int count = getCountInternal(); +// System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", +// Thread.currentThread(), count); + if (count == 0) { + System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", + Thread.currentThread(), count); + if (closed.compareAndSet(false, true)) { + runnable.run(); + } + break; + } else { +// preventAcquire.compareAndSet(false, true); + } + } finally { + stampedLock.unlockWrite(writeLockStamp); + } + } } @Override public boolean isClosed() { - return false; + return closed.get(); } @Override @@ -77,19 +175,21 @@ private void release() { } public RefCounterReleaser acquire() { +// if (preventAcquire.get()) { +// throw new Env.AlreadyClosedException(); +// } final long optimisticLockStamp = stampedLock.tryOptimisticRead(); final int stripeIdx = getStripeIdx(); - // Increment if greater than 0. final AtomicInteger counter = counters[stripeIdx]; counter.incrementAndGet(); - final boolean success = stampedLock.validate(optimisticLockStamp); - if (!success) { + if (!stampedLock.validate(optimisticLockStamp)) { // Undo incrementAndGet counter.decrementAndGet(); // Now repeat under lock final long readLockStamp = stampedLock.readLock(); +// System.out.printf("%s - acquire() under readlock%n", Thread.currentThread()); try { counter.incrementAndGet(); } finally { @@ -100,18 +200,21 @@ public RefCounterReleaser acquire() { } private void release(final AtomicInteger counter) { + // Try an optimistic non-blocking read on the assumption that the + // write lock in getCount() is not called very often and to limit + // overhead on release final long optimisticLockStamp = stampedLock.tryOptimisticRead(); - // Increment if greater than 0. counter.decrementAndGet(); - final boolean success = stampedLock.validate(optimisticLockStamp); - if (!success) { - // Undo decrementAndGet + if (!stampedLock.validate(optimisticLockStamp)) { + // Optimistic read not successful, so undo our change and try again + // with a proper read lock that may block counter.incrementAndGet(); // Now repeat under lock final long readLockStamp = stampedLock.readLock(); +// System.out.printf("%s - release() under readlock%n", Thread.currentThread()); try { counter.decrementAndGet(); } finally { @@ -123,13 +226,17 @@ private void release(final AtomicInteger counter) { public int getCount() { final long writeLockStamp = stampedLock.writeLock(); try { - int count = 0; - for (int i = 0; i < stripes; i++) { - count += counters[i].get(); - } - return count; + return getCountInternal(); } finally { stampedLock.unlockWrite(writeLockStamp); } } + + private int getCountInternal() { + int count = 0; + for (int i = 0; i < stripes; i++) { + count += counters[i].get(); + } + return count; + } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index f1a4927d..9057be22 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -26,14 +26,19 @@ public class RefCounterTest { public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { - System.out.println("Round: " + i + " StripedRefCounterImpl"); - - IntStream.of(1, 2, 4, 8, threadCount, threadCount * 2) + System.out.println("Round: " + i + " " + StripedRefCounterImpl.class.getSimpleName()); + IntStream.of(1, 2, 4, 8, 16, 32, 64) .forEach(stripes -> runTest(stripes, new StripedRefCounterImpl(stripes, this::onClose))); - System.out.println("Round: " + i + " StripedCounter"); - IntStream.of(1, 2, 4, 8, threadCount, threadCount * 2) + System.out.println("Round: " + i + " " + StampedLockRefCounterImpl.class.getSimpleName()); + IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) .forEach(stripes -> runTest(stripes, new StampedLockRefCounterImpl(stripes))); + + System.out.println("Round: " + i + " " + SimpleRefCounterImpl.class.getSimpleName()); + runTest(0, new SimpleRefCounterImpl()); + + System.out.println("Round: " + i + " " + SingleThreadedRefCounter.class.getSimpleName()); + runTest(0, 1, new SingleThreadedRefCounter(this::onClose)); } } @@ -91,6 +96,10 @@ private void doNoOpRefCounter() { } private void runTest(int stripes, final RefCounter refCounter) { + runTest(stripes, threadCount, refCounter); + } + + private void runTest(int stripes, final int threadCount, final RefCounter refCounter) { // System.out.println("Running test for " + stripes + " stripes"); final AtomicReference startTime = new AtomicReference<>(null); @@ -154,7 +163,8 @@ void testBehaviour() throws InterruptedException { // Reset the env env = new Object(); - final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); +// final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); + final RefCounter refCounter = new StampedLockRefCounterImpl(12); final CountDownLatch startLatch = new CountDownLatch(threadCount); final CompletableFuture[] futures = new CompletableFuture[threadCount]; final long[] counts = new long[threadCount]; @@ -198,7 +208,8 @@ void testBehaviour() throws InterruptedException { while (true) { try { - refCounter.close(); +// refCounter.close(); + refCounter.doWhenIdle(this::onClose); break; } catch (Env.EnvInUseException e) { Thread.sleep(100); @@ -220,6 +231,83 @@ void testBehaviour() throws InterruptedException { } } + @Test + void testGetCount() throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int rounds = 5; + final int iterations = 1_000_000; + + for (int k = 0; k < rounds; k++) { + final int round = k; + System.out.printf("Round %s ----------------------------------------%n", round); + + // Reset the env + env = new Object(); +// final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); + final RefCounter refCounter = new StampedLockRefCounterImpl(); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final long[] counts = new long[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); +// System.out.println(Thread.currentThread() + " - Starting"); + + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx]++; + } catch (Env.AlreadyClosedException e) { +// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + Thread.sleep(random.nextInt(1)); + // env is null after closure + Objects.requireNonNull(env, "Attempt to use a null env"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + releaser.release(); + } + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + Thread.sleep(100 + random.nextInt(200)); + + for (int i = 0; i < 10; i++) { + try { +// refCounter.close(); + System.out.println("count: " + refCounter.getCount()); + } catch (Env.EnvInUseException e) { + Thread.sleep(100 + random.nextInt(1000)); + } + } + + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); + + if (refCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getCount()); + } + } + } + private void countDownThenAwait(final CountDownLatch latch) { latch.countDown(); try { @@ -236,20 +324,6 @@ private void onClose() { System.out.println(Thread.currentThread() + " - Finishing onClose runnable"); } - @Test - void testBits() { - long val = 0; - val = val | (1L << 0); - val = val | (1L << 3); - val = val | (1L << 63); - - System.out.println("val: " + val + ", bits: " + Long.toBinaryString(val)); - - for (int i = 0; i < 64; i++) { - System.out.println("i: " + i + ", bit: " + getBit(val, i)); - } - } - @Test void testAtomicBitSet() { final int size = 16; From 91df336c59b1283a59de20b6345679ce62b8c018 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:09:49 +0000 Subject: [PATCH 10/61] Refactor ref counter classes --- src/main/java/org/lmdbjava/AtomicBitSet.java | 141 --------- src/main/java/org/lmdbjava/Env.java | 20 +- src/main/java/org/lmdbjava/Guard.java | 94 ------ .../java/org/lmdbjava/NoOpRefCounter.java | 23 +- src/main/java/org/lmdbjava/RefCounter.java | 47 +-- .../org/lmdbjava/SimpleRefCounterImpl.java | 77 ++--- .../lmdbjava/SingleThreadedRefCounter.java | 51 +-- .../lmdbjava/StampedLockRefCounterImpl.java | 242 -------------- .../java/org/lmdbjava/StripedRefCounter.java | 245 +++++++++++++++ .../org/lmdbjava/StripedRefCounterImpl.java | 296 ------------------ .../java/org/lmdbjava/RefCounterTest.java | 144 +++------ 11 files changed, 373 insertions(+), 1007 deletions(-) delete mode 100644 src/main/java/org/lmdbjava/AtomicBitSet.java delete mode 100644 src/main/java/org/lmdbjava/Guard.java delete mode 100644 src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java create mode 100644 src/main/java/org/lmdbjava/StripedRefCounter.java delete mode 100644 src/main/java/org/lmdbjava/StripedRefCounterImpl.java diff --git a/src/main/java/org/lmdbjava/AtomicBitSet.java b/src/main/java/org/lmdbjava/AtomicBitSet.java deleted file mode 100644 index 12dbcb76..00000000 --- a/src/main/java/org/lmdbjava/AtomicBitSet.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.lmdbjava; - - -import java.util.concurrent.atomic.AtomicLong; - -/** - * A bit set that can be mutated - */ -public class AtomicBitSet { - private static final int MAX_SIZE = (Long.BYTES * 8); - - private final int size; - private final int maxIdx; - private final AtomicLong atomicLong = new AtomicLong(0); - - public AtomicBitSet() { - this(MAX_SIZE); - } - - public AtomicBitSet(int size) { - if (size < 0 || size > MAX_SIZE) { - throw new IllegalArgumentException("size must be between 0 and " + MAX_SIZE + " (inclusive)"); - } - this.maxIdx = size - 1; - this.size = size; - } - - public boolean flip(int idx) { - checkIdx(idx); - final long newVal = atomicLong.accumulateAndGet(idx, (currVal, idx2) -> - currVal ^ (1L << idx2)); - return isSetWithNoCheck(newVal, idx); - } - - /** - * Set the bit at position idx and return the resulting bit set as a long. - */ - public long setAndGet(int idx) { - checkIdx(idx); - return atomicLong.accumulateAndGet(idx, (currVal, idx2) -> - currVal | (1L << idx2)); - } - - /** - * Sets the bit at position idx - * - * @return The previous value of the set as a long. - */ - public long getAndSet(int idx) { - checkIdx(idx); - return atomicLong.getAndAccumulate(idx, (currVal, idx2) -> - currVal | (1L << idx2)); - } - - /** - * Un-set the bit at position idx and return the resulting bit set as a long. - */ - public long unset(int idx) { - checkIdx(idx); - return atomicLong.updateAndGet(currVal -> - currVal & ~(1L << idx)); - } - - /** - * Set/un-set the bit at position idx, according to the value of isSet, - * and return the resulting bit set as a long. - */ - public long setAndGet(int idx, final boolean isSet) { - return isSet - ? setAndGet(idx) - : unset(idx); - } - - /** - * @return True if the bit at position idx is set. - */ - public boolean isSet(final int idx) { - checkIdx(idx); - return isSetWithNoCheck(atomicLong.get(), idx); - } - - /** - * @return The number of bits that have been set. - */ - public int countSet() { - return Long.bitCount(atomicLong.get()); - } - - public int countSet(final long val) { - return Long.bitCount(val); - } - - /** - * @return The number of bits that are un-set. - */ - public int countUnSet() { - return size - Long.bitCount(atomicLong.get()); - } - - public int countUnSet(final long val) { - return size - Long.bitCount(val); - } - - public void unSetAll() { - atomicLong.set(0L); - } - - public void setAll() { - if (size == MAX_SIZE) { - atomicLong.set(-1L); - } else { - for (int i = 0; i < size; i++) { - setAndGet(i); - } - } - } - - public long asLong() { - return atomicLong.get(); - } - - public boolean isSet(final long val, final int idx) { - checkIdx(idx); - return isSetWithNoCheck(val, idx); - } - - private static boolean isSetWithNoCheck(final long val, final int idx) { - return ((val >> idx) & 1L) != 0L; - } - - private void checkIdx(final int idx) { - if (idx < 0 || idx > maxIdx) { - throw new IllegalArgumentException("idx must be between 0 and " + maxIdx + " (inclusive)"); - } - } - - @Override - public String toString() { - return Long.toBinaryString(atomicLong.get()); - } -} diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 8263ebd9..80ed24e2 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -96,15 +96,21 @@ private Env( this.path = path; this.envFlagSet = envFlagSet; this.isSingleThreaded = isSingleThreaded; + this.refCounter = initRefCounter(isSingleThreaded); + } + + private RefCounter initRefCounter(boolean isSingleThreaded) { + final RefCounter refCounter; if (SHOULD_CHECK) { if (isSingleThreaded) { - this.refCounter = new SingleThreadedRefCounter(this::closeMdbEnv); + refCounter = new SingleThreadedRefCounter(); } else { - this.refCounter = new StripedRefCounterImpl(this::closeMdbEnv); + refCounter = new StripedRefCounter(); } } else { - this.refCounter = new NoOpRefCounter(this::closeMdbEnv); + refCounter = new NoOpRefCounter(); } + return refCounter; } /** @@ -148,8 +154,8 @@ public static Env open(final File path, final int size, final EnvFla */ @Override public void close() { - System.out.println("Closing Env"); - refCounter.close(); +// System.out.println("Closing Env"); + refCounter.close(this::closeMdbEnv); } private void closeMdbEnv() { @@ -598,10 +604,6 @@ void checkNotClosed() { refCounter.checkNotClosed(); } - void checkOpen() { - refCounter.checkOpen(); - } - private void validateDirectoryEmpty(final Path path) { if (!Files.exists(path)) { throw new InvalidCopyDestination("Path does not exist"); diff --git a/src/main/java/org/lmdbjava/Guard.java b/src/main/java/org/lmdbjava/Guard.java deleted file mode 100644 index 82fdd8c6..00000000 --- a/src/main/java/org/lmdbjava/Guard.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.lmdbjava; - - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; - -class Guard { - private static final long GOLDEN_RATIO = 0x9e3779b9L; - - private final int stripes; - private final AtomicInteger[] inUseCounts; - private final AtomicBoolean destroy = new AtomicBoolean(); - // private final AtomicBoolean destroyed = new AtomicBoolean(); - private final AtomicBitSet bitSet; - private final Runnable destroyRunnable; - - public Guard(final Runnable destroyRunnable, final int stripes) { - this.stripes = stripes; - this.destroyRunnable = destroyRunnable; - this.inUseCounts = new AtomicInteger[stripes]; - this.bitSet = new AtomicBitSet(stripes); - for (int i = 0; i < stripes; i++) { - inUseCounts[i] = new AtomicInteger(1); - } - } - - private int getStripeIdx() { - // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; - return (int) idx; - } - - public R acquire(final Supplier supplier) { - // Increment if greater than 0. - final int c = inUseCounts[getStripeIdx()].updateAndGet(count -> count > 0 - ? count + 1 - : count); - if (c <= 0) { - // The destroy flag may not have been set when we entered this method but count == 0 means destruction - // has been triggered since then. - throw new RuntimeException("Try again"); - } - - try { - return supplier.get(); - } finally { - release(); - } - } - - private void release() { - // Decrement but don't go lower than 0. - final int stripeIdx = getStripeIdx(); - release(stripeIdx); - } - - private void release(final int stripeIdx) { - // Decrement but don't go lower than 0. - final int newCount = inUseCounts[stripeIdx].updateAndGet(count -> { - if (count > 0) { - return count - 1; - } else if (count < 0) { - return count + 1; - } else { - return count; - } - }); - - if (newCount == 0) { - if (!bitSet.isSet(stripeIdx)) { - final long prevVal = bitSet.getAndSet(stripeIdx); - final boolean didChange = !bitSet.isSet(prevVal, stripeIdx); - if (didChange) { - if (bitSet.countUnSet(prevVal) == 1) { - destroyRunnable.run(); - } - } - } - } - } - - public void destroy() { - if (destroy.compareAndSet(false, true)) { - // Perform final decrement. Close is either performed now if the guard is not acquired or will be - // performed by the final thread that releases the acquisition. - for (int stripeIdx = 0; stripeIdx < stripes; stripeIdx++) { - release(stripeIdx); - } - } else { -// LOGGER.debug("Guard already destroyed"); - } - } -} diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index a36b94f2..857897c5 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -1,19 +1,14 @@ package org.lmdbjava; -import java.util.Objects; - public class NoOpRefCounter implements RefCounter { - private final Runnable onClose; - - public NoOpRefCounter(final Runnable onClose) { - this.onClose = Objects.requireNonNull(onClose); + public NoOpRefCounter() { } @Override public RefCounterReleaser acquire() { - return RefCounterReleaser.NO_OP_RELEASER; + return RefCounter.NO_OP_RELEASER; } @Override @@ -22,7 +17,8 @@ public void use(Runnable runnable) { } @Override - public void close() { + public void close(Runnable onClose) { + // Close with no checks onClose.run(); } @@ -31,23 +27,14 @@ public boolean isClosed() { return false; } - @Override - public EnvState getState() { - return null; - } - @Override public void checkNotClosed() { // no-op } - @Override - public void checkOpen() { - // no-op - } - @Override public int getCount() { return 0; } + } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index a4c0ef1a..d002262f 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -6,6 +6,10 @@ */ interface RefCounter { + RefCounterReleaser NO_OP_RELEASER = () -> { + // No-op + }; + /** * Call this before using the {@link RefCounter} controlled object. * @return A {@link RefCounterReleaser} to release once the work is complete @@ -15,47 +19,50 @@ interface RefCounter { /** * Calls {@link RefCounter#acquire()}, runs runnable, then calls {@link RefCounterReleaser#release()} */ - void use(final Runnable runnable); + default void use(final Runnable runnable) { + if (runnable != null) { + final RefCounterReleaser releaser = acquire(); + try { + runnable.run(); + } finally { + releaser.release(); + } + } + } /** - * Closes the {@link RefCounter} controlled item, but only after ensuring all active users of - * it have released. Once {@link RefCounter#close()} is called, all subsequent calls to - * {@link RefCounter#acquire()} or {@link RefCounter#use(Runnable)} will throw an - * {@link org.lmdbjava.Env.AlreadyClosedException} + * If the reference count is zero, onClose will be called. This {@link RefCounter} will be marked + * as closed so all future calls to acquire will throw a {@link org.lmdbjava.Env.AlreadyClosedException}. + * If the count is non-zero, {@link org.lmdbjava.Env.EnvInUseException} will be thrown. * @throws org.lmdbjava.Env.EnvInUseException If the {@link Env} has open transactions/cursors. + * @throws org.lmdbjava.Env.AlreadyClosedException If this {@link RefCounter} has already been + * successfully closed. */ - void close(); - - default void doWhenIdle(final Runnable runnable) { - - } + void close(final Runnable onClose); /** * @return True if {@link RefCounter} is in a state of {@link EnvState#CLOSED} */ boolean isClosed(); - EnvState getState(); - /** * If it is in a CLOSED state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} */ - void checkNotClosed(); + default void checkNotClosed() { + if (isClosed()) { + throw new Env.AlreadyClosedException(); + } + } /** - * If it is not in an OPEN state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} + * @return The current count of items in use. + * @throws org.lmdbjava.Env.AlreadyClosedException If called after it has been successfully closed. */ - void checkOpen(); - int getCount(); @FunctionalInterface interface RefCounterReleaser { - RefCounterReleaser NO_OP_RELEASER = () -> { - // No-op - }; - void release(); } } diff --git a/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java b/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java index 721769a3..4f11b3f9 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java @@ -1,76 +1,23 @@ package org.lmdbjava; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; class SimpleRefCounterImpl implements RefCounter { private final AtomicInteger counter; - private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicBoolean isClosed = new AtomicBoolean(false); private final AtomicBoolean preventAcquire = new AtomicBoolean(false); public SimpleRefCounterImpl() { this.counter = new AtomicInteger(0); } - @Override - public void use(Runnable runnable) { - acquire(); - try { - runnable.run(); - } finally { - release(); - } - } - - @Override - public void close() { - while (true) { - final int count = getCount(); - if (count == 0) { - break; - } - } - } - - @Override - public void doWhenIdle(final Runnable runnable) { - while (true) { - final int count = getCount(); -// System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", -// Thread.currentThread(), count); - if (count == 0) { - System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", - Thread.currentThread(), count); - if (closed.compareAndSet(false, true)) { - runnable.run(); - } - break; - } else { - preventAcquire.compareAndSet(false, true); - } - } - } - @Override public boolean isClosed() { - return closed.get(); - } - - @Override - public EnvState getState() { - return null; - } - - @Override - public void checkNotClosed() { - - } - - @Override - public void checkOpen() { - + return isClosed.get(); } public R acquire(final Supplier supplier) { @@ -90,12 +37,28 @@ public RefCounterReleaser acquire() { return this::release; } + @Override + public void close(final Runnable onClose) { + Objects.requireNonNull(onClose); + if (!isClosed.get()) { + final int count = getCount(); + if (count == 0) { + if (isClosed.compareAndSet(false, true)) { + onClose.run(); + } + } else { + throw new Env.EnvInUseException(count); + } + } + } + private void release() { // Increment if greater than 0. counter.decrementAndGet(); } + @Override public int getCount() { - return counter.get(); + return counter.get(); } } diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 3006db01..0c4d355c 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -5,18 +5,16 @@ public class SingleThreadedRefCounter implements RefCounter { - private final Runnable onClose; private int refCount; + private boolean isClosed = false; private EnvState envState; - public SingleThreadedRefCounter(final Runnable onClose) { - this.onClose = Objects.requireNonNull(onClose); - this.envState = EnvState.OPEN; + public SingleThreadedRefCounter() { } @Override public RefCounterReleaser acquire() { - if (envState != EnvState.OPEN) { + if (isClosed) { throw new Env.AlreadyClosedException(); } refCount++; @@ -41,43 +39,22 @@ public void use(Runnable runnable) { } @Override - public void close() { - if (envState == EnvState.OPEN) { - envState = EnvState.CLOSING; - } - - if (refCount > 0) { - throw new Env.EnvInUseException(); - } - - if (envState == EnvState.CLOSING) { - onClose.run(); - envState = EnvState.CLOSED; + public void close(final Runnable onClose) { + Objects.requireNonNull(onClose); + if (!isClosed) { + final int count = getCount(); + if (count == 0) { + isClosed = true; + onClose.run(); + } else { + throw new Env.EnvInUseException(count); + } } } @Override public boolean isClosed() { - return envState == EnvState.CLOSED; - } - - @Override - public EnvState getState() { - return envState; - } - - @Override - public void checkNotClosed() { - if (envState == EnvState.CLOSED) { - throw new Env.AlreadyClosedException(); - } - } - - @Override - public void checkOpen() { - if (envState != EnvState.OPEN) { - throw new Env.AlreadyClosedException(); - } + return isClosed; } @Override diff --git a/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java b/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java deleted file mode 100644 index 10a309a1..00000000 --- a/src/main/java/org/lmdbjava/StampedLockRefCounterImpl.java +++ /dev/null @@ -1,242 +0,0 @@ -package org.lmdbjava; - - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.StampedLock; -import java.util.function.Supplier; - -class StampedLockRefCounterImpl implements RefCounter { - private static final int DEFAULT_STRIPES = 32; - private static final int MAX_STRIPES = 256; - - private final StampedLock stampedLock; - private final int stripes; - private final AtomicInteger[] counters; - private final AtomicBoolean closed = new AtomicBoolean(false); -// private final AtomicBoolean preventAcquire = new AtomicBoolean(false); - /** - * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). - * Used with bitwise AND for O(1) hashing with no modulo operation. - */ - private final int stripeMask; - - - public StampedLockRefCounterImpl() { - this(DEFAULT_STRIPES); - } - - public StampedLockRefCounterImpl(final int stripeCount) { - this.stampedLock = new StampedLock(); - this.stripes = validateStripes(stripeCount); - this.stripeMask = stripeCount - 1; - this.counters = new AtomicInteger[stripeCount]; - for (int i = 0; i < stripeCount; i++) { - counters[i] = new AtomicInteger(0); - } - } - - private int validateStripes(final int stripeCount) { - if (stripeCount <= 0) { - throw new IllegalArgumentException( - "Stripe count must be positive, got: " + stripeCount); - } - if (stripeCount > MAX_STRIPES) { - throw new IllegalArgumentException( - "Stripe count exceeds maximum. Got: " + stripeCount + - ", max: " + MAX_STRIPES); - } - if ((stripeCount & (stripeCount - 1)) != 0) { - throw new IllegalArgumentException( - "Stripe count must be power of 2, got: " + stripeCount); - } - return stripeCount; - } - - /** - * Computes the stripe index for the current thread using XOR-fold hashing. - *

- * This method combines the upper and lower 32 bits of the thread ID using XOR, - * then masks to the stripe count using bitwise AND. This provides: - *

    - *
  • Even distribution across stripes
  • - *
  • Same thread always maps to same stripe
  • - *
  • Guaranteed non-negative result
  • - *
  • O(1) performance (~3ns)
  • - *
- * - * @return stripe index in range [0, stripeCount), guaranteed non-negative - */ - private int getStripeIdx() { - // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long threadId = Thread.currentThread().getId(); - - // TODO In >= Java17 might as well replace with a JDK method - // final int mixedThreadId = (int) RandomSupport.mixStafford13(threadId); - // bit-mixer/finalizer for improving the entropy of the threadId - final int mixedThreadId = mixBits(threadId); - return mixedThreadId & stripeMask; - } - - - /** - * Returns the 32 high bits of David Stafford's variant 4 mix64 function as int. - * - * better-bit-mixing-improving-on - * - * An evolution of the MurmurHash3 finalizer. - */ - private static int mixBits(long val) { - val = (val ^ (val >>> 33)) * 0x62a9d9ed799705f5L; - return (int)(((val ^ (val >>> 28)) * 0xcb24d0a5c88c35b3L) >>> 32); - } - - @Override - public void use(Runnable runnable) { - acquire(); - try { - runnable.run(); - } finally { - release(); - } - } - - @Override - public void close() { - while (true) { - final long writeLockStamp = stampedLock.writeLock(); - try { - final int count = getCountInternal(); - if (count == 0) { - break; - } - } finally { - stampedLock.unlockWrite(writeLockStamp); - } - } - } - - @Override - public void doWhenIdle(final Runnable runnable) { - while (true) { - final long writeLockStamp = stampedLock.writeLock(); - try { - final int count = getCountInternal(); -// System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", -// Thread.currentThread(), count); - if (count == 0) { - System.out.printf("%s - doWhenIdle() under writeLock, count: %s%n", - Thread.currentThread(), count); - if (closed.compareAndSet(false, true)) { - runnable.run(); - } - break; - } else { -// preventAcquire.compareAndSet(false, true); - } - } finally { - stampedLock.unlockWrite(writeLockStamp); - } - } - } - - @Override - public boolean isClosed() { - return closed.get(); - } - - @Override - public EnvState getState() { - return null; - } - - @Override - public void checkNotClosed() { - - } - - @Override - public void checkOpen() { - - } - - public R acquire(final Supplier supplier) { - acquire(); - try { - return supplier.get(); - } finally { - release(); - } - } - - private void release() { - final AtomicInteger counter = counters[getStripeIdx()]; - release(counter); - } - - public RefCounterReleaser acquire() { -// if (preventAcquire.get()) { -// throw new Env.AlreadyClosedException(); -// } - final long optimisticLockStamp = stampedLock.tryOptimisticRead(); - final int stripeIdx = getStripeIdx(); - final AtomicInteger counter = counters[stripeIdx]; - counter.incrementAndGet(); - - if (!stampedLock.validate(optimisticLockStamp)) { - // Undo incrementAndGet - counter.decrementAndGet(); - - // Now repeat under lock - final long readLockStamp = stampedLock.readLock(); -// System.out.printf("%s - acquire() under readlock%n", Thread.currentThread()); - try { - counter.incrementAndGet(); - } finally { - stampedLock.unlockRead(readLockStamp); - } - } - return () -> release(counter); - } - - private void release(final AtomicInteger counter) { - // Try an optimistic non-blocking read on the assumption that the - // write lock in getCount() is not called very often and to limit - // overhead on release - final long optimisticLockStamp = stampedLock.tryOptimisticRead(); - - counter.decrementAndGet(); - - if (!stampedLock.validate(optimisticLockStamp)) { - // Optimistic read not successful, so undo our change and try again - // with a proper read lock that may block - counter.incrementAndGet(); - - // Now repeat under lock - final long readLockStamp = stampedLock.readLock(); -// System.out.printf("%s - release() under readlock%n", Thread.currentThread()); - try { - counter.decrementAndGet(); - } finally { - stampedLock.unlockRead(readLockStamp); - } - } - } - - public int getCount() { - final long writeLockStamp = stampedLock.writeLock(); - try { - return getCountInternal(); - } finally { - stampedLock.unlockWrite(writeLockStamp); - } - } - - private int getCountInternal() { - int count = 0; - for (int i = 0; i < stripes; i++) { - count += counters[i].get(); - } - return count; - } -} diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java new file mode 100644 index 00000000..ad289db9 --- /dev/null +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -0,0 +1,245 @@ +package org.lmdbjava; + + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.StampedLock; + +class StripedRefCounter implements RefCounter { + private static final int MAGIC_ZERO_VALUE = Integer.MIN_VALUE; + private static final int MAGIC_CLOSED_VALUE = Integer.MAX_VALUE; + private static final int DEFAULT_STRIPES = 64; + private static final int MAX_STRIPES = 256; + + private final StampedLock stampedLock; + private final AtomicInteger[] counters; + private final AtomicBoolean isClosed = new AtomicBoolean(false); + /** + * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). + * Used with bitwise AND for O(1) hashing with no modulo operation. + */ + private final int stripeMask; + + public StripedRefCounter() { + this(DEFAULT_STRIPES); + } + + public StripedRefCounter(final int stripeCount) { + this.stampedLock = new StampedLock(); + validateStripeCount(stripeCount); + this.stripeMask = stripeCount - 1; + this.counters = new AtomicInteger[stripeCount]; + for (int i = 0; i < stripeCount; i++) { + counters[i] = new AtomicInteger(0); + } + } + + @Override + public boolean isClosed() { + return isClosed.get(); + } + + public RefCounterReleaser acquire() { + final AtomicInteger counter = counters[getStripeIdx()]; + try { + addToCounter(counter, 1); + } catch (final CountInProgressException e) { + // Counting is in progress so we need to get a lock which will likely block + // until the count is complete + synchronized (this) { + addToCounter(counter, 1); + } + } + return new RefCounterReleaserImpl(this, counter); + } + + private void release(final AtomicInteger counter) { + try { + addToCounter(counter, -1); + } catch (final CountInProgressException e) { + // Counting is in progress so we need to get a lock which will likely block + // until the count is complete + synchronized (this) { + addToCounter(counter, -1); + } + } + } + + @Override + public void close(final Runnable onClose) { + if (!isClosed.get()) { + Objects.requireNonNull(onClose); + + synchronized (this) { + // Once we have marked all counters, any threads trying to mutate the counters + // will fail, then attempt to get the lock, so will have to wait for us to complete + // the count. + markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 + try { + final int totalCount = sumCounters(); + if (totalCount == 0) { + if (isClosed.compareAndSet(false, true)) { + onClose.run(); + // Mark all counters as closed to prevent any future acquire calls + for (AtomicInteger counter : counters) { + counter.set(MAGIC_CLOSED_VALUE); + } + } + } else { + throw new Env.EnvInUseException(totalCount); + } + } finally { + if (!isClosed.get()) { + // Return all counters to their original positive values so + // acquire/release can resume as normal + markCountersAsNoCountInProgress(); // MAGIC_ZERO_VALUE=>0 else i=>i*-1 + } + } + } + } + } + + private int sumCounters() { + int totalCount = 0; + for (AtomicInteger counter : counters) { + int count = counter.get(); + if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE + throw new Env.AlreadyClosedException(); + } else if (count != MAGIC_ZERO_VALUE) { // Integer.MIN_VALUE + totalCount += count; + } + } + // The individual counts were all negative, so use the abs value + totalCount = Math.abs(totalCount); + return totalCount; + } + + public int getCount() { + checkNotClosed(); + synchronized (this) { + // This will stop any other thread from incrementing/decrementing the counter + markCountersAsCountInProgress(); + try { + return sumCounters(); + } finally { + markCountersAsNoCountInProgress(); + } + } + } + + private void addToCounter(final AtomicInteger counter, final int delta) { + counter.accumulateAndGet(delta, (currVal, delta2) -> { + if (currVal == MAGIC_CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } else if (currVal < 0) { + throw new CountInProgressException(); + } else { + return currVal + delta2; + } + }); + } + + private void markCountersAsNoCountInProgress() { + for (AtomicInteger counter : counters) { + // Multiply value by -1 so we can indicate to other threads that a count is in progress + // while maintaining the count. Have to use a special replacement value for zero. + counter.updateAndGet(currVal -> { + if (MAGIC_ZERO_VALUE == 0) { + return 0; + } else { + return Math.abs(currVal); + } + }); + } + } + + private void markCountersAsCountInProgress() { + for (AtomicInteger counter : counters) { + counter.updateAndGet(currVal -> { + if (currVal == 0) { + // Use a magic value to mark this zero value counter as having a count in progress + return MAGIC_ZERO_VALUE; + } else { + // Make the value negative to indicate a count in progress + return Math.abs(currVal) * -1; + } + }); + } + } + + private int validateStripeCount(final int stripeCount) { + if (stripeCount <= 0) { + throw new IllegalArgumentException( + "Stripe count must be positive, got: " + stripeCount); + } + if (stripeCount > MAX_STRIPES) { + throw new IllegalArgumentException( + "Stripe count exceeds maximum. Got: " + stripeCount + + ", max: " + MAX_STRIPES); + } + if ((stripeCount & (stripeCount - 1)) != 0) { + throw new IllegalArgumentException( + "Stripe count must be power of 2, got: " + stripeCount); + } + return stripeCount; + } + + /** + * Computes the stripe index for the current thread using Stafford variant 13 mixing. + *

+ * This method applies a high-quality 64-bit hash function (MurmurHash3 finalizer) + * to the thread ID before masking to the stripe count. This provides: + *

    + *
  • Excellent distribution for sequential thread IDs
  • + *
  • Same thread always maps to same stripe (deterministic)
  • + *
  • Strong avalanche properties (input bit changes affect all output bits)
  • + *
  • O(1) performance
  • + *
+ *

+ * The Stafford13 mixing function is used internally by {@link java.util.SplittableRandom} + * for seed initialization. See: + * + * Better Bit Mixing + * + * @return stripe index from 0 to stripeCount - 1 (inclusive) + */ + private int getStripeIdx() { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + long threadId = Thread.currentThread().getId(); + // Stafford13 for sequential inputs + threadId = (threadId ^ (threadId >>> 30)) * 0xbf58476d1ce4e5b9L; + threadId = (threadId ^ (threadId >>> 27)) * 0x94d049bb133111ebL; + return (int) ((threadId ^ (threadId >>> 31)) & stripeMask); + } + + private static class RefCounterReleaserImpl implements RefCounterReleaser { + + private final AtomicReference refCounterRef; + private final AtomicInteger counter; + + private RefCounterReleaserImpl(final StripedRefCounter refCounter, + final AtomicInteger counter) { + this.refCounterRef = new AtomicReference<>(refCounter); + this.counter = counter; + } + + @Override + public void release() { + // Prevent duplicate release calls + final StripedRefCounter refCounter = refCounterRef.getAndSet(null); + if (refCounter != null) { + refCounter.release(counter); + } + } + } + + /** + * Thrown when an attempt is made to mutate a counter while a sum of all counters + * is being taken. + */ + private static class CountInProgressException extends RuntimeException { + + } +} diff --git a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java b/src/main/java/org/lmdbjava/StripedRefCounterImpl.java deleted file mode 100644 index 1bd1c304..00000000 --- a/src/main/java/org/lmdbjava/StripedRefCounterImpl.java +++ /dev/null @@ -1,296 +0,0 @@ -package org.lmdbjava; - - -import static java.util.Objects.requireNonNull; - -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -class StripedRefCounterImpl implements RefCounter { - private static final int CLOSED_COUNT = Integer.MIN_VALUE; - // Golden Ratio constant used for better hash scattering - // See https://softwareengineering.stackexchange.com/a/402543 - private static final long GOLDEN_RATIO = 0x9e3779b9L; - - /** - * Number of stripes used to improve the concurrency - */ - private final int stripes; - private final StripeState[] stripeStates; - private final AtomicBitSet closedStripesBitSet; - /** - * Flag to indicate if {@link RefCounter#close()} has been called. - * Once set, it means that all subsequent {@link RefCounter#acquire()} will throw. - */ - private final Runnable onClose; - private final AtomicReference stateRef; - - StripedRefCounterImpl(Runnable onClose) { - // Default to 1 stripe per processor for max concurrency - this(Runtime.getRuntime().availableProcessors(), onClose); - } - - StripedRefCounterImpl(int stripes, Runnable onClose) { - this.stripes = stripes; - if (stripes <= 0) { - throw new IllegalArgumentException("stripes must be positive"); - } - this.stripeStates = new StripeState[stripes]; - this.onClose = requireNonNull(onClose); - this.stateRef = new AtomicReference<>(EnvState.OPEN); - this.closedStripesBitSet = new AtomicBitSet(stripes); - for (int stripeIdx = 0; stripeIdx < stripes; stripeIdx++) { - stripeStates[stripeIdx] = new StripeState(stripeIdx); - } - } - - @Override - public RefCounterReleaser acquire() { - if (stateRef.get() != EnvState.OPEN) { - // Close has been initiated, so acquire() is no longer allowed -// System.out.println("Throwing AlreadyClosedException 1"); - throw new Env.AlreadyClosedException(); - } - - // close() may be called after we have checked closeCalled, but the updateAndGet - // will ensure that we cannot increment the count if close() has been called. - - final StripeState stripeState = stripeStates[getStripeIdx()]; - // If we increment the count just before counter is made negative, then close() - // will have to wait for us to release. - final int newCount = stripeState.counter.updateAndGet(currVal -> { - int newVal = currVal; - if (currVal >= 0) { - newVal = currVal + 1; -// System.out.printf("%s - acquire() called, currVal: %s, newVal: %s%n", -// Thread.currentThread(), currVal, newVal); - if (newVal == Integer.MAX_VALUE) { - // MAX_VALUE is not allowed as that would become CLOSED_COUNT when made negative - throw new IllegalStateException("Too many concurrent acquire calls"); - } - } - return newVal; - }); - - if (newCount < 0) { - throw new Env.AlreadyClosedException(); - } - // Return the releaser than knows which stripe to release back to - return new RefCounterReleaserImpl(this, stripeState); - } - - @Override - public void use(final Runnable runnable) { - if (runnable != null) { - final RefCounterReleaser releaser = acquire(); - try { - runnable.run(); - } finally { - releaser.release(); - } - } - } - - private void release(final StripeState stripeState) { - final int count = stripeState.counter.updateAndGet(currVal -> { - int newVal; - if (currVal > 0) { - // Positive count, so in an open state, therefore -1 back down towards zero - newVal = currVal - 1; -// System.out.printf("%s - release() called, currVal: %s, newVal: %s%n", -// Thread.currentThread(), currVal, newVal); - } else if (currVal == CLOSED_COUNT) { - // CLOSED_COUNT is only set if the value is zero on close() - throw new IllegalStateException("currVal should never be CLOSED_COUNT on release()"); - } else if (currVal < 0) { - // Negative count, so in a closed state - // +1 to take the count back up towards zero - newVal = currVal + 1; -// System.out.printf("%s - release() called, currVal: %s, newVal: %s%n", -// Thread.currentThread(), currVal, newVal); - if (newVal == 0) { - // Reached zero, so set to the magic number, so counter stays negative, i.e. closed - newVal = CLOSED_COUNT; - } - } else { - throw new IllegalStateException("currVal should never be zero on release()"); - } - return newVal; - }); - -// if (count == CLOSED_COUNT) { -// markStripeAsClosed(stripeState); -// } - } - - private void markStripeAsClosed(final StripeState stripeState) { - // Mark this stripe as closed - final int idx = stripeState.index; - final long prevVal = closedStripesBitSet.getAndSet(idx); - final boolean didChange = !closedStripesBitSet.isSet(prevVal, idx); - if (didChange) { - if (closedStripesBitSet.countUnSet(prevVal) == 1) { - // We closed the last one - } - } - } - - private boolean setCountersInClosingState() { - // Only want to do this once - final boolean didChange = stateRef.compareAndSet(EnvState.OPEN, EnvState.CLOSING); - if (didChange) { -// System.out.println("close() called"); - // Place each stripe into a closed state - for (int stripe = 0; stripe < stripes; stripe++) { - final StripeState stripeState = stripeStates[stripe]; - final int count = stripeState.counter.updateAndGet(currVal -> { - if (currVal == 0) { - // Count is already at zero so there will be nothing to wait for. - // Ensures any thread that tries to increment will see it as closed - return CLOSED_COUNT; - } else if (currVal > 0) { - // Make it negative to indicate the closed state but maintain the ref count - // (albeit as a negative number) - final int newVal = currVal * -1; -// System.out.printf("%s - close() called, currVal: %s, newVal: %s%n", -// Thread.currentThread(), currVal, newVal); - return newVal; - } else { - throw new IllegalStateException("currVal should not be zero on close()"); - } - }); - -// if (count == CLOSED_COUNT) { -// markStripeAsClosed(stripeState); -// } - } - } - return didChange; - } - - @Override - public void close() { - // First ensure all counters are marked as closing to stop any new acquire calls - final boolean didChange = setCountersInClosingState(); - - // At this point, no new acquire calls are possible - if (didChange) { -// closedStripesArray. - - // If any counter is negative then there are still release() calls outstanding - for (int stripe = 0; stripe < stripes; stripe++) { - final StripeState stripeState = stripeStates[stripe]; - final int count = stripeState.counter.get(); - if (count < 0 && count != CLOSED_COUNT) { - throw new Env.EnvInUseException(getCount()); - } - } - - onClose.run(); - stateRef.set(EnvState.CLOSED); - } else { - final EnvState envState = stateRef.get(); - if (envState == EnvState.OPEN) { - throw new IllegalStateException("EnvState should not be OPEN at this point"); - } - } - } - - private int getStripeIdx() { - // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; - return (int) idx; - } - - @Override - public boolean isClosed() { - return stateRef.get() == EnvState.CLOSED; - } - - @Override - public EnvState getState() { - return stateRef.get(); - } - - @Override - public void checkNotClosed() { - // TODO should it return ==CLOSED or !=OPEN ? - if (stateRef.get() == EnvState.CLOSED) { - throw new Env.AlreadyClosedException(); - } - } - - @Override - public void checkOpen() { - if (stateRef.get() != EnvState.OPEN) { - throw new Env.AlreadyClosedException(); - } - } - - /** - * @return The total number of active users. Not atomic. - */ - @Override - public int getCount() { - return Arrays.stream(stripeStates) - .map(StripeState::getCounter) - .mapToInt(AtomicInteger::get) - .filter(i -> i != CLOSED_COUNT) - .map(Math::abs) // Count could be +ve/-ve so take abs value - .sum(); - } - - private static class StripeState { - - private final int index; - private final AtomicInteger counter; - - /** - * One latch per stripe. Each will start with a value of 1 - */ - - private StripeState(final int index) { - this.index = index; - this.counter = new AtomicInteger(0); - } - - int getIndex() { - return index; - } - - AtomicInteger getCounter() { - return counter; - } - - @Override - public String toString() { - return "Stripe{" + - "index=" + index + - ", counter=" + counter + - '}'; - } - } - - private static class RefCounterReleaserImpl implements RefCounterReleaser { - - private final AtomicReference refCounterRef; - private final StripeState stripeState; - - private RefCounterReleaserImpl(final StripedRefCounterImpl refCounter, - final StripeState stripeState) { - this.refCounterRef = new AtomicReference<>(refCounter); - this.stripeState = stripeState; - } - - @Override - public void release() { - // Prevent duplicate release calls - final StripedRefCounterImpl refCounter = refCounterRef.getAndSet(null); - if (refCounter != null) { - refCounter.release(stripeState); - } - } - } - -} diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 9057be22..135c9e32 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,8 +1,6 @@ package org.lmdbjava; -import static org.assertj.core.api.Assertions.assertThat; - import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -12,11 +10,13 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; public class RefCounterTest { + private static final long GOLDEN_RATIO = 0x9e3779b9L; private final int iterations = 1_000_000; private final int threadCount = Runtime.getRuntime().availableProcessors(); @@ -26,19 +26,31 @@ public class RefCounterTest { public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { - System.out.println("Round: " + i + " " + StripedRefCounterImpl.class.getSimpleName()); - IntStream.of(1, 2, 4, 8, 16, 32, 64) - .forEach(stripes -> runTest(stripes, new StripedRefCounterImpl(stripes, this::onClose))); + System.out.println("Multi-threaded tests ---------------------------------"); - System.out.println("Round: " + i + " " + StampedLockRefCounterImpl.class.getSimpleName()); + System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) - .forEach(stripes -> runTest(stripes, new StampedLockRefCounterImpl(stripes))); + .forEach(stripes -> runTest(stripes, new StripedRefCounter(stripes))); System.out.println("Round: " + i + " " + SimpleRefCounterImpl.class.getSimpleName()); runTest(0, new SimpleRefCounterImpl()); + System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); + runTest(0, new NoOpRefCounter()); + + System.out.println("Single-threaded tests ---------------------------------"); + + System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); + runTest(1, 1, new StripedRefCounter(1)); + + System.out.println("Round: " + i + " " + SimpleRefCounterImpl.class.getSimpleName()); + runTest(0, 1, new SimpleRefCounterImpl()); + + System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); + runTest(0, 1, new NoOpRefCounter()); + System.out.println("Round: " + i + " " + SingleThreadedRefCounter.class.getSimpleName()); - runTest(0, 1, new SingleThreadedRefCounter(this::onClose)); + runTest(0, 1, new SingleThreadedRefCounter()); } } @@ -50,12 +62,27 @@ public void noOpRefCounter() { } } + private int goldenRatioStripeIdx(final int stripes) { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; + return (int) idx; + } + + private int threadLocalRandom(final int stripes) { + // TODO In >= Java19, getId() is deprecated, so change to .threadId() + if (stripes <= 0) { + return -1; + } else { + return ThreadLocalRandom.current().nextInt(stripes); + } + } + private void doNoOpRefCounter() { // System.out.println("Running test for " + stripes + " stripes"); final AtomicReference startTime = new AtomicReference<>(null); final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final NoOpRefCounter refCounter = new NoOpRefCounter(this::onClose); + final NoOpRefCounter refCounter = new NoOpRefCounter(); final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { @@ -109,6 +136,16 @@ private void runTest(int stripes, final int threadCount, final RefCounter refCou final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { futures[i] = CompletableFuture.runAsync(() -> { +// if (refCounter instanceof StampedLockRefCounterImpl) { +// final int stripeIdx = ((StampedLockRefCounterImpl) refCounter).getStripeIdx(); +// System.out.printf("stripes: %s, threadId: %s, stripeIdx: %s, goldenRatioStripe: %s, threadLocalRandom: %s%n", +// stripes, +// Thread.currentThread().getId(), +// stripeIdx, +// goldenRatioStripeIdx(stripes), +// threadLocalRandom(stripeIdx)); +// } + // Wait for all threads to be ready countDownThenAwait(startLatch); @@ -164,7 +201,7 @@ void testBehaviour() throws InterruptedException { // Reset the env env = new Object(); // final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); - final RefCounter refCounter = new StampedLockRefCounterImpl(12); + final RefCounter refCounter = new StripedRefCounter(12); final CountDownLatch startLatch = new CountDownLatch(threadCount); final CompletableFuture[] futures = new CompletableFuture[threadCount]; final long[] counts = new long[threadCount]; @@ -209,7 +246,7 @@ void testBehaviour() throws InterruptedException { while (true) { try { // refCounter.close(); - refCounter.doWhenIdle(this::onClose); +// refCounter.doWhenIdle(this::onClose); break; } catch (Env.EnvInUseException e) { Thread.sleep(100); @@ -240,13 +277,13 @@ void testGetCount() throws InterruptedException { final int iterations = 1_000_000; for (int k = 0; k < rounds; k++) { - final int round = k; - System.out.printf("Round %s ----------------------------------------%n", round); +// final int round = k; + System.out.printf("Round %s ----------------------------------------%n", k); // Reset the env env = new Object(); // final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); - final RefCounter refCounter = new StampedLockRefCounterImpl(); + final RefCounter refCounter = new StripedRefCounter(); final CountDownLatch startLatch = new CountDownLatch(threadCount); final CompletableFuture[] futures = new CompletableFuture[threadCount]; final long[] counts = new long[threadCount]; @@ -323,83 +360,4 @@ private void onClose() { env = null; System.out.println(Thread.currentThread() + " - Finishing onClose runnable"); } - - @Test - void testAtomicBitSet() { - final int size = 16; - final AtomicBitSet bitSet = new AtomicBitSet(16); - - assertThat(bitSet.isSet(3)) - .isEqualTo(false); - assertThat(bitSet.countSet()) - .isEqualTo(0); - assertThat(bitSet.flip(3)) - .isEqualTo(true); - assertThat(bitSet.countSet()) - .isEqualTo(1); - assertThat(bitSet.flip(3)) - .isEqualTo(false); - assertThat(bitSet.countSet()) - .isEqualTo(0); - - bitSet.setAndGet(3); - bitSet.setAndGet(10); - assertThat(bitSet.countSet()) - .isEqualTo(2); - bitSet.setAndGet(10); - assertThat(bitSet.countSet()) - .isEqualTo(2); - bitSet.unset(10); - assertThat(bitSet.countSet()) - .isEqualTo(1); - bitSet.unset(10); - assertThat(bitSet.countSet()) - .isEqualTo(1); - - bitSet.unSetAll(); - assertThat(bitSet.countSet()) - .isEqualTo(0); - - System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); - for (int i = 0; i < size; i++) { - bitSet.setAndGet(i); - } - System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); - - bitSet.setAll(); - assertThat(bitSet.countSet()) - .isEqualTo(size); - System.out.println("val: " + bitSet.asLong() + ", bits: " + bitSet); - - bitSet.unSetAll(); - long asLong = bitSet.asLong(); - assertThat(bitSet.getAndSet(5)) - .isEqualTo(asLong); - assertThat(bitSet.getAndSet(5)) - .isNotEqualTo(asLong); - - bitSet.unSetAll(); - assertThat(bitSet.countSet()) - .isEqualTo(0); - assertThat(bitSet.countUnSet()) - .isEqualTo(size); - - bitSet.unSetAll(); - bitSet.setAndGet(3); - bitSet.setAndGet(10); - assertThat(bitSet.countSet()) - .isEqualTo(2); - assertThat(bitSet.countUnSet()) - .isEqualTo(size - 2); - } - - private String longAsBits(long val) { - return Long.toBinaryString(val); - } - - private long getBit(final long val, final long idx) { - return (val >> idx) & 1; - } - - } From 9f7043dc0af159bec8cb4403456fc463f975289c Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:03:15 +0000 Subject: [PATCH 11/61] Tidy code, add tests --- src/main/java/org/lmdbjava/Cursor.java | 4 - src/main/java/org/lmdbjava/Env.java | 15 ++- .../java/org/lmdbjava/StripedRefCounter.java | 11 +- src/main/java/org/lmdbjava/Txn.java | 3 - .../org/lmdbjava/StripedRefCounterTest.java | 108 ++++++++++++++++++ src/test/java/org/lmdbjava/TutorialTest.java | 1 + src/test/java/org/lmdbjava/TxnTest.java | 4 +- 7 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 src/test/java/org/lmdbjava/StripedRefCounterTest.java diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 4036d66b..35cc79f4 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -57,7 +57,6 @@ public final class Cursor implements AutoCloseable { this.ptrCursor = ptr; this.txn = txn; // The env needs to track open cursors to prevent env closure before the cursors are closed - System.out.println("Acquiring for cursor"); this.refCounterReleaser = env.acquire(); this.env = env; this.closed = new AtomicBoolean(false); @@ -87,10 +86,7 @@ public void close() { if (txn.isReadOnly() || txn.isReady()) { LIB.mdb_cursor_close(ptrCursor); } - System.out.println("Closing cursor"); refCounterReleaser.release(); - } else { - System.out.println("Already closed"); } } diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 80ed24e2..70cf14c3 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -154,7 +154,6 @@ public static Env open(final File path, final int size, final EnvFla */ @Override public void close() { -// System.out.println("Closing Env"); refCounter.close(this::closeMdbEnv); } @@ -357,6 +356,15 @@ public boolean isReadOnly() { return readOnly; } + /** + * Indicates if this environment is intended for use by a single thread for its + * entire life. + * @return True if single-threaded + */ + public boolean isSingleThreaded() { + return isSingleThreaded; + } + /** * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. * @@ -681,7 +689,7 @@ public EnvInUseException() { } public EnvInUseException(final int count) { - super("Environment has open " + count + " transactions/cursors so cannot be closed."); + super("Environment has " + count + " open transaction/cursor(s) so cannot be closed."); } } @@ -974,7 +982,8 @@ public Builder addEnvFlags(final Collection envFlags) { } /** - * If set the the {@link Env} will only be used by the same thread for its entire life. + * If set, the caller is asserting that the Env will only be used by a single thread + * throughout its entire life. * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. * using primitives rather than thread-safe objects. * By default, an Env is considered thread-safe. diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index ad289db9..29fe5812 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -1,11 +1,9 @@ package org.lmdbjava; - import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.StampedLock; class StripedRefCounter implements RefCounter { private static final int MAGIC_ZERO_VALUE = Integer.MIN_VALUE; @@ -13,7 +11,6 @@ class StripedRefCounter implements RefCounter { private static final int DEFAULT_STRIPES = 64; private static final int MAX_STRIPES = 256; - private final StampedLock stampedLock; private final AtomicInteger[] counters; private final AtomicBoolean isClosed = new AtomicBoolean(false); /** @@ -22,12 +19,11 @@ class StripedRefCounter implements RefCounter { */ private final int stripeMask; - public StripedRefCounter() { + StripedRefCounter() { this(DEFAULT_STRIPES); } - public StripedRefCounter(final int stripeCount) { - this.stampedLock = new StampedLock(); + StripedRefCounter(final int stripeCount) { validateStripeCount(stripeCount); this.stripeMask = stripeCount - 1; this.counters = new AtomicInteger[stripeCount]; @@ -169,7 +165,7 @@ private void markCountersAsCountInProgress() { } } - private int validateStripeCount(final int stripeCount) { + private void validateStripeCount(final int stripeCount) { if (stripeCount <= 0) { throw new IllegalArgumentException( "Stripe count must be positive, got: " + stripeCount); @@ -183,7 +179,6 @@ private int validateStripeCount(final int stripeCount) { throw new IllegalArgumentException( "Stripe count must be power of 2, got: " + stripeCount); } - return stripeCount; } /** diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index b3a1eeb2..6377f9c4 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -66,7 +66,6 @@ public final class Txn implements AutoCloseable { throw new IncompatibleParent(); } - System.out.println("Acquiring for txn"); this.refCounterReleaser = env.acquire(); try { final Pointer txnPtr = allocateDirect(RUNTIME, ADDRESS); @@ -111,7 +110,6 @@ public void close() { keyVal.close(); state = RELEASED; - System.out.println("Closing Txn"); release(); } @@ -261,7 +259,6 @@ Pointer pointer() { } void release() { - System.out.printf("%s - Txn.release() called%n", Thread.currentThread()); refCounterReleaser.release(); } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java new file mode 100644 index 00000000..b683f736 --- /dev/null +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -0,0 +1,108 @@ +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class StripedRefCounterTest { + + @Test + void acquire() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(); + final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(1); + + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + Assertions.assertThatThrownBy( + () -> { + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + }) + .isInstanceOf(Env.EnvInUseException.class); + assertThat(onCloseCallCount) + .hasValue(0); + + releaser.release(); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(0); + + releaser.release(); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(0); + + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount) + .hasValue(1); + + // Idempotent + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount) + .hasValue(1); + } + + @Test + void multipleThreads() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(); + final int threads = Runtime.getRuntime().availableProcessors(); + final int iterations = 100; + final AtomicInteger[] callCounts = new AtomicInteger[threads]; + for (int i = 0; i < threads; i++) { + callCounts[i] = new AtomicInteger(); + } + + IntStream.range(0, threads) + .boxed() + .map(i -> CompletableFuture.runAsync(() -> { + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); + callCounts[i].getAndIncrement(); + releaser.release(); + } + })) + .forEach(CompletableFuture::join); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(0); + + for (AtomicInteger callCount : callCounts) { + assertThat(callCount) + .hasValue(iterations); + } + } + + @Test + void testImmediateClose() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(); + assertThat(stripedRefCounter.isClosed()) + .isEqualTo(false); + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount) + .hasValue(1); + assertThat(stripedRefCounter.isClosed()) + .isEqualTo(true); + + assertThatThrownBy(stripedRefCounter::checkNotClosed) + .isInstanceOf(Env.AlreadyClosedException.class); + + // Check again as idempotent + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount) + .hasValue(1); + assertThat(stripedRefCounter.isClosed()) + .isEqualTo(true); + + assertThatThrownBy(stripedRefCounter::checkNotClosed) + .isInstanceOf(Env.AlreadyClosedException.class); + } +} diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index c2271363..9da39f90 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -286,6 +286,7 @@ void tutorial3() { tx2.renew(); c.seek(MDB_LAST); + c.close(); tx2.close(); env.close(); } diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 7210b613..e6ca032e 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -135,7 +135,9 @@ void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { - assertThat(roEnv.txnRead()).isNotNull(); + try (Txn readTxn = roEnv.txnRead()) { + assertThat(readTxn).isNotNull(); + } } } From e48f1c7b5477cae24ba967f9fe6f48bce27e8564 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:44:18 +0000 Subject: [PATCH 12/61] Fix bug in StripedRefCounter, improve tests --- .../java/org/lmdbjava/StripedRefCounter.java | 24 ++++- .../java/org/lmdbjava/RefCounterTest.java | 88 +++++++++++++------ .../org/lmdbjava/StripedRefCounterTest.java | 52 +++++++++++ 3 files changed, 136 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 29fe5812..82731fd9 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -45,7 +45,11 @@ public RefCounterReleaser acquire() { // Counting is in progress so we need to get a lock which will likely block // until the count is complete synchronized (this) { - addToCounter(counter, 1); + try { + addToCounter(counter, 1); + } catch (CountInProgressException ex) { + throw new IllegalStateException("Should not happen here as we hold the lock", ex); + } } } return new RefCounterReleaserImpl(this, counter); @@ -73,8 +77,15 @@ public void close(final Runnable onClose) { // will fail, then attempt to get the lock, so will have to wait for us to complete // the count. markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 + +// System.out.println("counters BEFORE: " + Arrays.stream(counters) +// .map(AtomicInteger::get) +// .map(String::valueOf) +// .collect(Collectors.joining(", "))); + try { final int totalCount = sumCounters(); +// System.out.println("totalCount: " + totalCount); if (totalCount == 0) { if (isClosed.compareAndSet(false, true)) { onClose.run(); @@ -91,6 +102,11 @@ public void close(final Runnable onClose) { // Return all counters to their original positive values so // acquire/release can resume as normal markCountersAsNoCountInProgress(); // MAGIC_ZERO_VALUE=>0 else i=>i*-1 + +// System.out.println("counters AFTER: " + Arrays.stream(counters) +// .map(AtomicInteger::get) +// .map(String::valueOf) +// .collect(Collectors.joining(", "))); } } } @@ -135,6 +151,10 @@ private void addToCounter(final AtomicInteger counter, final int delta) { return currVal + delta2; } }); +// System.out.println("delta: " + delta + ", counters: " + Arrays.stream(counters) +// .map(AtomicInteger::get) +// .map(String::valueOf) +// .collect(Collectors.joining(", "))); } private void markCountersAsNoCountInProgress() { @@ -142,7 +162,7 @@ private void markCountersAsNoCountInProgress() { // Multiply value by -1 so we can indicate to other threads that a count is in progress // while maintaining the count. Have to use a special replacement value for zero. counter.updateAndGet(currVal -> { - if (MAGIC_ZERO_VALUE == 0) { + if (currVal == MAGIC_ZERO_VALUE) { return 0; } else { return Math.abs(currVal); diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 135c9e32..1f213751 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,6 +1,9 @@ package org.lmdbjava; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -11,6 +14,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -190,9 +196,10 @@ private void runTest(int stripes, final int threadCount, final RefCounter refCou void testBehaviour() throws InterruptedException { final Random random = new Random(); final int threadCount = this.threadCount - 1; +// final int threadCount = 2; final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); final int rounds = 50; - final int iterations = 100_000_000; + final int iterations = 10_000_000; for (int k = 0; k < rounds; k++) { final int round = k; @@ -201,39 +208,51 @@ void testBehaviour() throws InterruptedException { // Reset the env env = new Object(); // final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); - final RefCounter refCounter = new StripedRefCounter(12); + final RefCounter refCounter = new StripedRefCounter(); final CountDownLatch startLatch = new CountDownLatch(threadCount); final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final long[] counts = new long[threadCount]; + final AtomicLong[] counts = new AtomicLong[threadCount]; + for (int i = 0; i < threadCount; i++) { + counts[i] = new AtomicLong(); + } + + final AtomicBoolean abortThreads = new AtomicBoolean(false); for (int i = 0; i < threadCount; i++) { final int threadIdx = i; futures[threadIdx] = CompletableFuture.runAsync(() -> { // Wait for all threads to be ready countDownThenAwait(startLatch); -// System.out.println(Thread.currentThread() + " - Starting"); + System.out.println(Thread.currentThread() + " - Starting"); for (int j = 0; j < iterations; j++) { + if (j % 100000 == 0) { + System.out.println(Thread.currentThread() + ", j: " + j); + } + if (abortThreads.get()) { + break; + } + final RefCounter.RefCounterReleaser releaser; try { releaser = refCounter.acquire(); - counts[threadIdx]++; + counts[threadIdx].incrementAndGet(); } catch (Env.AlreadyClosedException e) { -// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); break; } try { // Make the work between acquire and release take some time - Thread.sleep(random.nextInt(1)); +// Thread.sleep(random.nextInt(10)); // env is null after closure Objects.requireNonNull(env, "Attempt to use a null env"); - } catch (InterruptedException e) { - throw new RuntimeException(e); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); } finally { releaser.release(); } } -// System.out.println(Thread.currentThread() + " - Done"); + System.out.println(Thread.currentThread() + " - Done"); }, executorService); } @@ -241,30 +260,47 @@ void testBehaviour() throws InterruptedException { startLatch.await(); // Give the other threads a chance to get underway - Thread.sleep(100 + random.nextInt(200)); - - while (true) { + Thread.sleep(1000 + random.nextInt(200)); + final AtomicBoolean didClose = new AtomicBoolean(false); + final AtomicInteger closeCallCount = new AtomicInteger(); + while (!didClose.get()) { try { -// refCounter.close(); -// refCounter.doWhenIdle(this::onClose); - break; + System.out.println("close called"); + refCounter.close(() -> { + System.out.println("onClose called"); + env = null; + didClose.set(true); + closeCallCount.incrementAndGet(); + }); + if (didClose.get()) { + // We closed, so env should be null + assertThat(env) + .isNull(); + } } catch (Env.EnvInUseException e) { - Thread.sleep(100); + // Failed to close so env still alive + assertThat(env) + .isNotNull(); + abortThreads.set(true); + Thread.sleep(1000); } } // Wait for all workers to finish CompletableFuture.allOf(futures).join(); - System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); - - if (refCounter.getCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getCount()); - } - - if (!refCounter.isClosed()) { - throw new IllegalStateException("Env not closed"); - } + System.out.println("Acquire call count: " + Arrays.stream(counts) + .mapToLong(AtomicLong::get) + .sum()); + + assertThat(env) + .isNull(); + assertThat(refCounter.isClosed()) + .isEqualTo(true); + assertThatThrownBy(refCounter::getCount) + .isInstanceOf(Env.AlreadyClosedException.class); + assertThat(closeCallCount) + .hasValue(1); } } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index b683f736..5dc473ef 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -3,7 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Queue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import org.assertj.core.api.Assertions; @@ -79,6 +83,54 @@ void multipleThreads() { } } + @Test + void multipleThreads_delayedRelease() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(); + final int threads = Runtime.getRuntime().availableProcessors() - 2; + final int iterations = 100; + final ExecutorService executor = Executors.newFixedThreadPool(threads); + final ExecutorService executor2 = Executors.newFixedThreadPool(1); + final AtomicInteger[] callCounts = new AtomicInteger[threads]; + for (int i = 0; i < threads; i++) { + callCounts[i] = new AtomicInteger(); + } + + final Queue releasers = new ConcurrentLinkedQueue<>(); + final Queue> futures = new ConcurrentLinkedQueue<>(); + + IntStream.range(0, threads) + .boxed() + .map(i -> CompletableFuture.runAsync(() -> { + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); + releasers.add(releaser); + callCounts[i].getAndIncrement(); + futures.add(CompletableFuture.runAsync(() -> { + final int count = stripedRefCounter.getCount(); +// System.out.println(Thread.currentThread() + " - getting count: " + count); + assertThat(count) + .isNotEqualTo(0); + }, executor2)); + } + }, executor)) + .forEach(CompletableFuture::join); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(threads * iterations); + + for (AtomicInteger callCount : callCounts) { + assertThat(callCount) + .hasValue(iterations); + } + + releasers.forEach(RefCounter.RefCounterReleaser::release); + + futures.forEach(CompletableFuture::join); + + assertThat(stripedRefCounter.getCount()) + .isEqualTo(0); + } + @Test void testImmediateClose() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); From a6080b3d1ac0e3cefa11b5d7b1965af4b29fc637 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:56:06 +0000 Subject: [PATCH 13/61] Rename class --- src/main/java/org/lmdbjava/NoOpRefCounter.java | 13 +++---------- ...pleRefCounterImpl.java => SimpleRefCounter.java} | 4 ++-- src/test/java/org/lmdbjava/RefCounterTest.java | 8 ++++---- 3 files changed, 9 insertions(+), 16 deletions(-) rename src/main/java/org/lmdbjava/{SimpleRefCounterImpl.java => SimpleRefCounter.java} (94%) diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index 857897c5..b8b9f5d9 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -1,10 +1,8 @@ package org.lmdbjava; - public class NoOpRefCounter implements RefCounter { - public NoOpRefCounter() { - } + private boolean isClosed = false; @Override public RefCounterReleaser acquire() { @@ -18,23 +16,18 @@ public void use(Runnable runnable) { @Override public void close(Runnable onClose) { + isClosed = true; // Close with no checks onClose.run(); } @Override public boolean isClosed() { - return false; - } - - @Override - public void checkNotClosed() { - // no-op + return isClosed; } @Override public int getCount() { return 0; } - } diff --git a/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java similarity index 94% rename from src/main/java/org/lmdbjava/SimpleRefCounterImpl.java rename to src/main/java/org/lmdbjava/SimpleRefCounter.java index 4f11b3f9..6a07c3f5 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounterImpl.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -6,12 +6,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -class SimpleRefCounterImpl implements RefCounter { +class SimpleRefCounter implements RefCounter { private final AtomicInteger counter; private final AtomicBoolean isClosed = new AtomicBoolean(false); private final AtomicBoolean preventAcquire = new AtomicBoolean(false); - public SimpleRefCounterImpl() { + public SimpleRefCounter() { this.counter = new AtomicInteger(0); } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 1f213751..93a17685 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -38,8 +38,8 @@ public void perfTest() { IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) .forEach(stripes -> runTest(stripes, new StripedRefCounter(stripes))); - System.out.println("Round: " + i + " " + SimpleRefCounterImpl.class.getSimpleName()); - runTest(0, new SimpleRefCounterImpl()); + System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); + runTest(0, new SimpleRefCounter()); System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); runTest(0, new NoOpRefCounter()); @@ -49,8 +49,8 @@ public void perfTest() { System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); runTest(1, 1, new StripedRefCounter(1)); - System.out.println("Round: " + i + " " + SimpleRefCounterImpl.class.getSimpleName()); - runTest(0, 1, new SimpleRefCounterImpl()); + System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); + runTest(0, 1, new SimpleRefCounter()); System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); runTest(0, 1, new NoOpRefCounter()); From f9138dfcabbc3539bcbb7efd0dd6b1e8d642109e Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:04:01 +0000 Subject: [PATCH 14/61] Refactor test classes --- .../java/org/lmdbjava/RefCounterTest.java | 239 +-------------- .../org/lmdbjava/StripedRefCounterTest.java | 280 +++++++++++++++++- 2 files changed, 280 insertions(+), 239 deletions(-) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 93a17685..2a281980 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,22 +1,13 @@ package org.lmdbjava; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import java.time.Duration; import java.time.Instant; -import java.util.Arrays; import java.util.Objects; -import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -36,27 +27,27 @@ public void perfTest() { System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) - .forEach(stripes -> runTest(stripes, new StripedRefCounter(stripes))); + .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); - runTest(0, new SimpleRefCounter()); + runPerfTest(0, new SimpleRefCounter()); System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); - runTest(0, new NoOpRefCounter()); + runPerfTest(0, new NoOpRefCounter()); System.out.println("Single-threaded tests ---------------------------------"); System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); - runTest(1, 1, new StripedRefCounter(1)); + runPerfTest(1, 1, new StripedRefCounter(1)); System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); - runTest(0, 1, new SimpleRefCounter()); + runPerfTest(0, 1, new SimpleRefCounter()); System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); - runTest(0, 1, new NoOpRefCounter()); + runPerfTest(0, 1, new NoOpRefCounter()); System.out.println("Round: " + i + " " + SingleThreadedRefCounter.class.getSimpleName()); - runTest(0, 1, new SingleThreadedRefCounter()); + runPerfTest(0, 1, new SingleThreadedRefCounter()); } } @@ -68,21 +59,6 @@ public void noOpRefCounter() { } } - private int goldenRatioStripeIdx(final int stripes) { - // TODO In >= Java19, getId() is deprecated, so change to .threadId() - final long idx = (Thread.currentThread().getId() * GOLDEN_RATIO) % stripes; - return (int) idx; - } - - private int threadLocalRandom(final int stripes) { - // TODO In >= Java19, getId() is deprecated, so change to .threadId() - if (stripes <= 0) { - return -1; - } else { - return ThreadLocalRandom.current().nextInt(stripes); - } - } - private void doNoOpRefCounter() { // System.out.println("Running test for " + stripes + " stripes"); @@ -128,11 +104,11 @@ private void doNoOpRefCounter() { + ", iterationsPerSec: " + iterationsPerSec); } - private void runTest(int stripes, final RefCounter refCounter) { - runTest(stripes, threadCount, refCounter); + private void runPerfTest(int stripes, final RefCounter refCounter) { + runPerfTest(stripes, threadCount, refCounter); } - private void runTest(int stripes, final int threadCount, final RefCounter refCounter) { + private void runPerfTest(int stripes, final int threadCount, final RefCounter refCounter) { // System.out.println("Running test for " + stripes + " stripes"); final AtomicReference startTime = new AtomicReference<>(null); @@ -192,195 +168,6 @@ private void runTest(int stripes, final int threadCount, final RefCounter refCou + ", iterationsPerSec: " + iterationsPerSec); } - @Test - void testBehaviour() throws InterruptedException { - final Random random = new Random(); - final int threadCount = this.threadCount - 1; -// final int threadCount = 2; - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int rounds = 50; - final int iterations = 10_000_000; - - for (int k = 0; k < rounds; k++) { - final int round = k; - System.out.printf("Round %s ----------------------------------------%n", round); - - // Reset the env - env = new Object(); -// final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); - final RefCounter refCounter = new StripedRefCounter(); - final CountDownLatch startLatch = new CountDownLatch(threadCount); - final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final AtomicLong[] counts = new AtomicLong[threadCount]; - for (int i = 0; i < threadCount; i++) { - counts[i] = new AtomicLong(); - } - - final AtomicBoolean abortThreads = new AtomicBoolean(false); - - for (int i = 0; i < threadCount; i++) { - final int threadIdx = i; - futures[threadIdx] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); - System.out.println(Thread.currentThread() + " - Starting"); - - for (int j = 0; j < iterations; j++) { - if (j % 100000 == 0) { - System.out.println(Thread.currentThread() + ", j: " + j); - } - if (abortThreads.get()) { - break; - } - - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx].incrementAndGet(); - } catch (Env.AlreadyClosedException e) { - System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time -// Thread.sleep(random.nextInt(10)); - // env is null after closure - Objects.requireNonNull(env, "Attempt to use a null env"); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); - } finally { - releaser.release(); - } - } - System.out.println(Thread.currentThread() + " - Done"); - }, executorService); - } - - // Wait for all threads to start using the ref counter - startLatch.await(); - - // Give the other threads a chance to get underway - Thread.sleep(1000 + random.nextInt(200)); - final AtomicBoolean didClose = new AtomicBoolean(false); - final AtomicInteger closeCallCount = new AtomicInteger(); - while (!didClose.get()) { - try { - System.out.println("close called"); - refCounter.close(() -> { - System.out.println("onClose called"); - env = null; - didClose.set(true); - closeCallCount.incrementAndGet(); - }); - if (didClose.get()) { - // We closed, so env should be null - assertThat(env) - .isNull(); - } - } catch (Env.EnvInUseException e) { - // Failed to close so env still alive - assertThat(env) - .isNotNull(); - abortThreads.set(true); - Thread.sleep(1000); - } - } - - // Wait for all workers to finish - CompletableFuture.allOf(futures).join(); - - System.out.println("Acquire call count: " + Arrays.stream(counts) - .mapToLong(AtomicLong::get) - .sum()); - - assertThat(env) - .isNull(); - assertThat(refCounter.isClosed()) - .isEqualTo(true); - assertThatThrownBy(refCounter::getCount) - .isInstanceOf(Env.AlreadyClosedException.class); - assertThat(closeCallCount) - .hasValue(1); - } - } - - @Test - void testGetCount() throws InterruptedException { - final Random random = new Random(); - final int threadCount = this.threadCount - 1; - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int rounds = 5; - final int iterations = 1_000_000; - - for (int k = 0; k < rounds; k++) { -// final int round = k; - System.out.printf("Round %s ----------------------------------------%n", k); - - // Reset the env - env = new Object(); -// final StripedRefCounterImpl refCounter = new StripedRefCounterImpl(12, this::onClose); - final RefCounter refCounter = new StripedRefCounter(); - final CountDownLatch startLatch = new CountDownLatch(threadCount); - final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final long[] counts = new long[threadCount]; - - for (int i = 0; i < threadCount; i++) { - final int threadIdx = i; - futures[threadIdx] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); -// System.out.println(Thread.currentThread() + " - Starting"); - - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx]++; - } catch (Env.AlreadyClosedException e) { -// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time - Thread.sleep(random.nextInt(1)); - // env is null after closure - Objects.requireNonNull(env, "Attempt to use a null env"); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - releaser.release(); - } - } -// System.out.println(Thread.currentThread() + " - Done"); - }, executorService); - } - - // Wait for all threads to start using the ref counter - startLatch.await(); - - // Give the other threads a chance to get underway - Thread.sleep(100 + random.nextInt(200)); - - for (int i = 0; i < 10; i++) { - try { -// refCounter.close(); - System.out.println("count: " + refCounter.getCount()); - } catch (Env.EnvInUseException e) { - Thread.sleep(100 + random.nextInt(1000)); - } - } - - // Wait for all workers to finish - CompletableFuture.allOf(futures).join(); - - System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); - - if (refCounter.getCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getCount()); - } - } - } - private void countDownThenAwait(final CountDownLatch latch) { latch.countDown(); try { @@ -390,10 +177,4 @@ private void countDownThenAwait(final CountDownLatch latch) { throw new RuntimeException(e); } } - - private void onClose() { - System.out.println(Thread.currentThread() + " - Starting onClose runnable"); - env = null; - System.out.println(Thread.currentThread() + " - Finishing onClose runnable"); - } } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index 5dc473ef..eceb2628 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -3,51 +3,86 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Arrays; +import java.util.Objects; import java.util.Queue; +import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; class StripedRefCounterTest { + private final int threadCount = Runtime.getRuntime().availableProcessors(); + @Test void acquire() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); - + // Acquire twice + final RefCounter.RefCounterReleaser releaser1 = stripedRefCounter.acquire(); assertThat(stripedRefCounter.getCount()) .isEqualTo(1); + final RefCounter.RefCounterReleaser releaser2 = stripedRefCounter.acquire(); + assertThat(stripedRefCounter.getCount()) + .isEqualTo(2); final AtomicInteger onCloseCallCount = new AtomicInteger(); + // Close not called as 2 un-released Assertions.assertThatThrownBy( () -> { stripedRefCounter.close(onCloseCallCount::incrementAndGet); }) - .isInstanceOf(Env.EnvInUseException.class); + .isInstanceOf(Env.EnvInUseException.class) + .hasMessageContaining(" 2 "); assertThat(onCloseCallCount) .hasValue(0); - releaser.release(); + // Release 1st releaser + releaser1.release(); + assertThat(stripedRefCounter.getCount()) + .isEqualTo(1); + + // Close not called as 1 un-released + Assertions.assertThatThrownBy( + () -> { + stripedRefCounter.close(onCloseCallCount::incrementAndGet); + }) + .isInstanceOf(Env.EnvInUseException.class) + .hasMessageContaining(" 1 "); + assertThat(onCloseCallCount) + .hasValue(0); + // Release 2nd releaser + releaser2.release(); assertThat(stripedRefCounter.getCount()) .isEqualTo(0); - releaser.release(); + // no-op if already released + releaser1.release(); + assertThat(stripedRefCounter.getCount()) + .isEqualTo(0); + // no-op if already released + releaser2.release(); assertThat(stripedRefCounter.getCount()) .isEqualTo(0); + // onClose is called now stripedRefCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount) .hasValue(1); - // Idempotent + // no-op as onClose already called stripedRefCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount) .hasValue(1); @@ -56,14 +91,13 @@ void acquire() { @Test void multipleThreads() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - final int threads = Runtime.getRuntime().availableProcessors(); final int iterations = 100; - final AtomicInteger[] callCounts = new AtomicInteger[threads]; - for (int i = 0; i < threads; i++) { + final AtomicInteger[] callCounts = new AtomicInteger[threadCount]; + for (int i = 0; i < threadCount; i++) { callCounts[i] = new AtomicInteger(); } - IntStream.range(0, threads) + IntStream.range(0, threadCount) .boxed() .map(i -> CompletableFuture.runAsync(() -> { for (int j = 0; j < iterations; j++) { @@ -157,4 +191,230 @@ void testImmediateClose() { assertThatThrownBy(stripedRefCounter::checkNotClosed) .isInstanceOf(Env.AlreadyClosedException.class); } + + /** + * Lots of threads all doing acquire/release in a loop, then the main thread + * tries to call refCounter.close(...), which will throw an + * {@link org.lmdbjava.Env.EnvInUseException}. It then makes all worker threads + * stop their looping and calls refCounter.close(...) again, successfully this + * time. + */ + @Test + void testBehaviour() throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int rounds = 10; + final int iterations = 10_000_000; + final AtomicReference mockEnv = new AtomicReference<>(); + + for (int k = 0; k < rounds; k++) { + final int round = k; + System.out.printf("Round %s ----------------------------------------%n", round); + + // Reset the env + mockEnv.set(new Object()); + final RefCounter refCounter = new StripedRefCounter(); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final AtomicLong[] counts = new AtomicLong[threadCount]; + for (int i = 0; i < threadCount; i++) { + counts[i] = new AtomicLong(); + } + + final AtomicBoolean abortThreads = new AtomicBoolean(false); + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); +// System.out.println(Thread.currentThread() + " - Starting"); + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + break; + } + + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx].incrementAndGet(); + } catch (Env.AlreadyClosedException e) { + System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + sleep(random.nextInt(5)); + // env is null after closure + Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + sleep(200 + random.nextInt(200)); + final AtomicBoolean didClose = new AtomicBoolean(false); + int closeCallCount = 0; + final AtomicInteger onCloseCallCount = new AtomicInteger(); + while (!didClose.get()) { + try { + assertThat(mockEnv.get()) + .isNotNull(); + System.out.println("close called " + ++closeCallCount); + refCounter.close(() -> { + onCloseCallCount.incrementAndGet(); + System.out.println("onClose called " + onCloseCallCount.get()); + // Imitate closing the env + mockEnv.set(null); + didClose.set(true); + }); + if (didClose.get()) { + // We closed, so env should be null + assertThat(mockEnv) + .hasNullValue(); + } + } catch (Env.EnvInUseException e) { + // Failed to close as there are un-released items, so env still alive + assertThat(mockEnv.get()) + .isNotNull(); + // Now poke all the treads to make them cleanly finish what they are doing so we + // can try close() again + abortThreads.set(true); + sleep(500); + } + } + + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println("Acquire call count: " + Arrays.stream(counts) + .mapToLong(AtomicLong::get) + .sum()); + + // Make sure the mock env is all closed down + assertThat(mockEnv) + .hasNullValue(); + assertThat(refCounter.isClosed()) + .isEqualTo(true); + assertThatThrownBy(refCounter::getCount) + .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(refCounter::acquire) + .isInstanceOf(Env.AlreadyClosedException.class); + assertThat(onCloseCallCount) + .hasValue(1); + } + } + + /** + * Ensure we can call getCount when multiple threads are all calling acquire/release + * in a loop. + */ + @Test + void testGetCount() throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int rounds = 5; + final int iterations = 10_000_000; + final AtomicReference mockEnv = new AtomicReference<>(); + final AtomicBoolean abortThreads = new AtomicBoolean(false); + + for (int k = 0; k < rounds; k++) { +// final int round = k; + System.out.printf("Round %s ----------------------------------------%n", k); + + // Reset the env + mockEnv.set(new Object()); + abortThreads.set(false); + final RefCounter refCounter = new StripedRefCounter(); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final long[] counts = new long[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = CompletableFuture.runAsync(() -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); +// System.out.println(Thread.currentThread() + " - Starting"); + + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + break; + } + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx]++; + } catch (Env.AlreadyClosedException e) { +// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + sleep(random.nextInt(5)); + // env is null after closure + Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + // Random sleep after releasing so there is a time when the thread + // is not using the 'env' + sleep(5 + random.nextInt(5)); + } +// System.out.println(Thread.currentThread() + " - Done"); + }, executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + sleep(100 + random.nextInt(200)); + + for (int i = 0; i < 10; i++) { + try { + System.out.println("count: " + refCounter.getCount()); + } catch (Env.EnvInUseException e) { + sleep(100 + random.nextInt(200)); + } + } + abortThreads.set(true); + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); + + if (refCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getCount()); + } + } + } + + private void countDownThenAwait(final CountDownLatch latch) { + latch.countDown(); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + private static void sleep(final int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } } From 1b842475b0fe2f65813851c9d69c19dc1ad75bbd Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:15:13 +0000 Subject: [PATCH 15/61] Improve perfTest --- .../java/org/lmdbjava/RefCounterTest.java | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 2a281980..becdbea7 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -10,6 +10,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class RefCounterTest { @@ -19,35 +20,57 @@ public class RefCounterTest { private final int threadCount = Runtime.getRuntime().availableProcessors(); private volatile Object env = new Object(); + @Disabled // Manual performance test @Test public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { - System.out.println("Multi-threaded tests ---------------------------------"); + final int round = i; + System.out.println("Multi-threaded (all cores) tests ---------------------------------"); - System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); - System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, new SimpleRefCounter()); - System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, new NoOpRefCounter()); + IntStream.of(2, 4, 8) + .forEach(threads -> { + System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); + + System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); + IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) + .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); + + System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); + runPerfTest(0, new SimpleRefCounter()); + + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); + runPerfTest(0, new NoOpRefCounter()); + }); + System.out.println("Single-threaded tests ---------------------------------"); - System.out.println("Round: " + i + " " + StripedRefCounter.class.getSimpleName()); - runPerfTest(1, 1, new StripedRefCounter(1)); + System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); + IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) + .forEach(stripes -> runPerfTest(stripes, 1, new StripedRefCounter(stripes))); - System.out.println("Round: " + i + " " + SimpleRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, 1, new SimpleRefCounter()); - System.out.println("Round: " + i + " " + NoOpRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, 1, new NoOpRefCounter()); - System.out.println("Round: " + i + " " + SingleThreadedRefCounter.class.getSimpleName()); + System.out.println("Round: " + round + " " + SingleThreadedRefCounter.class.getSimpleName()); runPerfTest(0, 1, new SingleThreadedRefCounter()); + + + System.out.println("--------------------------------------------------------------------------------"); + System.out.println(); } } @@ -109,28 +132,14 @@ private void runPerfTest(int stripes, final RefCounter refCounter) { } private void runPerfTest(int stripes, final int threadCount, final RefCounter refCounter) { -// System.out.println("Running test for " + stripes + " stripes"); - final AtomicReference startTime = new AtomicReference<>(null); final CompletableFuture[] futures = new CompletableFuture[threadCount]; -// final RefCounter refCounter = new StripedRefCounterImpl(stripes, this::onClose); final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { futures[i] = CompletableFuture.runAsync(() -> { -// if (refCounter instanceof StampedLockRefCounterImpl) { -// final int stripeIdx = ((StampedLockRefCounterImpl) refCounter).getStripeIdx(); -// System.out.printf("stripes: %s, threadId: %s, stripeIdx: %s, goldenRatioStripe: %s, threadLocalRandom: %s%n", -// stripes, -// Thread.currentThread().getId(), -// stripeIdx, -// goldenRatioStripeIdx(stripes), -// threadLocalRandom(stripeIdx)); -// } - // Wait for all threads to be ready countDownThenAwait(startLatch); - // Capture the start time startTime.updateAndGet(currVal -> { if (currVal == null) { @@ -142,14 +151,8 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re for (int j = 0; j < iterations; j++) { final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - try { - // Make sure we have an env that is not 'closed' - Objects.requireNonNull(env); - } finally { - releaser.release(); - } + releaser.release(); } -// System.out.println(Thread.currentThread() + " - Done"); }, executorService); } CompletableFuture.allOf(futures).join(); From 49438e9e2828f2d1ac967a42679d568fb6cfb6c1 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:33:57 +0000 Subject: [PATCH 16/61] Fix NoOpRefCounter close method --- src/main/java/org/lmdbjava/NoOpRefCounter.java | 13 ++++++++----- src/main/java/org/lmdbjava/RefCounter.java | 8 ++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index b8b9f5d9..dc2aad6e 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -1,8 +1,10 @@ package org.lmdbjava; +import java.util.concurrent.atomic.AtomicBoolean; + public class NoOpRefCounter implements RefCounter { - private boolean isClosed = false; + private final AtomicBoolean isClosed = new AtomicBoolean(false); @Override public RefCounterReleaser acquire() { @@ -16,14 +18,15 @@ public void use(Runnable runnable) { @Override public void close(Runnable onClose) { - isClosed = true; - // Close with no checks - onClose.run(); + if (isClosed.compareAndSet(false, true)) { + // Close with no checks + onClose.run(); + } } @Override public boolean isClosed() { - return isClosed; + return isClosed.get(); } @Override diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index d002262f..f50e9567 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -6,8 +6,12 @@ */ interface RefCounter { - RefCounterReleaser NO_OP_RELEASER = () -> { - // No-op + @SuppressWarnings("Convert2Lambda") + RefCounterReleaser NO_OP_RELEASER = new RefCounterReleaser() { + @Override + public void release() { + // No-op + } }; /** From 36019a3f358daa521ba24eb22ecdab8d41b8ba23 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:06:57 +0000 Subject: [PATCH 17/61] Return Cursor.close() to original behavior --- src/main/java/org/lmdbjava/Cursor.java | 16 +++++++++++----- src/test/java/org/lmdbjava/RefCounterTest.java | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 35cc79f4..2d7d79bc 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -73,7 +73,7 @@ public final class Cursor implements AutoCloseable { * Close a cursor handle. * *

The cursor handle will be freed and must not be used again after this call. Its transaction - * must still be live if it is a write-transaction. + * must still be live (i.e. not committed) if it is a write-transaction. */ @Override public void close() { @@ -81,11 +81,17 @@ public void close() { kv.close(); if (SHOULD_CHECK) { env.checkNotClosed(); + if (!txn.isReadOnly()) { + // TODO Rather than throwing if the txn is not in the right state to close + // we could check the txn state and only call mdb_cursor_close if the state is appropriate, + // i.e. (txn.isReadOnly() || txn.isReady()) + // This would make using try-with-resources less likely to fail + + // Cannot close the mdb_cursor if the txn is writable and not in a ready state + txn.checkReady(); + } } - // Cannot close the mdb_cursor if the txn is writable and not in a ready state - if (txn.isReadOnly() || txn.isReady()) { - LIB.mdb_cursor_close(ptrCursor); - } + LIB.mdb_cursor_close(ptrCursor); refCounterReleaser.release(); } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index becdbea7..7847c581 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -26,7 +26,7 @@ public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { final int round = i; - System.out.println("Multi-threaded (all cores) tests ---------------------------------"); + System.out.println("Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) @@ -38,7 +38,7 @@ public void perfTest() { System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, new NoOpRefCounter()); - IntStream.of(2, 4, 8) + IntStream.of(8, 4, 2) .forEach(threads -> { System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); From 3ce28ded7a3d97e1b98817ee9ef5f04fa81aa899 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:21:10 +0000 Subject: [PATCH 18/61] Fix logic in SimpleRefCounter --- src/main/java/org/lmdbjava/Env.java | 2 +- src/main/java/org/lmdbjava/RefCounter.java | 2 +- .../java/org/lmdbjava/SimpleRefCounter.java | 24 ++++++++++++++----- .../lmdbjava/SingleThreadedRefCounter.java | 1 - 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 70cf14c3..4a354226 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -689,7 +689,7 @@ public EnvInUseException() { } public EnvInUseException(final int count) { - super("Environment has " + count + " open transaction/cursor(s) so cannot be closed."); + super("Environment has " + count + " open transaction(s)/cursor(s) so cannot be closed."); } } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index f50e9567..cd7ba3d7 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -45,7 +45,7 @@ default void use(final Runnable runnable) { void close(final Runnable onClose); /** - * @return True if {@link RefCounter} is in a state of {@link EnvState#CLOSED} + * @return True if {@link RefCounter} has been closed. */ boolean isClosed(); diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 6a07c3f5..1abf3265 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -9,7 +9,6 @@ class SimpleRefCounter implements RefCounter { private final AtomicInteger counter; private final AtomicBoolean isClosed = new AtomicBoolean(false); - private final AtomicBoolean preventAcquire = new AtomicBoolean(false); public SimpleRefCounter() { this.counter = new AtomicInteger(0); @@ -30,10 +29,16 @@ public R acquire(final Supplier supplier) { } public RefCounterReleaser acquire() { - if (preventAcquire.get()) { + if (isClosed.get()) { throw new Env.AlreadyClosedException(); } - counter.incrementAndGet(); + counter.updateAndGet(currVal -> { + if (currVal < 0) { + throw new Env.AlreadyClosedException(); + } else { + return currVal + 1; + } + }); return this::release; } @@ -43,8 +48,13 @@ public void close(final Runnable onClose) { if (!isClosed.get()) { final int count = getCount(); if (count == 0) { - if (isClosed.compareAndSet(false, true)) { - onClose.run(); + // Set to -1 to indicate closure + if (counter.compareAndSet(count, -1)) { + if (isClosed.compareAndSet(false, true)) { + onClose.run(); + } + } else { + throw new Env.EnvInUseException(getCount()); } } else { throw new Env.EnvInUseException(count); @@ -53,7 +63,9 @@ public void close(final Runnable onClose) { } private void release() { - // Increment if greater than 0. + if (isClosed.get()) { + throw new Env.AlreadyClosedException(); + } counter.decrementAndGet(); } diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 0c4d355c..3cc8850b 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -7,7 +7,6 @@ public class SingleThreadedRefCounter implements RefCounter { private int refCount; private boolean isClosed = false; - private EnvState envState; public SingleThreadedRefCounter() { } From 8d170c79fafee111982833dfbbdb3e82a583b26d Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:28:17 +0000 Subject: [PATCH 19/61] Improve close() method --- src/main/java/org/lmdbjava/SimpleRefCounter.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 1abf3265..8d71a863 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -46,18 +46,13 @@ public RefCounterReleaser acquire() { public void close(final Runnable onClose) { Objects.requireNonNull(onClose); if (!isClosed.get()) { - final int count = getCount(); - if (count == 0) { - // Set to -1 to indicate closure - if (counter.compareAndSet(count, -1)) { - if (isClosed.compareAndSet(false, true)) { - onClose.run(); - } - } else { - throw new Env.EnvInUseException(getCount()); + // Set to -1 to indicate closure, if the count is 0 + if (counter.compareAndSet(0, -1)) { + if (isClosed.compareAndSet(false, true)) { + onClose.run(); } } else { - throw new Env.EnvInUseException(count); + throw new Env.EnvInUseException(getCount()); } } } From 4b84cf6caa00598f01cf0b836f0bf9a25a168f72 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:30:46 +0000 Subject: [PATCH 20/61] Remove redundant method & ctor from SimpleRefCounter --- src/main/java/org/lmdbjava/SimpleRefCounter.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 8d71a863..ce6bdc44 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -4,30 +4,16 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; class SimpleRefCounter implements RefCounter { - private final AtomicInteger counter; + private final AtomicInteger counter = new AtomicInteger(0); private final AtomicBoolean isClosed = new AtomicBoolean(false); - public SimpleRefCounter() { - this.counter = new AtomicInteger(0); - } - @Override public boolean isClosed() { return isClosed.get(); } - public R acquire(final Supplier supplier) { - acquire(); - try { - return supplier.get(); - } finally { - release(); - } - } - public RefCounterReleaser acquire() { if (isClosed.get()) { throw new Env.AlreadyClosedException(); From a6eb02ba85957aa281d0c67dd36f71fa63537c8e Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:13:22 +0000 Subject: [PATCH 21/61] Improve SimpleRefCounter --- .../java/org/lmdbjava/SimpleRefCounter.java | 23 ++++++++----------- .../java/org/lmdbjava/RefCounterTest.java | 8 +++---- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index ce6bdc44..c8a33617 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -2,24 +2,23 @@ import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; class SimpleRefCounter implements RefCounter { + private static final int CLOSED_VALUE = Integer.MIN_VALUE; private final AtomicInteger counter = new AtomicInteger(0); - private final AtomicBoolean isClosed = new AtomicBoolean(false); @Override public boolean isClosed() { - return isClosed.get(); + return counter.get() == CLOSED_VALUE; } public RefCounterReleaser acquire() { - if (isClosed.get()) { + if (counter.get() == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } counter.updateAndGet(currVal -> { - if (currVal < 0) { + if (currVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } else { return currVal + 1; @@ -31,12 +30,10 @@ public RefCounterReleaser acquire() { @Override public void close(final Runnable onClose) { Objects.requireNonNull(onClose); - if (!isClosed.get()) { - // Set to -1 to indicate closure, if the count is 0 - if (counter.compareAndSet(0, -1)) { - if (isClosed.compareAndSet(false, true)) { - onClose.run(); - } + if (counter.get() != CLOSED_VALUE) { + // Set to CLOSED_VALUE to indicate closure, if the count is 0 + if (counter.compareAndSet(0, CLOSED_VALUE)) { + onClose.run(); } else { throw new Env.EnvInUseException(getCount()); } @@ -44,7 +41,7 @@ public void close(final Runnable onClose) { } private void release() { - if (isClosed.get()) { + if (counter.get() == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } counter.decrementAndGet(); @@ -52,6 +49,6 @@ private void release() { @Override public int getCount() { - return counter.get(); + return Math.max(0, counter.get()); } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 7847c581..1b91ff4f 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -44,14 +44,14 @@ public void perfTest() { System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) - .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); + .forEach(stripes -> runPerfTest(stripes, threads, new StripedRefCounter(stripes))); System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); - runPerfTest(0, new SimpleRefCounter()); + runPerfTest(0, threads, new SimpleRefCounter()); System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); - runPerfTest(0, new NoOpRefCounter()); - }); + runPerfTest(0, threads, new NoOpRefCounter()); + }); System.out.println("Single-threaded tests ---------------------------------"); From ded736e9f590643b003b96b25d8d721191f9226f Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:22:23 +0000 Subject: [PATCH 22/61] Remove redundant EnvState --- src/main/java/org/lmdbjava/EnvState.java | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/main/java/org/lmdbjava/EnvState.java diff --git a/src/main/java/org/lmdbjava/EnvState.java b/src/main/java/org/lmdbjava/EnvState.java deleted file mode 100644 index 9db52d15..00000000 --- a/src/main/java/org/lmdbjava/EnvState.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.lmdbjava; - - -public enum EnvState { - OPEN, - CLOSING, - CLOSED, - ; -} From cb890be14eb0dff5d4a7f84c45243d75bba480b5 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:28:05 +0000 Subject: [PATCH 23/61] Fix SimpleRefCounter.release() --- src/main/java/org/lmdbjava/SimpleRefCounter.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index c8a33617..9483feb8 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -14,9 +14,6 @@ public boolean isClosed() { } public RefCounterReleaser acquire() { - if (counter.get() == CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } counter.updateAndGet(currVal -> { if (currVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); @@ -41,10 +38,13 @@ public void close(final Runnable onClose) { } private void release() { - if (counter.get() == CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } - counter.decrementAndGet(); + counter.updateAndGet(currVal -> { + if (currVal == CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } else { + return currVal - 1; + } + }); } @Override From b61dbf9e2e71a84f28e26f9a3272fdd7b97e1520 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:10:48 +0000 Subject: [PATCH 24/61] Change (Striped|Simple)RefCounter to not throw in lambda Make perf test runs more consistent in terms of work done --- .../java/org/lmdbjava/SimpleRefCounter.java | 28 ++++----- .../java/org/lmdbjava/StripedRefCounter.java | 15 +++-- .../org/lmdbjava/SynchronisedRefCounter.java | 61 +++++++++++++++++++ .../java/org/lmdbjava/RefCounterTest.java | 23 +++++-- 4 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/lmdbjava/SynchronisedRefCounter.java diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 9483feb8..e290d46c 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -14,13 +14,13 @@ public boolean isClosed() { } public RefCounterReleaser acquire() { - counter.updateAndGet(currVal -> { - if (currVal == CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } else { - return currVal + 1; - } - }); + final int newVal = counter.updateAndGet(currVal -> + currVal == CLOSED_VALUE + ? currVal + : currVal + 1); + if (newVal == CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } return this::release; } @@ -38,13 +38,13 @@ public void close(final Runnable onClose) { } private void release() { - counter.updateAndGet(currVal -> { - if (currVal == CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } else { - return currVal - 1; - } - }); + final int newVal = counter.updateAndGet(currVal -> + currVal == CLOSED_VALUE + ? currVal + : currVal - 1); + if (newVal == CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } } @Override diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 82731fd9..4b592208 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -142,15 +142,20 @@ public int getCount() { } private void addToCounter(final AtomicInteger counter, final int delta) { - counter.accumulateAndGet(delta, (currVal, delta2) -> { - if (currVal == MAGIC_CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } else if (currVal < 0) { - throw new CountInProgressException(); + final int newVal = counter.accumulateAndGet(delta, (currVal, delta2) -> { + if (currVal == MAGIC_CLOSED_VALUE || currVal < 0) { + // Leave unchanged so we can throw once accumulateAndGet returns + return currVal; } else { return currVal + delta2; } }); + + if (newVal == MAGIC_CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } else if (newVal < 0) { + throw new CountInProgressException(); + } // System.out.println("delta: " + delta + ", counters: " + Arrays.stream(counters) // .map(AtomicInteger::get) // .map(String::valueOf) diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java new file mode 100644 index 00000000..ab801d71 --- /dev/null +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -0,0 +1,61 @@ +package org.lmdbjava; + + +import java.util.Objects; + +class SynchronisedRefCounter implements RefCounter { + private static final int CLOSED_VALUE = Integer.MIN_VALUE; + private boolean isClosed = false; + private int counter = 0; + + @Override + public boolean isClosed() { + synchronized (this) { + return isClosed; + } + } + + public RefCounterReleaser acquire() { + synchronized (this) { + if (isClosed) { + throw new Env.AlreadyClosedException(); + } + counter++; + } + return this::release; + } + + @Override + public void close(final Runnable onClose) { + Objects.requireNonNull(onClose); + synchronized (this) { + if (!isClosed) { + if (counter != 0) { + throw new Env.EnvInUseException(getCount()); + } else { + isClosed = true; + onClose.run(); + } + } + } + } + + private void release() { + synchronized (this) { + if (isClosed) { + throw new Env.AlreadyClosedException(); + } + if (counter == 0) { + throw new IllegalStateException("Attempt to decrement counter below zero"); + } + counter--; + } + } + + @Override + public int getCount() { + synchronized (this) { + return counter; + } + } +} diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 1b91ff4f..ddc42605 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -14,9 +14,7 @@ import org.junit.jupiter.api.Test; public class RefCounterTest { - private static final long GOLDEN_RATIO = 0x9e3779b9L; - - private final int iterations = 1_000_000; + private final int iterations = 20_000_000; private final int threadCount = Runtime.getRuntime().availableProcessors(); private volatile Object env = new Object(); @@ -35,10 +33,13 @@ public void perfTest() { System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, new SimpleRefCounter()); + System.out.println("Round: " + round + " " + SynchronisedRefCounter.class.getSimpleName()); + runPerfTest(0, new SynchronisedRefCounter()); + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, new NoOpRefCounter()); - IntStream.of(8, 4, 2) + IntStream.of(16, 8, 4, 2) .forEach(threads -> { System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); @@ -49,6 +50,9 @@ public void perfTest() { System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, threads, new SimpleRefCounter()); + System.out.println("Round: " + round + " " + SynchronisedRefCounter.class.getSimpleName()); + runPerfTest(0, threads, new SynchronisedRefCounter()); + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, threads, new NoOpRefCounter()); }); @@ -62,6 +66,9 @@ public void perfTest() { System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, 1, new SimpleRefCounter()); + System.out.println("Round: " + round + " " + SynchronisedRefCounter.class.getSimpleName()); + runPerfTest(0, 1, new SynchronisedRefCounter()); + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, 1, new NoOpRefCounter()); @@ -90,6 +97,7 @@ private void doNoOpRefCounter() { final NoOpRefCounter refCounter = new NoOpRefCounter(); final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int iterationsPerThread = iterations / threadCount; for (int i = 0; i < threadCount; i++) { futures[i] = CompletableFuture.runAsync(() -> { // Wait for all threads to be ready @@ -104,7 +112,7 @@ private void doNoOpRefCounter() { } }); - for (int j = 0; j < iterations; j++) { + for (int j = 0; j < iterationsPerThread; j++) { final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); try { // Make sure we have an env that is not 'closed' @@ -123,6 +131,7 @@ private void doNoOpRefCounter() { System.out.println("All Finished" + ", threads: " + threadCount + + ", iterationsPerThread: " + iterationsPerThread + ", duration: " + duration + ", iterationsPerSec: " + iterationsPerSec); } @@ -136,6 +145,7 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re final CompletableFuture[] futures = new CompletableFuture[threadCount]; final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final int iterationsPerThread = iterations / threadCount; for (int i = 0; i < threadCount; i++) { futures[i] = CompletableFuture.runAsync(() -> { // Wait for all threads to be ready @@ -149,7 +159,7 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re } }); - for (int j = 0; j < iterations; j++) { + for (int j = 0; j < iterationsPerThread; j++) { final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); releaser.release(); } @@ -167,6 +177,7 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re System.out.println("All Finished" + ", stripes: " + stripes + ", threads: " + threadCount + + ", iterationsPerThread: " + iterationsPerThread + ", duration: " + duration + ", iterationsPerSec: " + iterationsPerSec); } From e2e7d5bf628f2199ce06884cefafb7ed0ce5602f Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:59:41 +0000 Subject: [PATCH 25/61] Add JMH benchmark for RefCounter --- pom.xml | 13 ++ .../org/lmdbjava/RefCounterBenchmark.java | 137 ++++++++++++++++++ .../java/org/lmdbjava/RefCounterTest.java | 7 +- 3 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/lmdbjava/RefCounterBenchmark.java diff --git a/pom.xml b/pom.xml index 20a47038..87c8a199 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ 1.28.0 33.5.0-jre 0.8.14 + 1.37 0.10.4 2.2.18 5.14.1 @@ -125,6 +126,18 @@ ${mockito.version} test + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + diff --git a/src/test/java/org/lmdbjava/RefCounterBenchmark.java b/src/test/java/org/lmdbjava/RefCounterBenchmark.java new file mode 100644 index 00000000..443e1894 --- /dev/null +++ b/src/test/java/org/lmdbjava/RefCounterBenchmark.java @@ -0,0 +1,137 @@ +package org.lmdbjava; + + +import org.jspecify.annotations.NonNull; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +public class RefCounterBenchmark { + + private static final int ITERATIONS = 2; + private static final int WARMUP = 2; + private static final int FORK = 2; + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Measurement(iterations = ITERATIONS) + @Warmup(iterations = WARMUP) + @Fork(value = FORK, warmups = WARMUP) + @Threads(Threads.MAX) + public void allThreads(final MultiThreadPlan plan, final Blackhole blackhole) { + final RefCounter.RefCounterReleaser releaser = plan.refCounter.acquire(); + blackhole.consume(releaser); + releaser.release(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Measurement(iterations = ITERATIONS) + @Warmup(iterations = WARMUP) + @Fork(value = FORK, warmups = WARMUP) + @Threads(8) + public void eightThreads(final MultiThreadPlan plan, final Blackhole blackhole) { + final RefCounter.RefCounterReleaser releaser = plan.refCounter.acquire(); + blackhole.consume(releaser); + releaser.release(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Measurement(iterations = ITERATIONS) + @Warmup(iterations = WARMUP) + @Fork(value = FORK, warmups = WARMUP) + @Threads(4) + public void fourThreads(final MultiThreadPlan plan, final Blackhole blackhole) { + final RefCounter.RefCounterReleaser releaser = plan.refCounter.acquire(); + blackhole.consume(releaser); + releaser.release(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Measurement(iterations = ITERATIONS) + @Warmup(iterations = WARMUP) + @Fork(value = FORK, warmups = WARMUP) + @Threads(2) + public void twoThreads(final MultiThreadPlan plan, final Blackhole blackhole) { + final RefCounter.RefCounterReleaser releaser = plan.refCounter.acquire(); + blackhole.consume(releaser); + releaser.release(); + } + + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Measurement(iterations = ITERATIONS) + @Warmup(iterations = WARMUP) + @Fork(value = FORK, warmups = WARMUP) + @Threads(1) + public void oneThread(final SingleThreadPlan plan, final Blackhole blackhole) { + final RefCounter.RefCounterReleaser releaser = plan.refCounter.acquire(); + blackhole.consume(releaser); + releaser.release(); + } + + private static @NonNull RefCounter getRefCounter(final String refCounterName) { + final RefCounter refCounter; + switch (refCounterName) { + case "striped": + refCounter = new StripedRefCounter(); + break; + case "simple": + refCounter = new SimpleRefCounter(); + break; + case "synchronised": + refCounter = new SynchronisedRefCounter(); + break; + case "no-op": + refCounter = new NoOpRefCounter(); + break; + case "single": + refCounter = new SingleThreadedRefCounter(); + break; + default: + throw new IllegalArgumentException("Unknown name '" + refCounterName + "'"); + } + return refCounter; + } + + @State(Scope.Benchmark) + public static class MultiThreadPlan { + + private RefCounter refCounter; + + @Param({"striped", "simple", "synchronised", "no-op"}) + public String refCounterName; + + @Setup(Level.Invocation) + public void setUp() { + this.refCounter = getRefCounter(refCounterName); + } + } + + @State(Scope.Benchmark) + public static class SingleThreadPlan { + + private RefCounter refCounter; + + @Param({"striped", "simple", "synchronised", "no-op", "single"}) + public String refCounterName; + + @Setup(Level.Invocation) + public void setUp() { + this.refCounter = getRefCounter(refCounterName); + } + } +} diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index ddc42605..a428b5ff 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -27,7 +27,7 @@ public void perfTest() { System.out.println("Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) + IntStream.of(1, 16, 64) .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); @@ -44,7 +44,7 @@ public void perfTest() { System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) + IntStream.of(1, 16, 64) .forEach(stripes -> runPerfTest(stripes, threads, new StripedRefCounter(stripes))); System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); @@ -60,8 +60,7 @@ public void perfTest() { System.out.println("Single-threaded tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 2, 4, 8, 16, 32, 64, 128) - .forEach(stripes -> runPerfTest(stripes, 1, new StripedRefCounter(stripes))); + runPerfTest(1, 1, new StripedRefCounter()); System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, 1, new SimpleRefCounter()); From 5a5b8bfe80f9e247c0c5bf7a206ecee61053db07 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:02:47 +0100 Subject: [PATCH 26/61] gh-279 Fix concurrency issues in StripedRefCounter --- src/main/java/org/lmdbjava/Env.java | 2 +- .../java/org/lmdbjava/NoOpRefCounter.java | 2 +- src/main/java/org/lmdbjava/RefCounter.java | 5 +- .../java/org/lmdbjava/SimpleRefCounter.java | 2 +- .../lmdbjava/SingleThreadedRefCounter.java | 4 +- .../java/org/lmdbjava/StripedRefCounter.java | 219 ++++++++++++------ .../org/lmdbjava/SynchronisedRefCounter.java | 2 +- .../java/org/lmdbjava/RefCounterTest.java | 28 ++- .../org/lmdbjava/StripedRefCounterTest.java | 129 ++++++++++- 9 files changed, 297 insertions(+), 96 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 4a354226..16cad9f0 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -688,7 +688,7 @@ public EnvInUseException() { super("Environment has open transactions/cursors so cannot be closed."); } - public EnvInUseException(final int count) { + public EnvInUseException(final long count) { super("Environment has " + count + " open transaction(s)/cursor(s) so cannot be closed."); } } diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index dc2aad6e..84a9243e 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -30,7 +30,7 @@ public boolean isClosed() { } @Override - public int getCount() { + public long getCount() { return 0; } } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index cd7ba3d7..9e7bf4cf 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -38,9 +38,8 @@ default void use(final Runnable runnable) { * If the reference count is zero, onClose will be called. This {@link RefCounter} will be marked * as closed so all future calls to acquire will throw a {@link org.lmdbjava.Env.AlreadyClosedException}. * If the count is non-zero, {@link org.lmdbjava.Env.EnvInUseException} will be thrown. + * If already closed, this is a no-op. * @throws org.lmdbjava.Env.EnvInUseException If the {@link Env} has open transactions/cursors. - * @throws org.lmdbjava.Env.AlreadyClosedException If this {@link RefCounter} has already been - * successfully closed. */ void close(final Runnable onClose); @@ -62,7 +61,7 @@ default void checkNotClosed() { * @return The current count of items in use. * @throws org.lmdbjava.Env.AlreadyClosedException If called after it has been successfully closed. */ - int getCount(); + long getCount(); @FunctionalInterface interface RefCounterReleaser { diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index e290d46c..b429877d 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -48,7 +48,7 @@ private void release() { } @Override - public int getCount() { + public long getCount() { return Math.max(0, counter.get()); } } diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 3cc8850b..04b0ce3b 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -41,7 +41,7 @@ public void use(Runnable runnable) { public void close(final Runnable onClose) { Objects.requireNonNull(onClose); if (!isClosed) { - final int count = getCount(); + final long count = getCount(); if (count == 0) { isClosed = true; onClose.run(); @@ -57,7 +57,7 @@ public boolean isClosed() { } @Override - public int getCount() { + public long getCount() { return refCount; } diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 4b592208..5fa3c6c3 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -6,8 +6,10 @@ import java.util.concurrent.atomic.AtomicReference; class StripedRefCounter implements RefCounter { + private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); private static final int MAGIC_ZERO_VALUE = Integer.MIN_VALUE; private static final int MAGIC_CLOSED_VALUE = Integer.MAX_VALUE; + private static final int MAX_COUNTER_VALUE = Integer.MAX_VALUE - 1; private static final int DEFAULT_STRIPES = 64; private static final int MAX_STRIPES = 256; @@ -20,7 +22,7 @@ class StripedRefCounter implements RefCounter { private final int stripeMask; StripedRefCounter() { - this(DEFAULT_STRIPES); + this(Math.min(lowestPowerOfTwoGreaterThanOrEqualTo(PROCESSOR_COUNT), DEFAULT_STRIPES)); } StripedRefCounter(final int stripeCount) { @@ -32,89 +34,126 @@ class StripedRefCounter implements RefCounter { } } + public int getStripeCount() { + return counters.length; + } + @Override public boolean isClosed() { return isClosed.get(); } + @Override public RefCounterReleaser acquire() { final AtomicInteger counter = counters[getStripeIdx()]; - try { - addToCounter(counter, 1); - } catch (final CountInProgressException e) { - // Counting is in progress so we need to get a lock which will likely block + if (!addToCounter(counter, Delta.PLUS_ONE)) { + // Counting is in progress, so we need to get a lock which will likely block // until the count is complete synchronized (this) { - try { - addToCounter(counter, 1); - } catch (CountInProgressException ex) { - throw new IllegalStateException("Should not happen here as we hold the lock", ex); + if (!addToCounter(counter, Delta.PLUS_ONE)) { + throw new IllegalStateException("Count should not be in progress while we hold the lock"); } } } return new RefCounterReleaserImpl(this, counter); } + private static int getDefaultStripeCount() { + return Math.min( + MAX_STRIPES, + Math.max( + lowestPowerOfTwoGreaterThanOrEqualTo(PROCESSOR_COUNT * 2), + DEFAULT_STRIPES)); + } + + // Pkg private for testing + + /** + * Returns the highest power of two that is less than or equal to {@code value}. + * + * @param value input value, must be positive + * @return highest power of two <= value + * @throws IllegalArgumentException if {@code value <= 0} + */ + static int highestPowerOfTwoLessThanOrEqualTo(final int value) { + if (value <= 0) { + throw new IllegalArgumentException("Value must be positive, got: " + value); + } + return Integer.highestOneBit(value); + } + + /** + * Returns the lowest power of two that is greater than or equal to {@code value}. + * + * @param value input value, must be positive + * @return lowest power of two >= value + * @throws IllegalArgumentException if {@code value <= 0} or the result would overflow an int + */ + static int lowestPowerOfTwoGreaterThanOrEqualTo(final int value) { + if (value <= 0) { + throw new IllegalArgumentException("Value must be positive, got: " + value); + } + if (value > (1 << 30)) { + throw new IllegalArgumentException( + "Value is too large to round up to a positive int power of two, got: " + value); + } + return value == 1 ? 1 : Integer.highestOneBit(value - 1) << 1; + } + private void release(final AtomicInteger counter) { - try { - addToCounter(counter, -1); - } catch (final CountInProgressException e) { - // Counting is in progress so we need to get a lock which will likely block - // until the count is complete + if (!addToCounter(counter, Delta.MINUS_ONE)) { synchronized (this) { - addToCounter(counter, -1); + if (!addToCounter(counter, Delta.MINUS_ONE)) { + throw new IllegalStateException("Count should not be in progress while we hold the lock"); + } } } } @Override public void close(final Runnable onClose) { - if (!isClosed.get()) { - Objects.requireNonNull(onClose); + Objects.requireNonNull(onClose); - synchronized (this) { - // Once we have marked all counters, any threads trying to mutate the counters - // will fail, then attempt to get the lock, so will have to wait for us to complete - // the count. - markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 - -// System.out.println("counters BEFORE: " + Arrays.stream(counters) -// .map(AtomicInteger::get) -// .map(String::valueOf) -// .collect(Collectors.joining(", "))); - - try { - final int totalCount = sumCounters(); -// System.out.println("totalCount: " + totalCount); - if (totalCount == 0) { - if (isClosed.compareAndSet(false, true)) { - onClose.run(); - // Mark all counters as closed to prevent any future acquire calls - for (AtomicInteger counter : counters) { - counter.set(MAGIC_CLOSED_VALUE); - } - } - } else { - throw new Env.EnvInUseException(totalCount); - } - } finally { - if (!isClosed.get()) { - // Return all counters to their original positive values so - // acquire/release can resume as normal - markCountersAsNoCountInProgress(); // MAGIC_ZERO_VALUE=>0 else i=>i*-1 - -// System.out.println("counters AFTER: " + Arrays.stream(counters) -// .map(AtomicInteger::get) -// .map(String::valueOf) -// .collect(Collectors.joining(", "))); + // close is idempotent so silently drop out + if (isClosed.get()) { + return; + } + + synchronized (this) { + if (isClosed.get()) { + return; + } + + // Once we have marked all counters, any threads trying to mutate the counters + // will fail, then attempt to get the lock, so will have to wait for us to complete + // the count. + markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 + + try { + final long totalCount = sumCounters(); + if (totalCount == 0) { + // Only mark as closed if the runnable succeeds. + onClose.run(); + isClosed.set(true); + // Mark all counters as closed to prevent any future acquire() calls + for (AtomicInteger counter : counters) { + counter.set(MAGIC_CLOSED_VALUE); } + } else { + throw new Env.EnvInUseException(totalCount); + } + } finally { + if (!isClosed.get()) { + // Return all counters to their original positive values so + // acquire/release can resume as normal + markCountersAsNoCountInProgress(); // MAGIC_ZERO_VALUE=>0 else i=>i*-1 } } } } - private int sumCounters() { - int totalCount = 0; + private long sumCounters() { + long totalCount = 0; for (AtomicInteger counter : counters) { int count = counter.get(); if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE @@ -128,9 +167,11 @@ private int sumCounters() { return totalCount; } - public int getCount() { + @Override + public long getCount() { checkNotClosed(); synchronized (this) { + checkNotClosed(); // This will stop any other thread from incrementing/decrementing the counter markCountersAsCountInProgress(); try { @@ -141,27 +182,36 @@ public int getCount() { } } - private void addToCounter(final AtomicInteger counter, final int delta) { - final int newVal = counter.accumulateAndGet(delta, (currVal, delta2) -> { - if (currVal == MAGIC_CLOSED_VALUE || currVal < 0) { - // Leave unchanged so we can throw once accumulateAndGet returns - return currVal; - } else { - return currVal + delta2; + /** + * @return False if a count is in progress, else true + * @throws Env.AlreadyClosedException If this {@link RefCounter} has already been + * successfully closed. + */ + private boolean addToCounter(final AtomicInteger counter, final Delta delta) { + while (true) { + final int currVal = counter.get(); + + if (currVal == MAGIC_CLOSED_VALUE) { + throw new Env.AlreadyClosedException(); + } else if (currVal < 0) { + // A count is in progress + return false; + } else if (currVal == MAX_COUNTER_VALUE && delta == Delta.PLUS_ONE) { + throw new IllegalStateException("Reference count overflow"); + } else if (currVal == 0 && delta == Delta.MINUS_ONE) { + throw new IllegalStateException("Reference count underflow"); } - }); - if (newVal == MAGIC_CLOSED_VALUE) { - throw new Env.AlreadyClosedException(); - } else if (newVal < 0) { - throw new CountInProgressException(); + final int newVal = currVal + delta.deltaValue; + if (counter.compareAndSet(currVal, newVal)) { + return true; + } } -// System.out.println("delta: " + delta + ", counters: " + Arrays.stream(counters) -// .map(AtomicInteger::get) -// .map(String::valueOf) -// .collect(Collectors.joining(", "))); } + /** + * Must be called while holding the lock on this object. + */ private void markCountersAsNoCountInProgress() { for (AtomicInteger counter : counters) { // Multiply value by -1 so we can indicate to other threads that a count is in progress @@ -169,6 +219,10 @@ private void markCountersAsNoCountInProgress() { counter.updateAndGet(currVal -> { if (currVal == MAGIC_ZERO_VALUE) { return 0; + } else if (currVal == MAGIC_CLOSED_VALUE) { + // If this method is used correctly under lock, we should never see this value, but preserve the + // closed state just in case + return MAGIC_CLOSED_VALUE; } else { return Math.abs(currVal); } @@ -176,12 +230,19 @@ private void markCountersAsNoCountInProgress() { } } + /** + * Must be called while holding the lock on this object. + */ private void markCountersAsCountInProgress() { for (AtomicInteger counter : counters) { counter.updateAndGet(currVal -> { if (currVal == 0) { - // Use a magic value to mark this zero value counter as having a count in progress + // Use a magic value to mark this zero-value counter as having a count in progress return MAGIC_ZERO_VALUE; + } else if (currVal == MAGIC_CLOSED_VALUE) { + // If this method is used correctly under lock, we should never see this value, but preserve the + // closed state just in case + return MAGIC_CLOSED_VALUE; } else { // Make the value negative to indicate a count in progress return Math.abs(currVal) * -1; @@ -255,11 +316,15 @@ public void release() { } } - /** - * Thrown when an attempt is made to mutate a counter while a sum of all counters - * is being taken. - */ - private static class CountInProgressException extends RuntimeException { + private enum Delta { + PLUS_ONE(1), + MINUS_ONE(-1), + ; + + private final int deltaValue; + Delta(int deltaValue) { + this.deltaValue = deltaValue; + } } } diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index ab801d71..36a2e98f 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -53,7 +53,7 @@ private void release() { } @Override - public int getCount() { + public long getCount() { synchronized (this) { return counter; } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index a428b5ff..19f92f09 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,6 +1,7 @@ package org.lmdbjava; +import java.text.NumberFormat; import java.time.Duration; import java.time.Instant; import java.util.Objects; @@ -14,8 +15,9 @@ import org.junit.jupiter.api.Test; public class RefCounterTest { + private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); private final int iterations = 20_000_000; - private final int threadCount = Runtime.getRuntime().availableProcessors(); + private final int threadCount = PROCESSOR_COUNT; private volatile Object env = new Object(); @Disabled // Manual performance test @@ -24,12 +26,16 @@ public void perfTest() { // Do multiple rounds to let it warm up for (int i = 1; i <= 3; i++) { final int round = i; + // Run tests with all available processors System.out.println("Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 16, 64) + IntStream.of(1, 16, 32, 64, 128, 256) .forEach(stripes -> runPerfTest(stripes, new StripedRefCounter(stripes))); + final StripedRefCounter defaultStripedRefCounter = new StripedRefCounter(); + runPerfTest(defaultStripedRefCounter.getStripeCount(), defaultStripedRefCounter); + System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); runPerfTest(0, new SimpleRefCounter()); @@ -39,12 +45,15 @@ public void perfTest() { System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, new NoOpRefCounter()); - IntStream.of(16, 8, 4, 2) + + + // Run tests with set numbers of worker threads + IntStream.of(32, 16, 8, 4, 2) .forEach(threads -> { System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 16, 64) + IntStream.of(1, 16, 32, 64, 128, 256) .forEach(stripes -> runPerfTest(stripes, threads, new StripedRefCounter(stripes))); System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); @@ -57,6 +66,8 @@ public void perfTest() { runPerfTest(0, threads, new NoOpRefCounter()); }); + + System.out.println("Single-threaded tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); @@ -126,13 +137,13 @@ private void doNoOpRefCounter() { CompletableFuture.allOf(futures).join(); final Duration duration = Duration.between(startTime.get(), Instant.now()); - final double iterationsPerSec = (double) iterations / duration.toMillis() * 1000; + final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); System.out.println("All Finished" + ", threads: " + threadCount + ", iterationsPerThread: " + iterationsPerThread + ", duration: " + duration - + ", iterationsPerSec: " + iterationsPerSec); + + ", iterationsPerSec: " + NumberFormat.getInstance().format(iterationsPerSec)); } private void runPerfTest(int stripes, final RefCounter refCounter) { @@ -171,14 +182,15 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re } final Duration duration = Duration.between(startTime.get(), Instant.now()); - final double iterationsPerSec = (double) iterations / duration.toMillis() * 1000; + final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); + System.out.println("All Finished" + ", stripes: " + stripes + ", threads: " + threadCount + ", iterationsPerThread: " + iterationsPerThread + ", duration: " + duration - + ", iterationsPerSec: " + iterationsPerSec); + + ", iterationsPerSec: " + NumberFormat.getInstance().format(iterationsPerSec)); } private void countDownThenAwait(final CountDownLatch latch) { diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index eceb2628..1667e0e8 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -140,7 +140,7 @@ void multipleThreads_delayedRelease() { releasers.add(releaser); callCounts[i].getAndIncrement(); futures.add(CompletableFuture.runAsync(() -> { - final int count = stripedRefCounter.getCount(); + final long count = stripedRefCounter.getCount(); // System.out.println(Thread.currentThread() + " - getting count: " + count); assertThat(count) .isNotEqualTo(0); @@ -150,7 +150,7 @@ void multipleThreads_delayedRelease() { .forEach(CompletableFuture::join); assertThat(stripedRefCounter.getCount()) - .isEqualTo(threads * iterations); + .isEqualTo((long) threads * iterations); for (AtomicInteger callCount : callCounts) { assertThat(callCount) @@ -399,6 +399,131 @@ void testGetCount() throws InterruptedException { } } + @Test + void getCountRacingWithCloseDoesNotReturnZeroAfterClose() { + final StripedRefCounter refCounter = new StripedRefCounter(); + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + refCounter.close(onCloseCallCount::incrementAndGet); + + assertThatThrownBy(refCounter::getCount) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + @Test + void failedOnCloseDoesNotCloseOrCorruptCounter() { + final StripedRefCounter refCounter = new StripedRefCounter(); + + assertThatThrownBy(() -> refCounter.close(() -> { + throw new RuntimeException("boom"); + })).isInstanceOf(RuntimeException.class); + + assertThat(refCounter.isClosed()).isFalse(); + + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + assertThat(refCounter.getCount()).isEqualTo(1); + releaser.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + } + + @Test + void concurrentCloseIsIdempotent() { + final StripedRefCounter refCounter = new StripedRefCounter(); + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + final CountDownLatch startLatch = new CountDownLatch(2); + + final CompletableFuture first = CompletableFuture.runAsync(() -> { + countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); + final CompletableFuture second = CompletableFuture.runAsync(() -> { + countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); + + CompletableFuture.allOf(first, second).join(); + + assertThat(onCloseCallCount).hasValue(1); + assertThat(refCounter.isClosed()).isTrue(); + assertThatThrownBy(refCounter::acquire) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + @Test + void highestPowerOfTwoLessThanOrEqualTo() { + // Test powers of two + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1)) + .isEqualTo(1); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(2)) + .isEqualTo(2); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(4)) + .isEqualTo(4); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(8)) + .isEqualTo(8); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(16)) + .isEqualTo(16); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1024)) + .isEqualTo(1024); + + // Test non-powers of two + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(3)) + .isEqualTo(2); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(5)) + .isEqualTo(4); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(7)) + .isEqualTo(4); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(15)) + .isEqualTo(8); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(100)) + .isEqualTo(64); + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1000)) + .isEqualTo(512); + + // Test edge cases + assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(Integer.MAX_VALUE)) + .isEqualTo(1073741824); + } + + @Test + void lowestPowerOfTwoGreaterThanOrEqualTo() { + // Test powers of two + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1)) + .isEqualTo(1); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(2)) + .isEqualTo(2); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(4)) + .isEqualTo(4); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(8)) + .isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(16)) + .isEqualTo(16); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1024)) + .isEqualTo(1024); + + // Test non-powers of two + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(3)) + .isEqualTo(4); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(5)) + .isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(7)) + .isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(15)) + .isEqualTo(16); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(24)) + .isEqualTo(32); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(100)) + .isEqualTo(128); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1000)) + .isEqualTo(1024); + + // Test edge cases + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(536870912)) + .isEqualTo(536870912); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(536870913)) + .isEqualTo(1073741824); + } + private void countDownThenAwait(final CountDownLatch latch) { latch.countDown(); try { From 37dfbdb160ad4a34696b506b7cf88de3fa0da97b Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:19:10 +0100 Subject: [PATCH 27/61] gh-279 Tidy code, add tests --- src/main/java/org/lmdbjava/Env.java | 28 +- .../java/org/lmdbjava/NoOpRefCounter.java | 12 +- src/main/java/org/lmdbjava/RefCounter.java | 3 + src/main/java/org/lmdbjava/Txn.java | 8 +- src/test/java/org/lmdbjava/EnvTest.java | 529 ++++++++++-------- 5 files changed, 322 insertions(+), 258 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 16cad9f0..0ab11b61 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -77,6 +77,9 @@ public final class Env implements AutoCloseable { private final boolean readOnly; private final Path path; private final EnvFlagSet envFlagSet; + /** + * True if this Env has been created on the basis of only ever being used by a single thread. + */ private final boolean isSingleThreaded; private Env( @@ -144,21 +147,24 @@ public static Builder create(final BufferProxy proxy) { */ @Deprecated public static Env open(final File path, final int size, final EnvFlags... flags) { - return new Builder<>(PROXY_OPTIMAL).setMapSize(size, ByteUnit.MEBIBYTES).open(path, flags); + return new Builder<>(PROXY_OPTIMAL) + .setMapSize(size, ByteUnit.MEBIBYTES) + .setEnvFlags(flags) + .open(path, flags); } /** * Close the handle. * *

Will silently return if already closed or never opened. + * + * @throws EnvInUseException if a {@link Txn}, {@link Cursor} or {@link Dbi} is still open on this + * {@link Env}. */ @Override public void close() { - refCounter.close(this::closeMdbEnv); - } - - private void closeMdbEnv() { - LIB.mdb_env_close(ptr); + refCounter.close(() -> + LIB.mdb_env_close(ptr)); } /** @@ -359,6 +365,7 @@ public boolean isReadOnly() { /** * Indicates if this environment is intended for use by a single thread for its * entire life. + * * @return True if single-threaded */ public boolean isSingleThreaded() { @@ -646,6 +653,14 @@ public int readerCheck() { return resultPtr.intValue(); } + /** + * Acquire a permit to use this {@link Env}. + * Holding the permit will prevent the {@link Env} from being closed before it is released. + * + * @return A {@link org.lmdbjava.RefCounter.RefCounterReleaser} for releasing the permit once the use + * of this {@link Env} is complete. + * @throws AlreadyClosedException if this Env is already closed. + */ RefCounter.RefCounterReleaser acquire() { return refCounter.acquire(); } @@ -987,6 +1002,7 @@ public Builder addEnvFlags(final Collection envFlags) { * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. * using primitives rather than thread-safe objects. * By default, an Env is considered thread-safe. + * * @return this builder instance. */ public Builder singleThreaded() { diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index 84a9243e..ad0e7f21 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -2,22 +2,30 @@ import java.util.concurrent.atomic.AtomicBoolean; +/** + * Preforms no reference counting at all, but will throw a Env.AlreadyClosedException + * if the Env is closed when {@link NoOpRefCounter#acquire()} is called. + */ public class NoOpRefCounter implements RefCounter { private final AtomicBoolean isClosed = new AtomicBoolean(false); @Override public RefCounterReleaser acquire() { + if (isClosed.get()) { + throw new Env.AlreadyClosedException(); + } + return RefCounter.NO_OP_RELEASER; } @Override - public void use(Runnable runnable) { + public void use(final Runnable runnable) { runnable.run(); } @Override - public void close(Runnable onClose) { + public void close(final Runnable onClose) { if (isClosed.compareAndSet(false, true)) { // Close with no checks onClose.run(); diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index 9e7bf4cf..6027b542 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -66,6 +66,9 @@ default void checkNotClosed() { @FunctionalInterface interface RefCounterReleaser { + /** + * Call this after using the {@link RefCounter} controlled object. + */ void release(); } } diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 6377f9c4..3d668bdd 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -88,6 +88,8 @@ public void abort() { checkReady(); state = DONE; LIB.mdb_txn_abort(ptr); + + // TODO It is not clear whether this method should call refCounterReleaser.release() like close does } /** @@ -110,7 +112,7 @@ public void close() { keyVal.close(); state = RELEASED; - release(); + refCounterReleaser.release(); } /** Commits this transaction. */ @@ -258,10 +260,6 @@ Pointer pointer() { return ptr; } - void release() { - refCounterReleaser.release(); - } - /** Transaction must abort, has a child, or is invalid. */ public static final class BadException extends LmdbNativeException { diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 69a5ea4e..50639786 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -50,7 +50,9 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** Test {@link Env}. */ +/** + * Test {@link Env}. + */ public final class EnvTest { private TempDir tempDir; @@ -69,11 +71,11 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } @@ -82,142 +84,142 @@ void byteUnit() { @Test void cannotChangeMapSizeAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMapSize(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMapSize(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangePermissionsAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setFilePermissions(0664); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setFilePermissions(0664); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangeMaxDbsAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxDbs(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxDbs(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangeMaxReadersAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxReaders(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxReaders(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotInfoOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.info(); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.info(); + }) .isInstanceOf(AlreadyClosedException.class); } @Test void cannotOpenTwice() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - builder.open(file).close(); - //noinspection resource // This will fail to open - builder.open(file); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + builder.open(file).close(); + //noinspection resource // This will fail to open + builder.open(file); + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void cannotStatOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.stat(); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.stat(); + }) .isInstanceOf(AlreadyClosedException.class); } @Test void cannotSyncOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.sync(false); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.sync(false); + }) .isInstanceOf(AlreadyClosedException.class); } @@ -250,52 +252,52 @@ void copyDirectoryBased_noFlags() { @Test void copyDirectoryRejectsFileDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - FileUtil.deleteDir(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + FileUtil.deleteDir(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @Test void copyDirectoryRejectsMissingDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - Files.delete(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + try { + Files.delete(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @Test void copyDirectoryRejectsNonEmptyDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - final Path subDir = dest.resolve("hello"); - Files.createDirectory(subDir); - assertThat(Files.isDirectory(subDir)).isTrue(); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + try { + final Path subDir = dest.resolve("hello"); + Files.createDirectory(subDir); + assertThat(Files.isDirectory(subDir)).isTrue(); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @@ -313,16 +315,16 @@ void copyFileBased() { @Test void copyFileRejectsExistingDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempFile(); - Files.createFile(dest); - assertThat(Files.exists(dest)).isTrue(); - final Path src = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) + () -> { + final Path dest = tempDir.createTempFile(); + Files.createFile(dest); + assertThat(Files.exists(dest)).isTrue(); + final Path src = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @@ -341,12 +343,12 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -355,14 +357,14 @@ void createAsFile() { @Test void detectTransactionThreadViolation() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { - env.txnRead(); - env.txnRead(); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + env.txnRead(); + env.txnRead(); + } + }) .isInstanceOf(BadReaderLockException.class); } @@ -370,12 +372,12 @@ void detectTransactionThreadViolation() { void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -392,34 +394,34 @@ void info() { @Test void mapFull() { assertThatThrownBy( - () -> { - final Path dir = tempDir.createTempDir(); - final byte[] k = new byte[500]; - final ByteBuffer key = allocateDirect(500); - final ByteBuffer val = allocateDirect(1_024); - final Random rnd = new Random(); - try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } - }) + () -> { + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } + }) .isInstanceOf(MapFullException.class); } @@ -432,7 +434,7 @@ void readOnlySupported() { rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -454,7 +456,7 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { + Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -474,15 +476,15 @@ void setMapSize() { assertThat(mapFullExThrown).isTrue(); assertThatThrownBy( - () -> { - env.setMapSize(-1, ByteUnit.KIBIBYTES); - }) + () -> { + env.setMapSize(-1, ByteUnit.KIBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy( - () -> { - env.setMapSize(-1); - }) + () -> { + env.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); env.setMapSize(1024, ByteUnit.KIBIBYTES); @@ -592,13 +594,13 @@ void testDefaultOpenNoName2() { void addEnvFlag() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -610,16 +612,16 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -631,13 +633,13 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -650,17 +652,17 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -672,13 +674,13 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -692,13 +694,14 @@ void setEnvFlags_null1() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) {} + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -710,13 +713,14 @@ void setEnvFlags_null2() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) {} + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -728,14 +732,49 @@ void setEnvFlags_null3() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) {} + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } + + @Test + void closeWithOpenReadTxn() { + final Path file = tempDir.createTempFile(); + @SuppressWarnings("resource") final Env env = Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + // Open but don't close + final Txn readTxn = env.txnWrite(); + + Assertions.assertThatThrownBy(env::close) + .isInstanceOf(Env.EnvInUseException.class); + } + + @Test + void closeWithOpenWriteTxn() { + final Path file = tempDir.createTempFile(); + @SuppressWarnings("resource") final Env env = Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + // Open but don't close + final Txn writeTxn = env.txnWrite(); + + Assertions.assertThatThrownBy(env::close) + .isInstanceOf(Env.EnvInUseException.class); + } } From 519479f400d8c6031bc28119c94b485ce5fc3cc0 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:25:13 +0100 Subject: [PATCH 28/61] gh-279 Add Env.Builder.safeClose() --- src/main/java/org/lmdbjava/Env.java | 123 +++++++++++++++--- src/main/java/org/lmdbjava/RefCounter.java | 2 +- .../java/org/lmdbjava/StripedRefCounter.java | 89 +++++++++---- .../org/lmdbjava/StripedRefCounterTest.java | 35 ----- 4 files changed, 167 insertions(+), 82 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 0ab11b61..608c27b1 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -30,6 +30,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -81,6 +82,7 @@ public final class Env implements AutoCloseable { * True if this Env has been created on the basis of only ever being used by a single thread. */ private final boolean isSingleThreaded; + private final boolean safeClose; private Env( final BufferProxy proxy, @@ -89,7 +91,8 @@ private Env( final boolean noSubDir, final Path path, final EnvFlagSet envFlagSet, - final boolean isSingleThreaded) { + final boolean isSingleThreaded, + final boolean safeClose) { this.proxy = proxy; this.readOnly = readOnly; this.noSubDir = noSubDir; @@ -99,12 +102,13 @@ private Env( this.path = path; this.envFlagSet = envFlagSet; this.isSingleThreaded = isSingleThreaded; + this.safeClose = safeClose; this.refCounter = initRefCounter(isSingleThreaded); } private RefCounter initRefCounter(boolean isSingleThreaded) { final RefCounter refCounter; - if (SHOULD_CHECK) { + if (safeClose) { if (isSingleThreaded) { refCounter = new SingleThreadedRefCounter(); } else { @@ -158,13 +162,40 @@ public static Env open(final File path, final int size, final EnvFla * *

Will silently return if already closed or never opened. * - * @throws EnvInUseException if a {@link Txn}, {@link Cursor} or {@link Dbi} is still open on this - * {@link Env}. + *

Before and during this call, the caller MUST ensure that: + * + *

    + *
  • every {@link Txn}, {@link Cursor} obtained from this environment has + * already been closed; and + *
  • no other thread is executing any operation on this environment or on a handle + * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as + * {@code Dbi.get}. + *
+ * + *

Violating this contract is undefined behaviour that can crash the whole JVM + * ({@code SIGSEGV} on Linux/macOS, {@code EXCEPTION_ACCESS_VIOLATION 0xC0000005} on Windows); it + * does not raise a Java exception. The underlying {@code mdb_env_close} unmaps the + * memory map, so a transaction still being started or used on another thread then dereferences + * freed memory — typically observed as a native crash in {@code mdb_txn_renew0} / {@code + * mdb_txn_begin}. + * + *

If you must close an environment while reader threads may still be active, serialise the + * close against those readers in application code: e.g. a read/write lock where each reader holds + * the read lock for the entire duration of its transaction and {@code close()} holds the write + * lock, so the map is never unmapped while a read is in flight. + * + *

If safeClose has been enabled, {@link Env#close()} will throw a {@link EnvInUseException} if + * transactions or cursors are still active. + * + * @throws EnvInUseException if a {@link Txn} or {@link Cursor} is still open on this {@link Env}. */ @Override public void close() { - refCounter.close(() -> - LIB.mdb_env_close(ptr)); + refCounter.close(this::closeEnv); + } + + public void closeEnv() { + LIB.mdb_env_close(ptr); } /** @@ -372,6 +403,10 @@ public boolean isSingleThreaded() { return isSingleThreaded; } + boolean isSafeClose() { + return safeClose; + } + /** * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. * @@ -752,10 +787,11 @@ public static final class Builder { private long mapSize = MAP_SIZE_DEFAULT; private int maxDbs = 1; private int maxReaders = MAX_READERS_DEFAULT; - private boolean opened; + private boolean opened = false; private final BufferProxy proxy; private int mode = POSIX_MODE_DEFAULT; private boolean singleThreaded = false; + private boolean safeClose = false; private final AbstractFlagSet.Builder flagSetBuilder = EnvFlagSet.builder(); @@ -764,6 +800,12 @@ public static final class Builder { this.proxy = proxy; } + private void checkEnvNotOpened() { + if (opened) { + throw new AlreadyOpenException(); + } + } + /** * Opens the environment. * @@ -831,7 +873,7 @@ public Env open(final Path path) { final boolean readOnly = flags.isSet(MDB_RDONLY_ENV); final boolean noSubDir = flags.isSet(MDB_NOSUBDIR); checkRc(LIB.mdb_env_open(ptr, path.toAbsolutePath().toString(), flags.getMask(), mode)); - return new Env<>(proxy, ptr, readOnly, noSubDir, path, flags, singleThreaded); + return new Env<>(proxy, ptr, readOnly, noSubDir, path, flags, singleThreaded, safeClose); } catch (final LmdbNativeException e) { LIB.mdb_env_close(ptr); throw e; @@ -845,9 +887,7 @@ public Env open(final Path path) { * @return the builder */ public Builder setMapSize(final long mapSize) { - if (opened) { - throw new AlreadyOpenException(); - } + checkEnvNotOpened(); if (mapSize < 0) { throw new IllegalArgumentException("Negative value; overflow?"); } @@ -877,9 +917,7 @@ public Builder setMapSize(final long mapSize, final ByteUnit byteUnit) { * @return the builder */ public Builder setMaxDbs(final int dbs) { - if (opened) { - throw new AlreadyOpenException(); - } + checkEnvNotOpened(); this.maxDbs = dbs; return this; } @@ -891,9 +929,7 @@ public Builder setMaxDbs(final int dbs) { * @return the builder */ public Builder setMaxReaders(final int readers) { - if (opened) { - throw new AlreadyOpenException(); - } + checkEnvNotOpened(); this.maxReaders = readers; return this; } @@ -906,9 +942,7 @@ public Builder setMaxReaders(final int readers) { * @return the builder */ public Builder setFilePermissions(final int mode) { - if (opened) { - throw new AlreadyOpenException(); - } + checkEnvNotOpened(); this.mode = mode; return this; } @@ -921,6 +955,7 @@ public Builder setFilePermissions(final int mode) { * @return this builder instance. */ public Builder setEnvFlags(final Collection envFlags) { + checkEnvNotOpened(); flagSetBuilder.clear(); if (envFlags != null) { envFlags.stream().filter(Objects::nonNull).forEach(flagSetBuilder::addFlag); @@ -936,6 +971,7 @@ public Builder setEnvFlags(final Collection envFlags) { * @return this builder instance. */ public Builder setEnvFlags(final EnvFlags... envFlags) { + checkEnvNotOpened(); flagSetBuilder.clear(); if (envFlags != null) { Arrays.stream(envFlags).filter(Objects::nonNull).forEach(this.flagSetBuilder::addFlag); @@ -951,6 +987,7 @@ public Builder setEnvFlags(final EnvFlags... envFlags) { * @return this builder instance. */ public Builder setEnvFlags(final EnvFlagSet envFlagSet) { + checkEnvNotOpened(); flagSetBuilder.clear(); if (envFlagSet != null) { this.flagSetBuilder.setFlags(envFlagSet.getFlags()); @@ -965,6 +1002,7 @@ public Builder setEnvFlags(final EnvFlagSet envFlagSet) { * @return this builder instance. */ public Builder addEnvFlag(final EnvFlags envFlag) { + checkEnvNotOpened(); this.flagSetBuilder.addFlag(envFlag); return this; } @@ -976,6 +1014,7 @@ public Builder addEnvFlag(final EnvFlags envFlag) { * @return this builder instance. */ public Builder addEnvFlags(final EnvFlagSet envFlagSet) { + checkEnvNotOpened(); if (envFlagSet != null) { flagSetBuilder.addFlags(envFlagSet.getFlags()); } @@ -990,6 +1029,7 @@ public Builder addEnvFlags(final EnvFlagSet envFlagSet) { * @return this builder instance. */ public Builder addEnvFlags(final Collection envFlags) { + checkEnvNotOpened(); if (envFlags != null) { flagSetBuilder.addFlags(envFlags); } @@ -1006,9 +1046,52 @@ public Builder addEnvFlags(final Collection envFlags) { * @return this builder instance. */ public Builder singleThreaded() { + checkEnvNotOpened(); singleThreaded = true; return this; } + + /** + * If set to true, the caller is asserting that the Env will only be used by a single thread + * throughout its entire life. + * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. + * using primitives rather than thread-safe objects. + * By default, an Env is considered thread-safe. + * + * @return this builder instance. + */ + public Builder singleThreaded(final boolean singleThreaded) { + checkEnvNotOpened(); + this.singleThreaded = singleThreaded; + return this; + } + + /** + * See {@link Env.Builder#setSafeClose(boolean)} + */ + public Builder setSafeClose() { + checkEnvNotOpened(); + return setSafeClose(true); + } + + /** + * Enables the opt-in "safe close" for the resulting {@link Env}. + * + *

When enabled, the environment tracks its live transactions and cursors so that closure of the + * {@link Env} is prevented if transactions or cursors are active. This adds a small amount of + * bookkeeping on transaction start/close; it is disabled by default so + * applications that already manage their own threading (the common low-latency case) pay nothing. + * When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if transactions or + * cursors are active. + * + * @param safeClose true to enable transaction tracking and {@link Env#close(Duration)} + * @return the builder + */ + public Builder setSafeClose(final boolean safeClose) { + checkEnvNotOpened(); + this.safeClose = safeClose; + return this; + } } /** diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index 6027b542..d6607b2b 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -59,7 +59,7 @@ default void checkNotClosed() { /** * @return The current count of items in use. - * @throws org.lmdbjava.Env.AlreadyClosedException If called after it has been successfully closed. + * It will return 0 if already closed. */ long getCount(); diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 5fa3c6c3..1a072f71 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -7,10 +7,23 @@ class StripedRefCounter implements RefCounter { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); + /** + * Counter value used to indicate a count of zero while a sum of all counters is being + * performed. + */ private static final int MAGIC_ZERO_VALUE = Integer.MIN_VALUE; + /** + * Counter value used to indicate that this RefCounter has been closed. + */ private static final int MAGIC_CLOSED_VALUE = Integer.MAX_VALUE; + /** + * The maximum possible count value on one stripe. + */ private static final int MAX_COUNTER_VALUE = Integer.MAX_VALUE - 1; private static final int DEFAULT_STRIPES = 64; + /** + * Maximum number of stripes. + */ private static final int MAX_STRIPES = 256; private final AtomicInteger[] counters; @@ -22,7 +35,7 @@ class StripedRefCounter implements RefCounter { private final int stripeMask; StripedRefCounter() { - this(Math.min(lowestPowerOfTwoGreaterThanOrEqualTo(PROCESSOR_COUNT), DEFAULT_STRIPES)); + this(getDefaultStripeCount()); } StripedRefCounter(final int stripeCount) { @@ -66,22 +79,6 @@ private static int getDefaultStripeCount() { DEFAULT_STRIPES)); } - // Pkg private for testing - - /** - * Returns the highest power of two that is less than or equal to {@code value}. - * - * @param value input value, must be positive - * @return highest power of two <= value - * @throws IllegalArgumentException if {@code value <= 0} - */ - static int highestPowerOfTwoLessThanOrEqualTo(final int value) { - if (value <= 0) { - throw new IllegalArgumentException("Value must be positive, got: " + value); - } - return Integer.highestOneBit(value); - } - /** * Returns the lowest power of two that is greater than or equal to {@code value}. * @@ -97,7 +94,9 @@ static int lowestPowerOfTwoGreaterThanOrEqualTo(final int value) { throw new IllegalArgumentException( "Value is too large to round up to a positive int power of two, got: " + value); } - return value == 1 ? 1 : Integer.highestOneBit(value - 1) << 1; + return value == 1 + ? 1 + : Integer.highestOneBit(value - 1) << 1; } private void release(final AtomicInteger counter) { @@ -124,19 +123,25 @@ public void close(final Runnable onClose) { return; } - // Once we have marked all counters, any threads trying to mutate the counters - // will fail, then attempt to get the lock, so will have to wait for us to complete - // the count. + // Once we have marked all counters as count-in-progress, any threads trying to mutate the counters + // will fail, then re-attempt under lock, so will have to wait for us to complete the count. + // Marking all the counters is a non-atomic operation, so another thread may increment a counter + // while we are in the middle of marking them, however, once all are marked, threads will be blocked + // from decrementing until we have called markCountersAsNoCountInProgress(), thus we will get a non-zero + // count and throw an EnvInUseException. + markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 + // At this point, no other thread can mutate the counters, so we are safe to use a sum of all the counters. try { final long totalCount = sumCounters(); if (totalCount == 0) { - // Only mark as closed if the runnable succeeds. + // No permits on loan so safe to close. onClose.run(); + // Only mark as closed if the runnable succeeds. isClosed.set(true); // Mark all counters as closed to prevent any future acquire() calls - for (AtomicInteger counter : counters) { + for (final AtomicInteger counter : counters) { counter.set(MAGIC_CLOSED_VALUE); } } else { @@ -152,6 +157,10 @@ public void close(final Runnable onClose) { } } + /** + * MUST be called after {@link StripedRefCounter#markCountersAsCountInProgress()} has been called and under + * lock. Once complete, {@link StripedRefCounter#markCountersAsNoCountInProgress()} must be called. + */ private long sumCounters() { long totalCount = 0; for (AtomicInteger counter : counters) { @@ -159,6 +168,10 @@ private long sumCounters() { if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE throw new Env.AlreadyClosedException(); } else if (count != MAGIC_ZERO_VALUE) { // Integer.MIN_VALUE + // count should be negative at this point + if (count > 0) { + throw new IllegalStateException("Count should be negative at this point, got: " + count); + } totalCount += count; } } @@ -169,9 +182,13 @@ private long sumCounters() { @Override public long getCount() { - checkNotClosed(); + if (isClosed()) { + return 0; + } synchronized (this) { - checkNotClosed(); + if (isClosed()) { + return 0; + } // This will stop any other thread from incrementing/decrementing the counter markCountersAsCountInProgress(); try { @@ -188,15 +205,19 @@ public long getCount() { * successfully closed. */ private boolean addToCounter(final AtomicInteger counter, final Delta delta) { + // Use a while loop with get() and compareAndSet(), rather than throwing exceptions inside + // updateAndGet(). while (true) { final int currVal = counter.get(); if (currVal == MAGIC_CLOSED_VALUE) { + // Once MAGIC_CLOSED_VALUE is set, it is never mutated again. throw new Env.AlreadyClosedException(); } else if (currVal < 0) { - // A count is in progress + // A count is in progress, so we can drop out and try again under lock return false; } else if (currVal == MAX_COUNTER_VALUE && delta == Delta.PLUS_ONE) { + // This implies we have a LOT of txns/cursors open, should never happen throw new IllegalStateException("Reference count overflow"); } else if (currVal == 0 && delta == Delta.MINUS_ONE) { throw new IllegalStateException("Reference count underflow"); @@ -234,6 +255,12 @@ private void markCountersAsNoCountInProgress() { * Must be called while holding the lock on this object. */ private void markCountersAsCountInProgress() { + // It is possible that another thread will call acquire() while we are mid-loop. + // If that thread uses a counter that has not yet been marked as count-in-progress, they will + // succeed with incrementing the counter. + // We will then get a sum that includes the increment from their acquire() call. + // They will be blocked from calling release() until markCountersAsNoCountInProgress() has + // been called by us. for (AtomicInteger counter : counters) { counter.updateAndGet(currVal -> { if (currVal == 0) { @@ -327,4 +354,14 @@ private enum Delta { this.deltaValue = deltaValue; } } + + private static class Stripe { + private final StripedRefCounter stRefCounter; + private final AtomicInteger counter; + + Stripe(StripedRefCounter stRefCounter) { + this.stRefCounter = stRefCounter; + this.counter = new AtomicInteger(); + } + } } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index 1667e0e8..82e76378 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -450,41 +450,6 @@ void concurrentCloseIsIdempotent() { .isInstanceOf(Env.AlreadyClosedException.class); } - @Test - void highestPowerOfTwoLessThanOrEqualTo() { - // Test powers of two - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1)) - .isEqualTo(1); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(2)) - .isEqualTo(2); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(4)) - .isEqualTo(4); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(8)) - .isEqualTo(8); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(16)) - .isEqualTo(16); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1024)) - .isEqualTo(1024); - - // Test non-powers of two - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(3)) - .isEqualTo(2); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(5)) - .isEqualTo(4); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(7)) - .isEqualTo(4); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(15)) - .isEqualTo(8); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(100)) - .isEqualTo(64); - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(1000)) - .isEqualTo(512); - - // Test edge cases - assertThat(StripedRefCounter.highestPowerOfTwoLessThanOrEqualTo(Integer.MAX_VALUE)) - .isEqualTo(1073741824); - } - @Test void lowestPowerOfTwoGreaterThanOrEqualTo() { // Test powers of two From 8c9c54432dbb4886cc724014ffb1a0c033026a05 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:26:13 +0100 Subject: [PATCH 29/61] gh-279 Fix tests --- .../java/org/lmdbjava/CursorIterable.java | 2 + src/main/java/org/lmdbjava/Env.java | 3 +- src/main/java/org/lmdbjava/RefCounter.java | 12 +- .../java/org/lmdbjava/StripedRefCounter.java | 73 +++-- src/main/java/org/lmdbjava/Txn.java | 5 +- .../java/org/lmdbjava/CursorIterableTest.java | 13 +- src/test/java/org/lmdbjava/CursorTest.java | 249 ++++++++-------- src/test/java/org/lmdbjava/EnvTest.java | 8 + .../org/lmdbjava/StripedRefCounterTest.java | 8 +- src/test/java/org/lmdbjava/TutorialTest.java | 96 +++--- src/test/java/org/lmdbjava/TxnTest.java | 277 ++++++++---------- 11 files changed, 373 insertions(+), 373 deletions(-) diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index 65fc1023..569baf53 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -38,6 +38,8 @@ * *

An instance will create and close its own cursor. * + *

Not thread safe.

+ * * @param buffer type */ public final class CursorIterable implements Iterable>, AutoCloseable { diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 608c27b1..8e32e586 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -739,7 +739,8 @@ public EnvInUseException() { } public EnvInUseException(final long count) { - super("Environment has " + count + " open transaction(s)/cursor(s) so cannot be closed."); + super("Environment has " + count + " open transaction(s)/cursor(s) so cannot be closed. " + + "Close them then retry."); } } diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index d6607b2b..9966d8ab 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -6,16 +6,13 @@ */ interface RefCounter { - @SuppressWarnings("Convert2Lambda") - RefCounterReleaser NO_OP_RELEASER = new RefCounterReleaser() { - @Override - public void release() { - // No-op - } + RefCounterReleaser NO_OP_RELEASER = () -> { + // No-op }; /** * Call this before using the {@link RefCounter} controlled object. + * * @return A {@link RefCounterReleaser} to release once the work is complete */ RefCounterReleaser acquire(); @@ -28,7 +25,7 @@ default void use(final Runnable runnable) { final RefCounterReleaser releaser = acquire(); try { runnable.run(); - } finally { + } finally { releaser.release(); } } @@ -39,6 +36,7 @@ default void use(final Runnable runnable) { * as closed so all future calls to acquire will throw a {@link org.lmdbjava.Env.AlreadyClosedException}. * If the count is non-zero, {@link org.lmdbjava.Env.EnvInUseException} will be thrown. * If already closed, this is a no-op. + * * @throws org.lmdbjava.Env.EnvInUseException If the {@link Env} has open transactions/cursors. */ void close(final Runnable onClose); diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 1a072f71..4c3d9397 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -3,7 +3,6 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; class StripedRefCounter implements RefCounter { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); @@ -26,7 +25,7 @@ class StripedRefCounter implements RefCounter { */ private static final int MAX_STRIPES = 256; - private final AtomicInteger[] counters; + private final Stripe[] counters; private final AtomicBoolean isClosed = new AtomicBoolean(false); /** * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). @@ -41,9 +40,9 @@ class StripedRefCounter implements RefCounter { StripedRefCounter(final int stripeCount) { validateStripeCount(stripeCount); this.stripeMask = stripeCount - 1; - this.counters = new AtomicInteger[stripeCount]; + this.counters = new Stripe[stripeCount]; for (int i = 0; i < stripeCount; i++) { - counters[i] = new AtomicInteger(0); + counters[i] = new Stripe(this); } } @@ -56,9 +55,18 @@ public boolean isClosed() { return isClosed.get(); } + private AtomicInteger getCounterForThisThread() { + return counters[getStripeIdx()].counter; + } + + private Stripe getStripeForThisThread() { + return counters[getStripeIdx()]; + } + @Override public RefCounterReleaser acquire() { - final AtomicInteger counter = counters[getStripeIdx()]; + final Stripe stripe = getStripeForThisThread(); + final AtomicInteger counter = stripe.counter; if (!addToCounter(counter, Delta.PLUS_ONE)) { // Counting is in progress, so we need to get a lock which will likely block // until the count is complete @@ -68,7 +76,7 @@ public RefCounterReleaser acquire() { } } } - return new RefCounterReleaserImpl(this, counter); + return stripe.createReleaser(); } private static int getDefaultStripeCount() { @@ -141,8 +149,8 @@ public void close(final Runnable onClose) { // Only mark as closed if the runnable succeeds. isClosed.set(true); // Mark all counters as closed to prevent any future acquire() calls - for (final AtomicInteger counter : counters) { - counter.set(MAGIC_CLOSED_VALUE); + for (final Stripe stripe : counters) { + stripe.counter.set(MAGIC_CLOSED_VALUE); } } else { throw new Env.EnvInUseException(totalCount); @@ -163,8 +171,8 @@ public void close(final Runnable onClose) { */ private long sumCounters() { long totalCount = 0; - for (AtomicInteger counter : counters) { - int count = counter.get(); + for (Stripe stripe : counters) { + int count = stripe.counter.get(); if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE throw new Env.AlreadyClosedException(); } else if (count != MAGIC_ZERO_VALUE) { // Integer.MIN_VALUE @@ -234,10 +242,10 @@ private boolean addToCounter(final AtomicInteger counter, final Delta delta) { * Must be called while holding the lock on this object. */ private void markCountersAsNoCountInProgress() { - for (AtomicInteger counter : counters) { + for (Stripe stripe : counters) { // Multiply value by -1 so we can indicate to other threads that a count is in progress // while maintaining the count. Have to use a special replacement value for zero. - counter.updateAndGet(currVal -> { + stripe.counter.updateAndGet(currVal -> { if (currVal == MAGIC_ZERO_VALUE) { return 0; } else if (currVal == MAGIC_CLOSED_VALUE) { @@ -261,8 +269,8 @@ private void markCountersAsCountInProgress() { // We will then get a sum that includes the increment from their acquire() call. // They will be blocked from calling release() until markCountersAsNoCountInProgress() has // been called by us. - for (AtomicInteger counter : counters) { - counter.updateAndGet(currVal -> { + for (final Stripe stripe : counters) { + stripe.counter.updateAndGet(currVal -> { if (currVal == 0) { // Use a magic value to mark this zero-value counter as having a count in progress return MAGIC_ZERO_VALUE; @@ -322,27 +330,6 @@ private int getStripeIdx() { return (int) ((threadId ^ (threadId >>> 31)) & stripeMask); } - private static class RefCounterReleaserImpl implements RefCounterReleaser { - - private final AtomicReference refCounterRef; - private final AtomicInteger counter; - - private RefCounterReleaserImpl(final StripedRefCounter refCounter, - final AtomicInteger counter) { - this.refCounterRef = new AtomicReference<>(refCounter); - this.counter = counter; - } - - @Override - public void release() { - // Prevent duplicate release calls - final StripedRefCounter refCounter = refCounterRef.getAndSet(null); - if (refCounter != null) { - refCounter.release(counter); - } - } - } - private enum Delta { PLUS_ONE(1), MINUS_ONE(-1), @@ -356,12 +343,22 @@ private enum Delta { } private static class Stripe { - private final StripedRefCounter stRefCounter; + private final StripedRefCounter stripedRefCounter; private final AtomicInteger counter; - Stripe(StripedRefCounter stRefCounter) { - this.stRefCounter = stRefCounter; + Stripe(StripedRefCounter stripedRefCounter) { + this.stripedRefCounter = stripedRefCounter; this.counter = new AtomicInteger(); } + + RefCounterReleaser createReleaser() { + final AtomicBoolean hasReleased = new AtomicBoolean(false); + return () -> { + // Prevent duplicate release calls + if (hasReleased.compareAndSet(false, true)) { + stripedRefCounter.release(counter); + } + }; + } } } diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 3d668bdd..7598ccf0 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -93,7 +93,7 @@ public void abort() { } /** - * Closes this transaction by aborting if not already committed. + * Closes this transaction. Any uncommitted work will be aborted first. * *

Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for * each read-only buffer (ie the key and value). @@ -311,7 +311,8 @@ public static final class NotReadyException extends LmdbException { /** Creates a new instance. */ public NotReadyException() { - super("Transaction is not in ready state"); + super("Transaction is not in ready state, i.e. it has been closed/committed/aborted/reset. " + + "You may see this if have you tried to close a cursor after committing the transaction?"); } } diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 43663d40..697378fb 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -115,12 +115,13 @@ private void populateTestDataList() { } private void populateDatabase(final Dbi dbi) { - try (Txn txn = env.txnWrite(); - final Cursor cursor = dbi.openCursor(txn)) { - cursor.put(bb(2), bb(3), MDB_NOOVERWRITE); - cursor.put(bb(4), bb(5)); - cursor.put(bb(6), bb(7)); - cursor.put(bb(8), bb(9)); + try (Txn txn = env.txnWrite()) { + try (final Cursor cursor = dbi.openCursor(txn)) { + cursor.put(bb(2), bb(3), MDB_NOOVERWRITE); + cursor.put(bb(4), bb(5)); + cursor.put(bb(6), bb(7)); + cursor.put(bb(8), bb(9)); + } txn.commit(); } } diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index ae0b6aee..df13829c 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -47,10 +47,11 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.Cursor.ClosedException; -import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.ReadOnlyRequiredException; -/** Test {@link Cursor}. */ +/** + * Test {@link Cursor}. + */ public final class CursorTest { private Env env; @@ -60,13 +61,13 @@ public final class CursorTest { void beforeEach() { tempDir = new TempDir(); Path file = tempDir.createTempFile(); - env = - create(PROXY_OPTIMAL) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxReaders(1) - .setMaxDbs(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file); + env = create(PROXY_OPTIMAL) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxReaders(1) + .setMaxDbs(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); } @AfterEach @@ -78,104 +79,104 @@ void afterEach() { @Test void closedCursorRejectsSubsequentGets() { assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - try (Txn txn = env.txnWrite()) { - final Cursor c = db.openCursor(txn); - c.close(); - c.seek(MDB_FIRST); - } - }) + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + try (Txn txn = env.txnWrite()) { + final Cursor c = db.openCursor(txn); + c.close(); + c.seek(MDB_FIRST); + } + }) .isInstanceOf(ClosedException.class); } @Test void closedEnvRejectsSeekFirstCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsSeekLastCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_LAST)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_LAST)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsSeekNextCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsCloseCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::close); - }) + () -> { + doEnvClosedTest(null, Cursor::close); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsFirstCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::first); - }) + () -> { + doEnvClosedTest(null, Cursor::first); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsLastCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::last); - }) + () -> { + doEnvClosedTest(null, Cursor::last); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsPrevCall() { assertThatThrownBy( - () -> { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt()).isEqualTo(1); - assertThat(c.val().getInt()).isEqualTo(10); - c.next(); - }, - Cursor::prev); - }) + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + c.next(); + }, + Cursor::prev); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsDeleteCall() { assertThatThrownBy( - () -> { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt()).isEqualTo(1); - assertThat(c.val().getInt()).isEqualTo(10); - }, - Cursor::delete); - }) + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + }, + Cursor::delete); + }) .isInstanceOf(Env.EnvInUseException.class); } @@ -188,7 +189,7 @@ void countWithDupsort() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); assertThat(c.count()).isEqualTo(1L); c.put(bb(1), bb(4), MDB_APPENDDUP); @@ -205,7 +206,7 @@ void countWithoutDupsort() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThat(c.put(bb(1), bb(2), MDB_NOOVERWRITE)).isTrue(); assertThat(c.put(bb(1), bb(4))).isTrue(); assertThat(c.put(bb(1), bb(6), PutFlagSet.EMPTY)).isTrue(); @@ -224,28 +225,38 @@ void countWithoutDupsort() { } } - @Disabled // close() method changed to only do the mdb_cursor_close call if in the right txn state + @Disabled // Disabled because we have no way to close the env in afterEach() because we + // can't close the cursor. This is trying to test something that you shouldn't do and that + // leaves @Test void cursorCannotCloseIfTransactionCommitted() { - assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE, MDB_DUPSORT) - .open(); - try (Txn txn = env.txnWrite()) { - try (Cursor c = db.openCursor(txn); ) { - c.put(bb(1), bb(2), MDB_APPENDDUP); - assertThat(c.count()).isEqualTo(1L); - c.put(bb(1), bb(4), MDB_APPENDDUP); - assertThat(c.count()).isEqualTo(2L); - txn.commit(); - } - } - }) - .isInstanceOf(NotReadyException.class); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + + try (Txn txn = env.txnWrite()) { + Cursor c = db.openCursor(txn); + c.put(bb(1), bb(2), MDB_APPENDDUP); + assertThat(c.count()).isEqualTo(1L); + c.put(bb(1), bb(4), MDB_APPENDDUP); + assertThat(c.count()).isEqualTo(2L); + + assertThat(txn.isReady()) + .isTrue(); + + txn.commit(); + + assertThat(txn.isReady()) + .isFalse(); + + // Cursor is not in a ready state to be closed because we have committed + // This makes it impossible to close the cursor and thus the env + assertThatThrownBy(c::close) + .isInstanceOf(Txn.NotReadyException.class); + } } @Test @@ -253,7 +264,7 @@ void cursorFirstLastNextPrev() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); c.put(bb(5), bb(6)); @@ -287,7 +298,7 @@ void delete() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -311,7 +322,7 @@ void delete2() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -335,7 +346,7 @@ void delete3() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -359,7 +370,7 @@ void getKeyVal() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); @@ -395,7 +406,7 @@ void putMultiple() { final int key = 100; final ByteBuffer k = bb(key); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.putMultiple(k, values, elemCount, MDB_MULTIPLE); assertThat(c.count()).isEqualTo((long) elemCount); } @@ -410,11 +421,11 @@ void putMultipleWithoutMdbMultipleFlag() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1); + }) .isInstanceOf(IllegalArgumentException.class); } } @@ -428,11 +439,11 @@ void putMultipleWithoutMdbMultipleFlag2() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1, PutFlags.EMPTY); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1, PutFlags.EMPTY); + }) .isInstanceOf(IllegalArgumentException.class); } } @@ -446,11 +457,11 @@ void putMultipleWithoutMdbMultipleFlag3() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1, (PutFlagSet) null); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1, (PutFlagSet) null); + }) .isInstanceOf(NullPointerException.class); } } @@ -477,21 +488,21 @@ void renewTxRo() { @Test void renewTxRw() { assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - try (Txn txn = env.txnWrite()) { - assertThat(txn.isReadOnly()).isFalse(); - - try (Cursor c = db.openCursor(txn)) { - c.renew(txn); - } - } - }) + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + try (Txn txn = env.txnWrite()) { + assertThat(txn.isReadOnly()).isFalse(); + + try (Cursor c = db.openCursor(txn)) { + c.renew(txn); + } + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @@ -540,7 +551,7 @@ void returnValueForNoDupData() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA)).isTrue(); assertThat(c.put(bb(5), bb(7), MDB_NODUPDATA)).isTrue(); @@ -553,7 +564,7 @@ void returnValueForNoOverwrite() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NOOVERWRITE)).isTrue(); // fails, but gets exist val @@ -596,7 +607,11 @@ private void doEnvClosedTest( final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { final Dbi db = - env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 50639786..bcfbcc06 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -752,6 +752,7 @@ void closeWithOpenReadTxn() { .setMaxDbs(1) .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() .open(file); // Open but don't close @@ -759,6 +760,9 @@ void closeWithOpenReadTxn() { Assertions.assertThatThrownBy(env::close) .isInstanceOf(Env.EnvInUseException.class); + + readTxn.close(); + env.close(); } @Test @@ -769,6 +773,7 @@ void closeWithOpenWriteTxn() { .setMaxDbs(1) .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() .open(file); // Open but don't close @@ -776,5 +781,8 @@ void closeWithOpenWriteTxn() { Assertions.assertThatThrownBy(env::close) .isInstanceOf(Env.EnvInUseException.class); + + writeTxn.close(); + env.close(); } } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index 82e76378..5d9d1a8a 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -304,8 +304,8 @@ void testBehaviour() throws InterruptedException { .hasNullValue(); assertThat(refCounter.isClosed()) .isEqualTo(true); - assertThatThrownBy(refCounter::getCount) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThat(refCounter.getCount()) + .isZero(); assertThatThrownBy(refCounter::acquire) .isInstanceOf(Env.AlreadyClosedException.class); assertThat(onCloseCallCount) @@ -406,8 +406,8 @@ void getCountRacingWithCloseDoesNotReturnZeroAfterClose() { refCounter.close(onCloseCallCount::incrementAndGet); - assertThatThrownBy(refCounter::getCount) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThat(refCounter.getCount()) + .isZero(); } @Test diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index 9da39f90..aa41d0bc 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -231,38 +231,37 @@ void tutorial3() { try (Txn txn = env.txnWrite()) { // A cursor always belongs to a particular Dbi. - final Cursor c = db.openCursor(txn); + try (Cursor c = db.openCursor(txn)) { - // We can put via a Cursor. Note we're adding keys in a strange order, - // as we want to show you that LMDB returns them in sorted order. - key.put("zzz".getBytes(UTF_8)).flip(); - val.put("lmdb".getBytes(UTF_8)).flip(); - c.put(key, val); - key.clear(); - key.put("aaa".getBytes(UTF_8)).flip(); - c.put(key, val); - key.clear(); - key.put("ccc".getBytes(UTF_8)).flip(); - c.put(key, val); - - // We can read from the Cursor by key. - c.get(key, MDB_SET); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); + // We can put via a Cursor. Note we're adding keys in a strange order, + // as we want to show you that LMDB returns them in sorted order. + key.put("zzz".getBytes(UTF_8)).flip(); + val.put("lmdb".getBytes(UTF_8)).flip(); + c.put(key, val); + key.clear(); + key.put("aaa".getBytes(UTF_8)).flip(); + c.put(key, val); + key.clear(); + key.put("ccc".getBytes(UTF_8)).flip(); + c.put(key, val); - // Let's see that LMDB provides the keys in appropriate order.... - c.seek(MDB_FIRST); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("aaa"); + // We can read from the Cursor by key. + c.get(key, MDB_SET); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); - c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("zzz"); + // Let's see that LMDB provides the keys in appropriate order.... + c.seek(MDB_FIRST); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("aaa"); - c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); + c.seek(MDB_LAST); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("zzz"); - // Cursors can also delete the current key. - c.delete(); + c.seek(MDB_PREV); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); - c.close(); + // Cursors can also delete the current key. + c.delete(); + } txn.commit(); } @@ -370,34 +369,33 @@ void tutorial5() { final ByteBuffer val = ByteBuffer.allocateDirect(env.getMaxKeySize()); try (Txn txn = env.txnWrite()) { - final Cursor c = db.openCursor(txn); + try (Cursor c = db.openCursor(txn)) { - // Store one key, but many values, and in non-natural order. - key.put("key".getBytes(UTF_8)).flip(); - val.put("xxx".getBytes(UTF_8)).flip(); - c.put(key, val); - val.clear(); - val.put("kkk".getBytes(UTF_8)).flip(); - c.put(key, val); - val.clear(); - val.put("lll".getBytes(UTF_8)).flip(); - c.put(key, val); - - // Cursor can tell us how many values the current key has. - final long count = c.count(); - assertThat(count).isEqualTo(3L); + // Store one key, but many values, and in non-natural order. + key.put("key".getBytes(UTF_8)).flip(); + val.put("xxx".getBytes(UTF_8)).flip(); + c.put(key, val); + val.clear(); + val.put("kkk".getBytes(UTF_8)).flip(); + c.put(key, val); + val.clear(); + val.put("lll".getBytes(UTF_8)).flip(); + c.put(key, val); - // Let's position the Cursor. Note sorting still works. - c.seek(MDB_FIRST); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("kkk"); + // Cursor can tell us how many values the current key has. + final long count = c.count(); + assertThat(count).isEqualTo(3L); - c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("xxx"); + // Let's position the Cursor. Note sorting still works. + c.seek(MDB_FIRST); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("kkk"); - c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("lll"); + c.seek(MDB_LAST); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("xxx"); - c.close(); + c.seek(MDB_PREV); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("lll"); + } txn.commit(); } diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index e6ca032e..6241e9fd 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -51,7 +51,9 @@ import org.lmdbjava.Txn.ReadWriteRequiredException; import org.lmdbjava.Txn.ResetException; -/** Test {@link Txn}. */ +/** + * Test {@link Txn}. + */ public final class TxnTest { private Path file; @@ -63,8 +65,7 @@ public final class TxnTest { void beforeEach() { tempDir = new TempDir(); file = tempDir.createTempFile(); - env = - create() + env = create() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) @@ -81,17 +82,17 @@ void afterEach() { @Test void largeKeysRejected() { assertThatThrownBy( - () -> { - final Dbi dbi = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); - key.limit(key.capacity()); - dbi.put(key, bb(2)); - }) + () -> { + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); + key.limit(key.capacity()); + dbi.put(key, bb(2)); + }) .isInstanceOf(BadValueSizeException.class); } @@ -134,7 +135,7 @@ void rangeSearch() { void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); } @@ -144,52 +145,52 @@ void readOnlyTxnAllowedInReadOnlyEnv() { @Test void readWriteTxnDeniedInReadOnlyEnv() { assertThatThrownBy( - () -> { - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - env.close(); - try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { - roEnv.txnWrite(); // error - } - }) + () -> { + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + env.close(); + try (Env roEnv = + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + roEnv.txnWrite(); // error + } + }) .isInstanceOf(EnvIsReadOnly.class); } @Test void testCheckNotCommitted() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.commit(); - txn.checkReady(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.commit(); + txn.checkReady(); + } + }) .isInstanceOf(NotReadyException.class); } @Test void testCheckReadOnly() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnWrite()) { - txn.checkReadOnly(); - } - }) + () -> { + try (Txn txn = env.txnWrite()) { + txn.checkReadOnly(); + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void testCheckWritesAllowed() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.checkWritesAllowed(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.checkWritesAllowed(); + } + }) .isInstanceOf(ReadWriteRequiredException.class); } @@ -224,100 +225,78 @@ void txCanCommitThenCloseWithoutError() { @Test void txCannotAbortIfAlreadyCommitted() { - assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - assertThat(txn.getState()).isEqualTo(READY); - txn.commit(); - assertThat(txn.getState()).isEqualTo(DONE); - txn.abort(); - } - }) - .isInstanceOf(NotReadyException.class); + + try (Txn txn = env.txnRead()) { + assertThat(txn.getState()).isEqualTo(READY); + txn.commit(); + assertThat(txn.getState()).isEqualTo(DONE); + + assertThatThrownBy(txn::abort) + .isInstanceOf(NotReadyException.class); + } } @Test void txCannotCommitTwice() { - assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.commit(); - txn.commit(); // error - } - }) - .isInstanceOf(NotReadyException.class); + try (Txn txn = env.txnRead()) { + txn.commit(); + assertThatThrownBy(txn::commit) + .isInstanceOf(NotReadyException.class); + } } @Test void txConstructionDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - env.close(); - env.txnRead(); - }) + env.close(); + assertThatThrownBy(env::txnRead) .isInstanceOf(AlreadyClosedException.class); } @Test void txRenewDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - final Txn txnRead = env.txnRead(); - txnRead.close(); - env.close(); - txnRead.renew(); - }) + final Txn txnRead = env.txnRead(); + txnRead.close(); + env.close(); + assertThatThrownBy(txnRead::renew) .isInstanceOf(AlreadyClosedException.class); } @Test void txCloseDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.close(); - }) + final Txn txnRead = env.txnRead(); + env.close(); + assertThatThrownBy(txnRead::close) .isInstanceOf(AlreadyClosedException.class); } @Test void txCommitDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.commit(); - }) + final Txn txnRead = env.txnRead(); + env.close(); + assertThatThrownBy(txnRead::commit) .isInstanceOf(AlreadyClosedException.class); } @Test void txAbortDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.abort(); - }) + final Txn txnRead = env.txnRead(); + env.close(); + assertThatThrownBy(txnRead::abort) .isInstanceOf(AlreadyClosedException.class); } @Test void txResetDeniedIfEnvClosed() { - assertThatThrownBy( - () -> { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.reset(); - }) + final Txn txnRead = env.txnRead(); + env.close(); + assertThatThrownBy(txnRead::reset) .isInstanceOf(AlreadyClosedException.class); } @Test public void txParent() { try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { + Txn txChild = env.txn(txRoot)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } @@ -327,9 +306,9 @@ public void txParent() { public void txParent2() { try (Txn txRoot = env.txnWrite()) { assertThatThrownBy( - () -> { - env.txn(txRoot, (TxnFlagSet) null); - }) + () -> { + env.txn(txRoot, (TxnFlagSet) null); + }) .isInstanceOf(NullPointerException.class); } } @@ -337,7 +316,7 @@ public void txParent2() { @Test public void txParent3() { try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { + Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } @@ -346,35 +325,35 @@ public void txParent3() { @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { - env.close(); - assertThat(txChild.getParent()).isEqualTo(txRoot); - } - }) + () -> { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot)) { + env.close(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + }) .isInstanceOf(AlreadyClosedException.class); } @Test void txParentROChildRWIncompatible() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnRead()) { - env.txn(txRoot); // error - } - }) + () -> { + try (Txn txRoot = env.txnRead()) { + env.txn(txRoot); // error + } + }) .isInstanceOf(IncompatibleParent.class); } @Test void txParentRWChildROIncompatible() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnWrite()) { - env.txn(txRoot, MDB_RDONLY_TXN); // error - } - }) + () -> { + try (Txn txRoot = env.txnWrite()) { + env.txn(txRoot, MDB_RDONLY_TXN); // error + } + }) .isInstanceOf(IncompatibleParent.class); } @@ -414,54 +393,54 @@ void txReadWrite() { @Test void txRenewDeniedWithoutPriorReset() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.renew(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.renew(); + } + }) .isInstanceOf(NotResetException.class); } @Test void txResetDeniedForAlreadyResetTransaction() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.reset(); - txn.renew(); - txn.reset(); - txn.reset(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.reset(); + txn.renew(); + txn.reset(); + txn.reset(); + } + }) .isInstanceOf(ResetException.class); } @Test void txResetDeniedForReadWriteTransaction() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnWrite()) { - txn.reset(); - } - }) + () -> { + try (Txn txn = env.txnWrite()) { + txn.reset(); + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void zeroByteKeysRejected() { assertThatThrownBy( - () -> { - final Dbi dbi = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - final ByteBuffer key = allocateDirect(4); - key.putInt(1); - assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped - dbi.put(key, bb(2)); - }) + () -> { + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + final ByteBuffer key = allocateDirect(4); + key.putInt(1); + assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped + dbi.put(key, bb(2)); + }) .isInstanceOf(BadValueSizeException.class); } } From 3c2f18150a94af0c3057b2a1a192187293d5b565 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:48:28 +0100 Subject: [PATCH 30/61] gh-279 Run mvn build --- src/main/java/org/lmdbjava/Cursor.java | 2 +- .../java/org/lmdbjava/CursorIterable.java | 2 +- src/main/java/org/lmdbjava/Env.java | 23 +++--- .../java/org/lmdbjava/NoOpRefCounter.java | 29 ++++++-- src/main/java/org/lmdbjava/RefCounter.java | 19 +++-- .../java/org/lmdbjava/SimpleRefCounter.java | 15 ++++ .../lmdbjava/SingleThreadedRefCounter.java | 18 +++++ .../java/org/lmdbjava/StripedRefCounter.java | 15 ++++ .../org/lmdbjava/SynchronisedRefCounter.java | 15 ++++ src/main/java/org/lmdbjava/Txn.java | 2 +- .../java/org/lmdbjava/CursorIterableTest.java | 3 +- src/test/java/org/lmdbjava/CursorTest.java | 3 +- src/test/java/org/lmdbjava/EnvTest.java | 3 +- .../org/lmdbjava/RefCounterBenchmark.java | 15 ++++ .../java/org/lmdbjava/RefCounterTest.java | 15 ++++ .../org/lmdbjava/StripedRefCounterTest.java | 70 ++++++++++++------- src/test/java/org/lmdbjava/TutorialTest.java | 3 +- src/test/java/org/lmdbjava/TxnTest.java | 3 +- 18 files changed, 200 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 2d7d79bc..44a7f061 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index 569baf53..c7bb57e4 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 8e32e586..55282791 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -191,11 +190,8 @@ public static Env open(final File path, final int size, final EnvFla */ @Override public void close() { - refCounter.close(this::closeEnv); - } - - public void closeEnv() { - LIB.mdb_env_close(ptr); + refCounter.close(() -> + LIB.mdb_env_close(ptr)); } /** @@ -307,6 +303,7 @@ public List getDbiNames() { * *

This method must not be called from concurrent threads. * + * @param charset the charset to use when converting byte arrays to strings * @return a list of DBI names (never null) */ public List getDbiNames(final Charset charset) { @@ -727,6 +724,9 @@ public String toString() { + '}'; } + /** + * Indicates that one or more transactions or cursors are in use on the {@link Env}. + */ public static final class EnvInUseException extends LmdbException { private static final long serialVersionUID = 1L; @@ -738,8 +738,12 @@ public EnvInUseException() { super("Environment has open transactions/cursors so cannot be closed."); } + /** + * Creates a new instance. + * @param count The number of open transactions/cursors. + */ public EnvInUseException(final long count) { - super("Environment has " + count + " open transaction(s)/cursor(s) so cannot be closed. " + + super("Environment has " + count + " open transactions/cursors so cannot be closed. " + "Close them then retry."); } } @@ -1059,6 +1063,7 @@ public Builder singleThreaded() { * using primitives rather than thread-safe objects. * By default, an Env is considered thread-safe. * + * @param singleThreaded Set to true if the Env will only ever be used by a single thread. * @return this builder instance. */ public Builder singleThreaded(final boolean singleThreaded) { @@ -1085,7 +1090,7 @@ public Builder setSafeClose() { * When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if transactions or * cursors are active. * - * @param safeClose true to enable transaction tracking and {@link Env#close(Duration)} + * @param safeClose true to enable cursor/transaction tracking. * @return the builder */ public Builder setSafeClose(final boolean safeClose) { diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index ad0e7f21..e6990a29 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -1,13 +1,35 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; import java.util.concurrent.atomic.AtomicBoolean; /** - * Preforms no reference counting at all, but will throw a Env.AlreadyClosedException - * if the Env is closed when {@link NoOpRefCounter#acquire()} is called. + * Preforms no reference counting at all, but will throw an Env.AlreadyClosedException + * if the {@link Env} is closed when {@link NoOpRefCounter#acquire()} is called. */ public class NoOpRefCounter implements RefCounter { + /** + * A {@link RefCounterReleaser} that does nothing. + */ + private static final RefCounterReleaser NO_OP_RELEASER = () -> { + // No-op + }; + private final AtomicBoolean isClosed = new AtomicBoolean(false); @Override @@ -15,8 +37,7 @@ public RefCounterReleaser acquire() { if (isClosed.get()) { throw new Env.AlreadyClosedException(); } - - return RefCounter.NO_OP_RELEASER; + return NO_OP_RELEASER; } @Override diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index 9966d8ab..d038450a 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; /** @@ -6,10 +21,6 @@ */ interface RefCounter { - RefCounterReleaser NO_OP_RELEASER = () -> { - // No-op - }; - /** * Call this before using the {@link RefCounter} controlled object. * diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index b429877d..c197c2f3 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 04b0ce3b..a04cf7fb 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -1,8 +1,26 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; import java.util.Objects; +/** + * A {@link RefCounter} intented for use only in single threaded environments. + */ public class SingleThreadedRefCounter implements RefCounter { private int refCount; diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 4c3d9397..89bfef81 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; import java.util.Objects; diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index 36a2e98f..9fe35c2b 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 7598ccf0..0ed7ee04 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 697378fb..3dca93f6 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.util.Arrays.asList; diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index df13829c..6ab3b525 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.lang.Long.BYTES; diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index bcfbcc06..d6540bfe 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.nio.ByteBuffer.allocateDirect; diff --git a/src/test/java/org/lmdbjava/RefCounterBenchmark.java b/src/test/java/org/lmdbjava/RefCounterBenchmark.java index 443e1894..2acd322f 100644 --- a/src/test/java/org/lmdbjava/RefCounterBenchmark.java +++ b/src/test/java/org/lmdbjava/RefCounterBenchmark.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 19f92f09..e8bd003b 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index 5d9d1a8a..da013d21 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; @@ -122,32 +137,37 @@ void multipleThreads_delayedRelease() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); final int threads = Runtime.getRuntime().availableProcessors() - 2; final int iterations = 100; - final ExecutorService executor = Executors.newFixedThreadPool(threads); - final ExecutorService executor2 = Executors.newFixedThreadPool(1); - final AtomicInteger[] callCounts = new AtomicInteger[threads]; - for (int i = 0; i < threads; i++) { - callCounts[i] = new AtomicInteger(); - } - - final Queue releasers = new ConcurrentLinkedQueue<>(); - final Queue> futures = new ConcurrentLinkedQueue<>(); + final AtomicInteger[] callCounts; + final Queue releasers; + final Queue> futures; + try (ExecutorService executor = Executors.newFixedThreadPool(threads)) { + try (ExecutorService executor2 = Executors.newFixedThreadPool(1)) { + callCounts = new AtomicInteger[threads]; + for (int i = 0; i < threads; i++) { + callCounts[i] = new AtomicInteger(); + } - IntStream.range(0, threads) - .boxed() - .map(i -> CompletableFuture.runAsync(() -> { - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); - releasers.add(releaser); - callCounts[i].getAndIncrement(); - futures.add(CompletableFuture.runAsync(() -> { - final long count = stripedRefCounter.getCount(); -// System.out.println(Thread.currentThread() + " - getting count: " + count); - assertThat(count) - .isNotEqualTo(0); - }, executor2)); - } - }, executor)) - .forEach(CompletableFuture::join); + releasers = new ConcurrentLinkedQueue<>(); + futures = new ConcurrentLinkedQueue<>(); + + IntStream.range(0, threads) + .boxed() + .map(i -> CompletableFuture.runAsync(() -> { + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); + releasers.add(releaser); + callCounts[i].getAndIncrement(); + futures.add(CompletableFuture.runAsync(() -> { + final long count = stripedRefCounter.getCount(); + // System.out.println(Thread.currentThread() + " - getting count: " + count); + assertThat(count) + .isNotEqualTo(0); + }, executor2)); + } + }, executor)) + .forEach(CompletableFuture::join); + } + } assertThat(stripedRefCounter.getCount()) .isEqualTo((long) threads * iterations); diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index aa41d0bc..f631895d 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.nio.charset.StandardCharsets.UTF_8; diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 6241e9fd..d02084b9 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.nio.ByteBuffer.allocateDirect; From 542904cad8d758876a45f0452e2ed57407d69f77 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:50:00 +0100 Subject: [PATCH 31/61] gh-279 Run mvn fmt --- src/main/java/org/lmdbjava/Cursor.java | 7 +- .../java/org/lmdbjava/CursorIterable.java | 2 +- src/main/java/org/lmdbjava/Env.java | 272 ++++----- .../java/org/lmdbjava/NoOpRefCounter.java | 15 +- src/main/java/org/lmdbjava/RefCounter.java | 25 +- .../java/org/lmdbjava/SimpleRefCounter.java | 13 +- .../lmdbjava/SingleThreadedRefCounter.java | 8 +- .../java/org/lmdbjava/StripedRefCounter.java | 155 +++-- .../org/lmdbjava/SynchronisedRefCounter.java | 1 - src/main/java/org/lmdbjava/Txn.java | 13 +- src/test/java/org/lmdbjava/CursorTest.java | 208 ++++--- src/test/java/org/lmdbjava/EnvTest.java | 533 +++++++++--------- .../org/lmdbjava/RefCounterBenchmark.java | 2 - .../java/org/lmdbjava/RefCounterTest.java | 178 +++--- .../org/lmdbjava/StripedRefCounterTest.java | 370 ++++++------ src/test/java/org/lmdbjava/TxnTest.java | 215 ++++--- 16 files changed, 975 insertions(+), 1042 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 44a7f061..09b36f7b 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -48,9 +48,7 @@ public final class Cursor implements AutoCloseable { private final Env env; private final RefCounter.RefCounterReleaser refCounterReleaser; - Cursor(final Pointer ptr, - final Txn txn, - final Env env) { + Cursor(final Pointer ptr, final Txn txn, final Env env) { requireNonNull(ptr); requireNonNull(txn); requireNonNull(env); @@ -83,7 +81,8 @@ public void close() { env.checkNotClosed(); if (!txn.isReadOnly()) { // TODO Rather than throwing if the txn is not in the right state to close - // we could check the txn state and only call mdb_cursor_close if the state is appropriate, + // we could check the txn state and only call mdb_cursor_close if the state is + // appropriate, // i.e. (txn.isReadOnly() || txn.isReady()) // This would make using try-with-resources less likely to fail diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index c7bb57e4..ac53b16b 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -38,7 +38,7 @@ * *

An instance will create and close its own cursor. * - *

Not thread safe.

+ *

Not thread safe. * * @param buffer type */ diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 55282791..9b03554a 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -51,9 +51,7 @@ */ public final class Env implements AutoCloseable { - /** - * Java system property name that can be set to disable optional checks. - */ + /** Java system property name that can be set to disable optional checks. */ public static final String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks"; /** @@ -77,10 +75,10 @@ public final class Env implements AutoCloseable { private final boolean readOnly; private final Path path; private final EnvFlagSet envFlagSet; - /** - * True if this Env has been created on the basis of only ever being used by a single thread. - */ + + /** True if this Env has been created on the basis of only ever being used by a single thread. */ private final boolean isSingleThreaded; + private final boolean safeClose; private Env( @@ -131,7 +129,7 @@ public static Builder create() { /** * Create an {@link Env} using the passed {@link BufferProxy}. * - * @param buffer type + * @param buffer type * @param proxy the proxy to use (required) * @return the environment (never null) */ @@ -140,13 +138,13 @@ public static Builder create(final BufferProxy proxy) { } /** - * @param path file system destination - * @param size size in megabytes + * @param path file system destination + * @param size size in megabytes * @param flags the flags for this new environment * @return env the environment (never null) * @deprecated Instead use {@link Env#create()} or {@link Env#create(BufferProxy)} - *

Opens an environment with a single default database in 0664 mode using the {@link - * ByteBufferProxy#PROXY_OPTIMAL}. + *

Opens an environment with a single default database in 0664 mode using the {@link + * ByteBufferProxy#PROXY_OPTIMAL}. */ @Deprecated public static Env open(final File path, final int size, final EnvFlags... flags) { @@ -164,8 +162,8 @@ public static Env open(final File path, final int size, final EnvFla *

Before and during this call, the caller MUST ensure that: * *

    - *
  • every {@link Txn}, {@link Cursor} obtained from this environment has - * already been closed; and + *
  • every {@link Txn}, {@link Cursor} obtained from this environment has already been closed; + * and *
  • no other thread is executing any operation on this environment or on a handle * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as * {@code Dbi.get}. @@ -190,8 +188,7 @@ public static Env open(final File path, final int size, final EnvFla */ @Override public void close() { - refCounter.close(() -> - LIB.mdb_env_close(ptr)); + refCounter.close(() -> LIB.mdb_env_close(ptr)); } /** @@ -209,7 +206,7 @@ public void close() { * transactions, because it employs a read-only transaction. See long-lived transactions under * "Caveats" in the LMDB native documentation. * - * @param path writable destination path as described above + * @param path writable destination path as described above * @param flags special options for this copy * @deprecated Use {@link Env#copy(Path, CopyFlagSet)} */ @@ -255,7 +252,7 @@ public void copy(final Path path) { * transactions, because it employs a read-only transaction. See long-lived transactions under * "Caveats" in the LMDB native documentation. * - * @param path writable destination path as described above + * @param path writable destination path as described above * @param flags special options for this copy */ public void copy(final Path path, final CopyFlagSet flags) { @@ -328,7 +325,7 @@ public void setMapSize(final long mapSize) { /** * Set the size of the data memory map. * - * @param mapSize new map size in the units of byteUnit. + * @param mapSize new map size in the units of byteUnit. * @param byteUnit The unit that mapSize is in. */ public void setMapSize(final long mapSize, final ByteUnit byteUnit) { @@ -391,8 +388,7 @@ public boolean isReadOnly() { } /** - * Indicates if this environment is intended for use by a single thread for its - * entire life. + * Indicates if this environment is intended for use by a single thread for its entire life. * * @return True if single-threaded */ @@ -417,12 +413,12 @@ public DbiBuilder createDbi() { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and default - * {@link Comparator} that is not invoked from native code. + *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and default + * {@link Comparator} that is not invoked from native code. */ @Deprecated() public Dbi openDbi(final String name, final DbiFlags... flags) { @@ -430,19 +426,19 @@ public Dbi openDbi(final String name, final DbiFlags... flags) { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator for cursor start/stop key comparisons. If null, LMDB's - * comparator will be used. - * @param flags to open the database with + * comparator will be used. + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated - * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys. - *

    It is very important that the passed comparator behaves in the same way as the - * comparator LMDB uses for its insertion order (for the type of data that will be stored in - * the database), or you fully understand the implications of them behaving differently. - * LMDB's comparator is unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is - * used. + *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys. + *

    It is very important that the passed comparator behaves in the same way as the + * comparator LMDB uses for its insertion order (for the type of data that will be stored in + * the database), or you fully understand the implications of them behaving differently. + * LMDB's comparator is unsigned lexicographical, unless {@link DbiFlags#MDB_INTEGERKEY} is + * used. */ @Deprecated() public Dbi openDbi( @@ -451,18 +447,18 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator for cursor start/stop key comparisons and optionally for - * LMDB to call back to. If null, LMDB's comparator will be used. - * @param nativeCb whether LMDB native code calls back to the Java comparator - * @param flags to open the database with + * LMDB to call back to. If null, LMDB's comparator will be used. + * @param nativeCb whether LMDB native code calls back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated - * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing - * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be - * called by LMDB to determine insertion/iteration order. Calling back to a java comparator - * may significantly impact performance. + *

    Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing + * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be + * called by LMDB to determine insertion/iteration order. Calling back to a java comparator + * may significantly impact performance. */ @Deprecated() public Dbi openDbi( @@ -474,12 +470,12 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is - * not invoked from native code. + *

    Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is + * not invoked from native code. */ @Deprecated() public Dbi openDbi(final byte[] name, final DbiFlags... flags) { @@ -487,13 +483,13 @@ public Dbi openDbi(final byte[] name, final DbiFlags... flags) { } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom iterator comparator (or null to use LMDB default) - * @param flags to open the database with + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that - * is not invoked from native code. + *

    Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that + * is not invoked from native code. */ @Deprecated() public Dbi openDbi( @@ -502,16 +498,16 @@ public Dbi openDbi( } /** - * @param name name of the database (or null if no name is required) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native code calls back to the Java comparator - * @param flags to open the database with + * @param nativeCb whether native code calls back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that - * may be invoked from native code if specified. - *

    This method will automatically commit the private transaction before returning. This - * ensures the Dbi is available in the Env. + *

    Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that + * may be invoked from native code if specified. + *

    This method will automatically commit the private transaction before returning. This + * ensures the Dbi is available in the Env. */ @Deprecated() public Dbi openDbi( @@ -528,27 +524,27 @@ public Dbi openDbi( } /** - * @param txn transaction to use (required; not closed) - * @param name name of the database (or null if no name is required) + * @param txn transaction to use (required; not closed) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native LMDB code should call back to the Java comparator - * @param flags to open the database with + * @param nativeCb whether native LMDB code should call back to the Java comparator + * @param flags to open the database with * @return a database that is ready to use * @deprecated Instead use {@link Env#createDbi()} - *

    Open the {@link Dbi} using the passed {@link Txn}. - *

    The caller must commit the transaction after this method returns in order to retain the - * Dbi in the Env. - *

    A {@link Comparator} may be provided when calling this method. Such comparator is - * primarily used by {@link CursorIterable} instances. A secondary (but uncommon) use of the - * comparator is to act as a callback from the native library if nativeCb is - * true. This is usually avoided due to the overhead of native code calling back - * into Java. It is instead highly recommended to set the correct {@link DbiFlags} to allow - * the native library to correctly order the intended keys. - *

    A default comparator will be provided if null is passed as the comparator. - * If a custom comparator is provided, it must strictly match the lexicographical order of - * keys in the native LMDB database. - *

    This method (and its overloaded convenience variants) must not be called from concurrent - * threads. + *

    Open the {@link Dbi} using the passed {@link Txn}. + *

    The caller must commit the transaction after this method returns in order to retain the + * Dbi in the Env. + *

    A {@link Comparator} may be provided when calling this method. Such comparator is + * primarily used by {@link CursorIterable} instances. A secondary (but uncommon) use of the + * comparator is to act as a callback from the native library if nativeCb is + * true. This is usually avoided due to the overhead of native code calling back + * into Java. It is instead highly recommended to set the correct {@link DbiFlags} to allow + * the native library to correctly order the intended keys. + *

    A default comparator will be provided if null is passed as the comparator. + * If a custom comparator is provided, it must strictly match the lexicographical order of + * keys in the native LMDB database. + *

    This method (and its overloaded convenience variants) must not be called from concurrent + * threads. */ @Deprecated() public Dbi openDbi( @@ -582,7 +578,7 @@ public Stat stat() { * Flushes the data buffers to disk. * * @param force force a synchronous flush (otherwise if the environment has the MDB_NOSYNC flag - * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) + * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) */ public void sync(final boolean force) { checkNotClosed(); @@ -592,10 +588,10 @@ public void sync(final boolean force) { /** * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (eg for a reusable, read-only transaction) + * @param flags applicable flags (eg for a reusable, read-only transaction) * @return a transaction (never null) * @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)} - *

    Obtain a transaction with the requested parent and flags. + *

    Obtain a transaction with the requested parent and flags. */ @Deprecated public Txn txn(final Txn parent, final TxnFlags... flags) { @@ -616,9 +612,9 @@ public Txn txn(final Txn parent) { * Obtain a transaction with the requested parent and flags. * * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags - * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} - * for re-use. + * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags + * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} + * for re-use. * @return a transaction (never null) */ public Txn txn(final Txn parent, final TxnFlagSet flags) { @@ -686,20 +682,18 @@ public int readerCheck() { } /** - * Acquire a permit to use this {@link Env}. - * Holding the permit will prevent the {@link Env} from being closed before it is released. + * Acquire a permit to use this {@link Env}. Holding the permit will prevent the {@link Env} from + * being closed before it is released. * - * @return A {@link org.lmdbjava.RefCounter.RefCounterReleaser} for releasing the permit once the use - * of this {@link Env} is complete. + * @return A {@link org.lmdbjava.RefCounter.RefCounterReleaser} for releasing the permit once the + * use of this {@link Env} is complete. * @throws AlreadyClosedException if this Env is already closed. */ RefCounter.RefCounterReleaser acquire() { return refCounter.acquire(); } - /** - * For testing use. - */ + /** For testing use. */ EnvFlagSet getEnvFlagSet() { return envFlagSet; } @@ -724,55 +718,47 @@ public String toString() { + '}'; } - /** - * Indicates that one or more transactions or cursors are in use on the {@link Env}. - */ + /** Indicates that one or more transactions or cursors are in use on the {@link Env}. */ public static final class EnvInUseException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public EnvInUseException() { super("Environment has open transactions/cursors so cannot be closed."); } /** * Creates a new instance. + * * @param count The number of open transactions/cursors. */ public EnvInUseException(final long count) { - super("Environment has " + count + " open transactions/cursors so cannot be closed. " + - "Close them then retry."); + super( + "Environment has " + + count + + " open transactions/cursors so cannot be closed. " + + "Close them then retry."); } } - /** - * Object has already been closed and the operation is therefore prohibited. - */ + /** Object has already been closed and the operation is therefore prohibited. */ public static final class AlreadyClosedException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public AlreadyClosedException() { super("Environment has already been closed"); } } - /** - * Object has already been opened and the operation is therefore prohibited. - */ + /** Object has already been opened and the operation is therefore prohibited. */ public static final class AlreadyOpenException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public AlreadyOpenException() { super("Environment has already been opened"); } @@ -814,12 +800,12 @@ private void checkEnvNotOpened() { /** * Opens the environment. * - * @param path file system destination - * @param mode Unix permissions to set on created files and semaphores + * @param path file system destination + * @param mode Unix permissions to set on created files and semaphores * @param flags the flags for this new environment * @return an environment ready for use * @deprecated Instead use {@link Builder#open(Path)}, {@link Builder#setFilePermissions(int)} - * and {@link Builder#setEnvFlags(EnvFlags...)}. + * and {@link Builder#setEnvFlags(EnvFlags...)}. */ @Deprecated public Env open(final File path, final int mode, final EnvFlags... flags) { @@ -843,11 +829,11 @@ public Env open(final File path) { /** * Opens the environment with 0664 mode. * - * @param path file system destination + * @param path file system destination * @param flags the flags for this new environment * @return an environment ready for use * @deprecated Instead use {@link Builder#open(Path)} and {@link - * Builder#setEnvFlags(EnvFlags...)}. + * Builder#setEnvFlags(EnvFlags...)}. */ @Deprecated public Env open(final File path, final EnvFlags... flags) { @@ -903,7 +889,7 @@ public Builder setMapSize(final long mapSize) { /** * Sets the map size in the supplied unit. * - * @param mapSize new map size in the units of byteUnit. + * @param mapSize new map size in the units of byteUnit. * @param byteUnit The unit that mapSize is in. * @return the builder */ @@ -956,7 +942,7 @@ public Builder setFilePermissions(final int mode) { * Sets all the flags used to open this {@link Env}. * * @param envFlags The flags to use. Clears any existing flags. A null value results in no flags - * being set. + * being set. * @return this builder instance. */ public Builder setEnvFlags(final Collection envFlags) { @@ -972,7 +958,7 @@ public Builder setEnvFlags(final Collection envFlags) { * Sets all the flags used to open this {@link Env}. * * @param envFlags The flags to use. Clears any existing flags. A null value results in no flags - * being set. + * being set. * @return this builder instance. */ public Builder setEnvFlags(final EnvFlags... envFlags) { @@ -988,7 +974,7 @@ public Builder setEnvFlags(final EnvFlags... envFlags) { * Sets all the flags used to open this {@link Env}. * * @param envFlagSet The flags to use. Clears any existing flags. A null value results in no - * flags being set. + * flags being set. * @return this builder instance. */ public Builder setEnvFlags(final EnvFlagSet envFlagSet) { @@ -1030,7 +1016,7 @@ public Builder addEnvFlags(final EnvFlagSet envFlagSet) { * Adds a {@link Collection} of {@link EnvFlags} to any existing flags. * * @param envFlags The {@link Collection} of flags to add to any existing flags. A null value is - * a no-op. + * a no-op. * @return this builder instance. */ public Builder addEnvFlags(final Collection envFlags) { @@ -1042,11 +1028,10 @@ public Builder addEnvFlags(final Collection envFlags) { } /** - * If set, the caller is asserting that the Env will only be used by a single thread - * throughout its entire life. - * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. - * using primitives rather than thread-safe objects. - * By default, an Env is considered thread-safe. + * If set, the caller is asserting that the Env will only be used by a single thread throughout + * its entire life. This allows the {@link Env} to make minor optimisations that are not + * thread-safe, e.g. using primitives rather than thread-safe objects. By default, an Env is + * considered thread-safe. * * @return this builder instance. */ @@ -1058,10 +1043,9 @@ public Builder singleThreaded() { /** * If set to true, the caller is asserting that the Env will only be used by a single thread - * throughout its entire life. - * This allows the {@link Env} to make minor optimisations that are not thread-safe, e.g. - * using primitives rather than thread-safe objects. - * By default, an Env is considered thread-safe. + * throughout its entire life. This allows the {@link Env} to make minor optimisations that are + * not thread-safe, e.g. using primitives rather than thread-safe objects. By default, an Env is + * considered thread-safe. * * @param singleThreaded Set to true if the Env will only ever be used by a single thread. * @return this builder instance. @@ -1072,9 +1056,7 @@ public Builder singleThreaded(final boolean singleThreaded) { return this; } - /** - * See {@link Env.Builder#setSafeClose(boolean)} - */ + /** See {@link Env.Builder#setSafeClose(boolean)} */ public Builder setSafeClose() { checkEnvNotOpened(); return setSafeClose(true); @@ -1083,12 +1065,12 @@ public Builder setSafeClose() { /** * Enables the opt-in "safe close" for the resulting {@link Env}. * - *

    When enabled, the environment tracks its live transactions and cursors so that closure of the - * {@link Env} is prevented if transactions or cursors are active. This adds a small amount of - * bookkeeping on transaction start/close; it is disabled by default so - * applications that already manage their own threading (the common low-latency case) pay nothing. - * When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if transactions or - * cursors are active. + *

    When enabled, the environment tracks its live transactions and cursors so that closure of + * the {@link Env} is prevented if transactions or cursors are active. This adds a small amount + * of bookkeeping on transaction start/close; it is disabled by default so + * applications that already manage their own threading (the common low-latency case) pay + * nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if + * transactions or cursors are active. * * @param safeClose true to enable cursor/transaction tracking. * @return the builder @@ -1100,9 +1082,7 @@ public Builder setSafeClose(final boolean safeClose) { } } - /** - * File is not a valid LMDB file. - */ + /** File is not a valid LMDB file. */ public static final class FileInvalidException extends LmdbNativeException { static final int MDB_INVALID = -30_793; @@ -1113,9 +1093,7 @@ public static final class FileInvalidException extends LmdbNativeException { } } - /** - * The specified copy destination is invalid. - */ + /** The specified copy destination is invalid. */ public static final class InvalidCopyDestination extends LmdbException { private static final long serialVersionUID = 1L; @@ -1130,9 +1108,7 @@ public InvalidCopyDestination(final String message) { } } - /** - * Environment mapsize reached. - */ + /** Environment mapsize reached. */ public static final class MapFullException extends LmdbNativeException { static final int MDB_MAP_FULL = -30_792; @@ -1143,9 +1119,7 @@ public static final class MapFullException extends LmdbNativeException { } } - /** - * Environment maxreaders reached. - */ + /** Environment maxreaders reached. */ public static final class ReadersFullException extends LmdbNativeException { static final int MDB_READERS_FULL = -30_790; @@ -1156,9 +1130,7 @@ public static final class ReadersFullException extends LmdbNativeException { } } - /** - * Environment version mismatch. - */ + /** Environment version mismatch. */ public static final class VersionMismatchException extends LmdbNativeException { static final int MDB_VERSION_MISMATCH = -30_794; diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index e6990a29..794f519f 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -18,17 +18,16 @@ import java.util.concurrent.atomic.AtomicBoolean; /** - * Preforms no reference counting at all, but will throw an Env.AlreadyClosedException - * if the {@link Env} is closed when {@link NoOpRefCounter#acquire()} is called. + * Preforms no reference counting at all, but will throw an Env.AlreadyClosedException if the {@link + * Env} is closed when {@link NoOpRefCounter#acquire()} is called. */ public class NoOpRefCounter implements RefCounter { - /** - * A {@link RefCounterReleaser} that does nothing. - */ - private static final RefCounterReleaser NO_OP_RELEASER = () -> { - // No-op - }; + /** A {@link RefCounterReleaser} that does nothing. */ + private static final RefCounterReleaser NO_OP_RELEASER = + () -> { + // No-op + }; private final AtomicBoolean isClosed = new AtomicBoolean(false); diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index d038450a..d3fcb514 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -15,10 +15,7 @@ */ package org.lmdbjava; -/** - * Used to prevent the closure of a thing while other threads are actively - * using that thing. - */ +/** Used to prevent the closure of a thing while other threads are actively using that thing. */ interface RefCounter { /** @@ -29,7 +26,8 @@ interface RefCounter { RefCounterReleaser acquire(); /** - * Calls {@link RefCounter#acquire()}, runs runnable, then calls {@link RefCounterReleaser#release()} + * Calls {@link RefCounter#acquire()}, runs runnable, then calls {@link + * RefCounterReleaser#release()} */ default void use(final Runnable runnable) { if (runnable != null) { @@ -44,9 +42,9 @@ default void use(final Runnable runnable) { /** * If the reference count is zero, onClose will be called. This {@link RefCounter} will be marked - * as closed so all future calls to acquire will throw a {@link org.lmdbjava.Env.AlreadyClosedException}. - * If the count is non-zero, {@link org.lmdbjava.Env.EnvInUseException} will be thrown. - * If already closed, this is a no-op. + * as closed so all future calls to acquire will throw a {@link + * org.lmdbjava.Env.AlreadyClosedException}. If the count is non-zero, {@link + * org.lmdbjava.Env.EnvInUseException} will be thrown. If already closed, this is a no-op. * * @throws org.lmdbjava.Env.EnvInUseException If the {@link Env} has open transactions/cursors. */ @@ -57,9 +55,7 @@ default void use(final Runnable runnable) { */ boolean isClosed(); - /** - * If it is in a CLOSED state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} - */ + /** If it is in a CLOSED state, throw a {@link org.lmdbjava.Env.AlreadyClosedException} */ default void checkNotClosed() { if (isClosed()) { throw new Env.AlreadyClosedException(); @@ -67,17 +63,14 @@ default void checkNotClosed() { } /** - * @return The current count of items in use. - * It will return 0 if already closed. + * @return The current count of items in use. It will return 0 if already closed. */ long getCount(); @FunctionalInterface interface RefCounterReleaser { - /** - * Call this after using the {@link RefCounter} controlled object. - */ + /** Call this after using the {@link RefCounter} controlled object. */ void release(); } } diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index c197c2f3..8d6fd0b9 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -15,7 +15,6 @@ */ package org.lmdbjava; - import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -29,10 +28,8 @@ public boolean isClosed() { } public RefCounterReleaser acquire() { - final int newVal = counter.updateAndGet(currVal -> - currVal == CLOSED_VALUE - ? currVal - : currVal + 1); + final int newVal = + counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal + 1); if (newVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } @@ -53,10 +50,8 @@ public void close(final Runnable onClose) { } private void release() { - final int newVal = counter.updateAndGet(currVal -> - currVal == CLOSED_VALUE - ? currVal - : currVal - 1); + final int newVal = + counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal - 1); if (newVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index a04cf7fb..59e85aea 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -15,19 +15,15 @@ */ package org.lmdbjava; - import java.util.Objects; -/** - * A {@link RefCounter} intented for use only in single threaded environments. - */ +/** A {@link RefCounter} intented for use only in single threaded environments. */ public class SingleThreadedRefCounter implements RefCounter { private int refCount; private boolean isClosed = false; - public SingleThreadedRefCounter() { - } + public SingleThreadedRefCounter() {} @Override public RefCounterReleaser acquire() { diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 89bfef81..1551155f 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -21,30 +21,29 @@ class StripedRefCounter implements RefCounter { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); + /** - * Counter value used to indicate a count of zero while a sum of all counters is being - * performed. + * Counter value used to indicate a count of zero while a sum of all counters is being performed. */ private static final int MAGIC_ZERO_VALUE = Integer.MIN_VALUE; - /** - * Counter value used to indicate that this RefCounter has been closed. - */ + + /** Counter value used to indicate that this RefCounter has been closed. */ private static final int MAGIC_CLOSED_VALUE = Integer.MAX_VALUE; - /** - * The maximum possible count value on one stripe. - */ + + /** The maximum possible count value on one stripe. */ private static final int MAX_COUNTER_VALUE = Integer.MAX_VALUE - 1; + private static final int DEFAULT_STRIPES = 64; - /** - * Maximum number of stripes. - */ + + /** Maximum number of stripes. */ private static final int MAX_STRIPES = 256; private final Stripe[] counters; private final AtomicBoolean isClosed = new AtomicBoolean(false); + /** - * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). - * Used with bitwise AND for O(1) hashing with no modulo operation. + * Bit mask for fast stripe index calculation. Equal to (stripeCount - 1). Used with bitwise AND + * for O(1) hashing with no modulo operation. */ private final int stripeMask; @@ -97,9 +96,7 @@ public RefCounterReleaser acquire() { private static int getDefaultStripeCount() { return Math.min( MAX_STRIPES, - Math.max( - lowestPowerOfTwoGreaterThanOrEqualTo(PROCESSOR_COUNT * 2), - DEFAULT_STRIPES)); + Math.max(lowestPowerOfTwoGreaterThanOrEqualTo(PROCESSOR_COUNT * 2), DEFAULT_STRIPES)); } /** @@ -117,9 +114,7 @@ static int lowestPowerOfTwoGreaterThanOrEqualTo(final int value) { throw new IllegalArgumentException( "Value is too large to round up to a positive int power of two, got: " + value); } - return value == 1 - ? 1 - : Integer.highestOneBit(value - 1) << 1; + return value == 1 ? 1 : Integer.highestOneBit(value - 1) << 1; } private void release(final AtomicInteger counter) { @@ -146,16 +141,21 @@ public void close(final Runnable onClose) { return; } - // Once we have marked all counters as count-in-progress, any threads trying to mutate the counters + // Once we have marked all counters as count-in-progress, any threads trying to mutate the + // counters // will fail, then re-attempt under lock, so will have to wait for us to complete the count. - // Marking all the counters is a non-atomic operation, so another thread may increment a counter - // while we are in the middle of marking them, however, once all are marked, threads will be blocked - // from decrementing until we have called markCountersAsNoCountInProgress(), thus we will get a non-zero + // Marking all the counters is a non-atomic operation, so another thread may increment a + // counter + // while we are in the middle of marking them, however, once all are marked, threads will be + // blocked + // from decrementing until we have called markCountersAsNoCountInProgress(), thus we will get + // a non-zero // count and throw an EnvInUseException. markCountersAsCountInProgress(); // 0=>MAGIC_ZERO_VALUE else i=>i*-1 - // At this point, no other thread can mutate the counters, so we are safe to use a sum of all the counters. + // At this point, no other thread can mutate the counters, so we are safe to use a sum of all + // the counters. try { final long totalCount = sumCounters(); if (totalCount == 0) { @@ -181,16 +181,17 @@ public void close(final Runnable onClose) { } /** - * MUST be called after {@link StripedRefCounter#markCountersAsCountInProgress()} has been called and under - * lock. Once complete, {@link StripedRefCounter#markCountersAsNoCountInProgress()} must be called. + * MUST be called after {@link StripedRefCounter#markCountersAsCountInProgress()} has been called + * and under lock. Once complete, {@link StripedRefCounter#markCountersAsNoCountInProgress()} must + * be called. */ private long sumCounters() { long totalCount = 0; for (Stripe stripe : counters) { int count = stripe.counter.get(); - if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE + if (count == MAGIC_CLOSED_VALUE) { // Integer.MAX_VALUE throw new Env.AlreadyClosedException(); - } else if (count != MAGIC_ZERO_VALUE) { // Integer.MIN_VALUE + } else if (count != MAGIC_ZERO_VALUE) { // Integer.MIN_VALUE // count should be negative at this point if (count > 0) { throw new IllegalStateException("Count should be negative at this point, got: " + count); @@ -224,8 +225,8 @@ public long getCount() { /** * @return False if a count is in progress, else true - * @throws Env.AlreadyClosedException If this {@link RefCounter} has already been - * successfully closed. + * @throws Env.AlreadyClosedException If this {@link RefCounter} has already been successfully + * closed. */ private boolean addToCounter(final AtomicInteger counter, final Delta delta) { // Use a while loop with get() and compareAndSet(), rather than throwing exceptions inside @@ -253,30 +254,28 @@ private boolean addToCounter(final AtomicInteger counter, final Delta delta) { } } - /** - * Must be called while holding the lock on this object. - */ + /** Must be called while holding the lock on this object. */ private void markCountersAsNoCountInProgress() { for (Stripe stripe : counters) { // Multiply value by -1 so we can indicate to other threads that a count is in progress // while maintaining the count. Have to use a special replacement value for zero. - stripe.counter.updateAndGet(currVal -> { - if (currVal == MAGIC_ZERO_VALUE) { - return 0; - } else if (currVal == MAGIC_CLOSED_VALUE) { - // If this method is used correctly under lock, we should never see this value, but preserve the - // closed state just in case - return MAGIC_CLOSED_VALUE; - } else { - return Math.abs(currVal); - } - }); + stripe.counter.updateAndGet( + currVal -> { + if (currVal == MAGIC_ZERO_VALUE) { + return 0; + } else if (currVal == MAGIC_CLOSED_VALUE) { + // If this method is used correctly under lock, we should never see this value, but + // preserve the + // closed state just in case + return MAGIC_CLOSED_VALUE; + } else { + return Math.abs(currVal); + } + }); } } - /** - * Must be called while holding the lock on this object. - */ + /** Must be called while holding the lock on this object. */ private void markCountersAsCountInProgress() { // It is possible that another thread will call acquire() while we are mid-loop. // If that thread uses a counter that has not yet been marked as count-in-progress, they will @@ -285,54 +284,54 @@ private void markCountersAsCountInProgress() { // They will be blocked from calling release() until markCountersAsNoCountInProgress() has // been called by us. for (final Stripe stripe : counters) { - stripe.counter.updateAndGet(currVal -> { - if (currVal == 0) { - // Use a magic value to mark this zero-value counter as having a count in progress - return MAGIC_ZERO_VALUE; - } else if (currVal == MAGIC_CLOSED_VALUE) { - // If this method is used correctly under lock, we should never see this value, but preserve the - // closed state just in case - return MAGIC_CLOSED_VALUE; - } else { - // Make the value negative to indicate a count in progress - return Math.abs(currVal) * -1; - } - }); + stripe.counter.updateAndGet( + currVal -> { + if (currVal == 0) { + // Use a magic value to mark this zero-value counter as having a count in progress + return MAGIC_ZERO_VALUE; + } else if (currVal == MAGIC_CLOSED_VALUE) { + // If this method is used correctly under lock, we should never see this value, but + // preserve the + // closed state just in case + return MAGIC_CLOSED_VALUE; + } else { + // Make the value negative to indicate a count in progress + return Math.abs(currVal) * -1; + } + }); } } private void validateStripeCount(final int stripeCount) { if (stripeCount <= 0) { - throw new IllegalArgumentException( - "Stripe count must be positive, got: " + stripeCount); + throw new IllegalArgumentException("Stripe count must be positive, got: " + stripeCount); } if (stripeCount > MAX_STRIPES) { throw new IllegalArgumentException( - "Stripe count exceeds maximum. Got: " + stripeCount + - ", max: " + MAX_STRIPES); + "Stripe count exceeds maximum. Got: " + stripeCount + ", max: " + MAX_STRIPES); } if ((stripeCount & (stripeCount - 1)) != 0) { - throw new IllegalArgumentException( - "Stripe count must be power of 2, got: " + stripeCount); + throw new IllegalArgumentException("Stripe count must be power of 2, got: " + stripeCount); } } /** * Computes the stripe index for the current thread using Stafford variant 13 mixing. - *

    - * This method applies a high-quality 64-bit hash function (MurmurHash3 finalizer) - * to the thread ID before masking to the stripe count. This provides: + * + *

    This method applies a high-quality 64-bit hash function (MurmurHash3 finalizer) to the + * thread ID before masking to the stripe count. This provides: + * *

      - *
    • Excellent distribution for sequential thread IDs
    • - *
    • Same thread always maps to same stripe (deterministic)
    • - *
    • Strong avalanche properties (input bit changes affect all output bits)
    • - *
    • O(1) performance
    • + *
    • Excellent distribution for sequential thread IDs + *
    • Same thread always maps to same stripe (deterministic) + *
    • Strong avalanche properties (input bit changes affect all output bits) + *
    • O(1) performance *
    - *

    - * The Stafford13 mixing function is used internally by {@link java.util.SplittableRandom} - * for seed initialization. See: - * - * Better Bit Mixing + * + *

    The Stafford13 mixing function is used internally by {@link java.util.SplittableRandom} for + * seed initialization. See: Better Bit + * Mixing * * @return stripe index from 0 to stripeCount - 1 (inclusive) */ diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index 9fe35c2b..0abd2ab4 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -15,7 +15,6 @@ */ package org.lmdbjava; - import java.util.Objects; class SynchronisedRefCounter implements RefCounter { diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 0ed7ee04..70052b60 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -46,10 +46,7 @@ public final class Txn implements AutoCloseable { private State state; private RefCounter.RefCounterReleaser refCounterReleaser; - Txn(final Env env, - final Txn parent, - final BufferProxy proxy, - final TxnFlagSet flags) { + Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlagSet flags) { if (SHOULD_CHECK) { Objects.requireNonNull(flags); @@ -89,7 +86,8 @@ public void abort() { state = DONE; LIB.mdb_txn_abort(ptr); - // TODO It is not clear whether this method should call refCounterReleaser.release() like close does + // TODO It is not clear whether this method should call refCounterReleaser.release() like close + // does } /** @@ -311,8 +309,9 @@ public static final class NotReadyException extends LmdbException { /** Creates a new instance. */ public NotReadyException() { - super("Transaction is not in ready state, i.e. it has been closed/committed/aborted/reset. " + - "You may see this if have you tried to close a cursor after committing the transaction?"); + super( + "Transaction is not in ready state, i.e. it has been closed/committed/aborted/reset. " + + "You may see this if have you tried to close a cursor after committing the transaction?"); } } diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 6ab3b525..9094adaa 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -48,9 +48,7 @@ import org.lmdbjava.Cursor.ClosedException; import org.lmdbjava.Txn.ReadOnlyRequiredException; -/** - * Test {@link Cursor}. - */ +/** Test {@link Cursor}. */ public final class CursorTest { private Env env; @@ -60,13 +58,14 @@ public final class CursorTest { void beforeEach() { tempDir = new TempDir(); Path file = tempDir.createTempFile(); - env = create(PROXY_OPTIMAL) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxReaders(1) - .setMaxDbs(1) - .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() - .open(file); + env = + create(PROXY_OPTIMAL) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxReaders(1) + .setMaxDbs(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); } @AfterEach @@ -78,104 +77,104 @@ void afterEach() { @Test void closedCursorRejectsSubsequentGets() { assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - try (Txn txn = env.txnWrite()) { - final Cursor c = db.openCursor(txn); - c.close(); - c.seek(MDB_FIRST); - } - }) + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + try (Txn txn = env.txnWrite()) { + final Cursor c = db.openCursor(txn); + c.close(); + c.seek(MDB_FIRST); + } + }) .isInstanceOf(ClosedException.class); } @Test void closedEnvRejectsSeekFirstCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsSeekLastCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_LAST)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_LAST)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsSeekNextCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); - }) + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsCloseCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::close); - }) + () -> { + doEnvClosedTest(null, Cursor::close); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsFirstCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::first); - }) + () -> { + doEnvClosedTest(null, Cursor::first); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsLastCall() { assertThatThrownBy( - () -> { - doEnvClosedTest(null, Cursor::last); - }) + () -> { + doEnvClosedTest(null, Cursor::last); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsPrevCall() { assertThatThrownBy( - () -> { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt()).isEqualTo(1); - assertThat(c.val().getInt()).isEqualTo(10); - c.next(); - }, - Cursor::prev); - }) + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + c.next(); + }, + Cursor::prev); + }) .isInstanceOf(Env.EnvInUseException.class); } @Test void closedEnvRejectsDeleteCall() { assertThatThrownBy( - () -> { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt()).isEqualTo(1); - assertThat(c.val().getInt()).isEqualTo(10); - }, - Cursor::delete); - }) + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + }, + Cursor::delete); + }) .isInstanceOf(Env.EnvInUseException.class); } @@ -188,7 +187,7 @@ void countWithDupsort() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); assertThat(c.count()).isEqualTo(1L); c.put(bb(1), bb(4), MDB_APPENDDUP); @@ -205,7 +204,7 @@ void countWithoutDupsort() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThat(c.put(bb(1), bb(2), MDB_NOOVERWRITE)).isTrue(); assertThat(c.put(bb(1), bb(4))).isTrue(); assertThat(c.put(bb(1), bb(6), PutFlagSet.EMPTY)).isTrue(); @@ -243,18 +242,15 @@ void cursorCannotCloseIfTransactionCommitted() { c.put(bb(1), bb(4), MDB_APPENDDUP); assertThat(c.count()).isEqualTo(2L); - assertThat(txn.isReady()) - .isTrue(); + assertThat(txn.isReady()).isTrue(); txn.commit(); - assertThat(txn.isReady()) - .isFalse(); + assertThat(txn.isReady()).isFalse(); // Cursor is not in a ready state to be closed because we have committed // This makes it impossible to close the cursor and thus the env - assertThatThrownBy(c::close) - .isInstanceOf(Txn.NotReadyException.class); + assertThatThrownBy(c::close).isInstanceOf(Txn.NotReadyException.class); } } @@ -263,7 +259,7 @@ void cursorFirstLastNextPrev() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); c.put(bb(5), bb(6)); @@ -297,7 +293,7 @@ void delete() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -321,7 +317,7 @@ void delete2() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -345,7 +341,7 @@ void delete3() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); assertThat(c.seek(MDB_FIRST)).isTrue(); @@ -369,7 +365,7 @@ void getKeyVal() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); @@ -405,7 +401,7 @@ void putMultiple() { final int key = 100; final ByteBuffer k = bb(key); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.putMultiple(k, values, elemCount, MDB_MULTIPLE); assertThat(c.count()).isEqualTo((long) elemCount); } @@ -420,11 +416,11 @@ void putMultipleWithoutMdbMultipleFlag() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1); + }) .isInstanceOf(IllegalArgumentException.class); } } @@ -438,11 +434,11 @@ void putMultipleWithoutMdbMultipleFlag2() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1, PutFlags.EMPTY); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1, PutFlags.EMPTY); + }) .isInstanceOf(IllegalArgumentException.class); } } @@ -456,11 +452,11 @@ void putMultipleWithoutMdbMultipleFlag3() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { assertThatThrownBy( - () -> { - c.putMultiple(bb(100), bb(1), 1, (PutFlagSet) null); - }) + () -> { + c.putMultiple(bb(100), bb(1), 1, (PutFlagSet) null); + }) .isInstanceOf(NullPointerException.class); } } @@ -487,21 +483,21 @@ void renewTxRo() { @Test void renewTxRw() { assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - try (Txn txn = env.txnWrite()) { - assertThat(txn.isReadOnly()).isFalse(); - - try (Cursor c = db.openCursor(txn)) { - c.renew(txn); - } - } - }) + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + try (Txn txn = env.txnWrite()) { + assertThat(txn.isReadOnly()).isFalse(); + + try (Cursor c = db.openCursor(txn)) { + c.renew(txn); + } + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @@ -550,7 +546,7 @@ void returnValueForNoDupData() { .setDbiFlags(MDB_CREATE, MDB_DUPSORT) .open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA)).isTrue(); assertThat(c.put(bb(5), bb(7), MDB_NODUPDATA)).isTrue(); @@ -563,7 +559,7 @@ void returnValueForNoOverwrite() { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { // ok assertThat(c.put(bb(5), bb(6), MDB_NOOVERWRITE)).isTrue(); // fails, but gets exist val @@ -606,11 +602,7 @@ private void doEnvClosedTest( final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index d6540bfe..cbd0423b 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -49,9 +49,7 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** - * Test {@link Env}. - */ +/** Test {@link Env}. */ public final class EnvTest { private TempDir tempDir; @@ -70,11 +68,11 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } @@ -83,142 +81,142 @@ void byteUnit() { @Test void cannotChangeMapSizeAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMapSize(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMapSize(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangePermissionsAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setFilePermissions(0664); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setFilePermissions(0664); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangeMaxDbsAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxDbs(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxDbs(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotChangeMaxReadersAfterOpen() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxReaders(1); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxReaders(1); + } + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotInfoOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.info(); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.info(); + }) .isInstanceOf(AlreadyClosedException.class); } @Test void cannotOpenTwice() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - builder.open(file).close(); - //noinspection resource // This will fail to open - builder.open(file); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + builder.open(file).close(); + //noinspection resource // This will fail to open + builder.open(file); + }) .isInstanceOf(AlreadyOpenException.class); } @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void cannotStatOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.stat(); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.stat(); + }) .isInstanceOf(AlreadyClosedException.class); } @Test void cannotSyncOnceClosed() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.sync(false); - }) + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.sync(false); + }) .isInstanceOf(AlreadyClosedException.class); } @@ -251,52 +249,52 @@ void copyDirectoryBased_noFlags() { @Test void copyDirectoryRejectsFileDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - FileUtil.deleteDir(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + FileUtil.deleteDir(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @Test void copyDirectoryRejectsMissingDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - Files.delete(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + try { + Files.delete(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @Test void copyDirectoryRejectsNonEmptyDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - final Path subDir = dest.resolve("hello"); - Files.createDirectory(subDir); - assertThat(Files.isDirectory(subDir)).isTrue(); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) + () -> { + final Path dest = tempDir.createTempDir(); + try { + final Path subDir = dest.resolve("hello"); + Files.createDirectory(subDir); + assertThat(Files.isDirectory(subDir)).isTrue(); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @@ -314,16 +312,16 @@ void copyFileBased() { @Test void copyFileRejectsExistingDestination() { assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempFile(); - Files.createFile(dest); - assertThat(Files.exists(dest)).isTrue(); - final Path src = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) + () -> { + final Path dest = tempDir.createTempFile(); + Files.createFile(dest); + assertThat(Files.exists(dest)).isTrue(); + final Path src = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + }) .isInstanceOf(InvalidCopyDestination.class); } @@ -342,12 +340,12 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -356,14 +354,14 @@ void createAsFile() { @Test void detectTransactionThreadViolation() { assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { - env.txnRead(); - env.txnRead(); - } - }) + () -> { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + env.txnRead(); + env.txnRead(); + } + }) .isInstanceOf(BadReaderLockException.class); } @@ -371,12 +369,12 @@ void detectTransactionThreadViolation() { void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -393,34 +391,34 @@ void info() { @Test void mapFull() { assertThatThrownBy( - () -> { - final Path dir = tempDir.createTempDir(); - final byte[] k = new byte[500]; - final ByteBuffer key = allocateDirect(500); - final ByteBuffer val = allocateDirect(1_024); - final Random rnd = new Random(); - try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } - }) + () -> { + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } + }) .isInstanceOf(MapFullException.class); } @@ -433,7 +431,7 @@ void readOnlySupported() { rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -455,7 +453,7 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { + Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -475,15 +473,15 @@ void setMapSize() { assertThat(mapFullExThrown).isTrue(); assertThatThrownBy( - () -> { - env.setMapSize(-1, ByteUnit.KIBIBYTES); - }) + () -> { + env.setMapSize(-1, ByteUnit.KIBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy( - () -> { - env.setMapSize(-1); - }) + () -> { + env.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); env.setMapSize(1024, ByteUnit.KIBIBYTES); @@ -593,13 +591,13 @@ void testDefaultOpenNoName2() { void addEnvFlag() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -611,16 +609,16 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -632,13 +630,13 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -651,17 +649,17 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -673,13 +671,13 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -693,14 +691,13 @@ void setEnvFlags_null1() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) { - } + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -712,14 +709,13 @@ void setEnvFlags_null2() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) { - } + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -731,14 +727,13 @@ void setEnvFlags_null3() { Assertions.assertThatThrownBy( () -> { try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) { - } + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -746,19 +741,20 @@ void setEnvFlags_null3() { @Test void closeWithOpenReadTxn() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") final Env env = Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() - .open(file); + @SuppressWarnings("resource") + final Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); // Open but don't close final Txn readTxn = env.txnWrite(); - Assertions.assertThatThrownBy(env::close) - .isInstanceOf(Env.EnvInUseException.class); + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); readTxn.close(); env.close(); @@ -767,19 +763,20 @@ void closeWithOpenReadTxn() { @Test void closeWithOpenWriteTxn() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") final Env env = Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() - .open(file); + @SuppressWarnings("resource") + final Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); // Open but don't close final Txn writeTxn = env.txnWrite(); - Assertions.assertThatThrownBy(env::close) - .isInstanceOf(Env.EnvInUseException.class); + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); writeTxn.close(); env.close(); diff --git a/src/test/java/org/lmdbjava/RefCounterBenchmark.java b/src/test/java/org/lmdbjava/RefCounterBenchmark.java index 2acd322f..ca58efae 100644 --- a/src/test/java/org/lmdbjava/RefCounterBenchmark.java +++ b/src/test/java/org/lmdbjava/RefCounterBenchmark.java @@ -15,7 +15,6 @@ */ package org.lmdbjava; - import org.jspecify.annotations.NonNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -85,7 +84,6 @@ public void twoThreads(final MultiThreadPlan plan, final Blackhole blackhole) { releaser.release(); } - @Benchmark @BenchmarkMode(Mode.Throughput) @Measurement(iterations = ITERATIONS) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index e8bd003b..2c8a82e2 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -15,7 +15,6 @@ */ package org.lmdbjava; - import java.text.NumberFormat; import java.time.Duration; import java.time.Instant; @@ -42,7 +41,8 @@ public void perfTest() { for (int i = 1; i <= 3; i++) { final int round = i; // Run tests with all available processors - System.out.println("Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); + System.out.println( + "Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 16, 32, 64, 128, 256) @@ -60,28 +60,32 @@ public void perfTest() { System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); runPerfTest(0, new NoOpRefCounter()); - - // Run tests with set numbers of worker threads IntStream.of(32, 16, 8, 4, 2) - .forEach(threads -> { - System.out.println("Multi-threaded (" + threads + " threads) tests ---------------------------------"); - - System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); - IntStream.of(1, 16, 32, 64, 128, 256) - .forEach(stripes -> runPerfTest(stripes, threads, new StripedRefCounter(stripes))); - - System.out.println("Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); - runPerfTest(0, threads, new SimpleRefCounter()); - - System.out.println("Round: " + round + " " + SynchronisedRefCounter.class.getSimpleName()); - runPerfTest(0, threads, new SynchronisedRefCounter()); - - System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); - runPerfTest(0, threads, new NoOpRefCounter()); - }); - - + .forEach( + threads -> { + System.out.println( + "Multi-threaded (" + + threads + + " threads) tests ---------------------------------"); + + System.out.println( + "Round: " + round + " " + StripedRefCounter.class.getSimpleName()); + IntStream.of(1, 16, 32, 64, 128, 256) + .forEach( + stripes -> runPerfTest(stripes, threads, new StripedRefCounter(stripes))); + + System.out.println( + "Round: " + round + " " + SimpleRefCounter.class.getSimpleName()); + runPerfTest(0, threads, new SimpleRefCounter()); + + System.out.println( + "Round: " + round + " " + SynchronisedRefCounter.class.getSimpleName()); + runPerfTest(0, threads, new SynchronisedRefCounter()); + + System.out.println("Round: " + round + " " + NoOpRefCounter.class.getSimpleName()); + runPerfTest(0, threads, new NoOpRefCounter()); + }); System.out.println("Single-threaded tests ---------------------------------"); @@ -100,8 +104,8 @@ public void perfTest() { System.out.println("Round: " + round + " " + SingleThreadedRefCounter.class.getSimpleName()); runPerfTest(0, 1, new SingleThreadedRefCounter()); - - System.out.println("--------------------------------------------------------------------------------"); + System.out.println( + "--------------------------------------------------------------------------------"); System.out.println(); } } @@ -115,7 +119,7 @@ public void noOpRefCounter() { } private void doNoOpRefCounter() { -// System.out.println("Running test for " + stripes + " stripes"); + // System.out.println("Running test for " + stripes + " stripes"); final AtomicReference startTime = new AtomicReference<>(null); final CompletableFuture[] futures = new CompletableFuture[threadCount]; @@ -124,41 +128,50 @@ private void doNoOpRefCounter() { final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); final int iterationsPerThread = iterations / threadCount; for (int i = 0; i < threadCount; i++) { - futures[i] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); - - // Capture the start time - startTime.updateAndGet(currVal -> { - if (currVal == null) { - return Instant.now(); - } else { - return currVal; - } - }); - - for (int j = 0; j < iterationsPerThread; j++) { - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - try { - // Make sure we have an env that is not 'closed' - Objects.requireNonNull(env); - } finally { - releaser.release(); - } - } -// System.out.println(Thread.currentThread() + " - Done"); - }, executorService); + futures[i] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + + // Capture the start time + startTime.updateAndGet( + currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterationsPerThread; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + try { + // Make sure we have an env that is not 'closed' + Objects.requireNonNull(env); + } finally { + releaser.release(); + } + } + // System.out.println(Thread.currentThread() + " - Done"); + }, + executorService); } CompletableFuture.allOf(futures).join(); final Duration duration = Duration.between(startTime.get(), Instant.now()); final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - System.out.println("All Finished" - + ", threads: " + threadCount - + ", iterationsPerThread: " + iterationsPerThread - + ", duration: " + duration - + ", iterationsPerSec: " + NumberFormat.getInstance().format(iterationsPerSec)); + System.out.println( + "All Finished" + + ", threads: " + + threadCount + + ", iterationsPerThread: " + + iterationsPerThread + + ", duration: " + + duration + + ", iterationsPerSec: " + + NumberFormat.getInstance().format(iterationsPerSec)); } private void runPerfTest(int stripes, final RefCounter refCounter) { @@ -172,23 +185,27 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); final int iterationsPerThread = iterations / threadCount; for (int i = 0; i < threadCount; i++) { - futures[i] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); - // Capture the start time - startTime.updateAndGet(currVal -> { - if (currVal == null) { - return Instant.now(); - } else { - return currVal; - } - }); - - for (int j = 0; j < iterationsPerThread; j++) { - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - releaser.release(); - } - }, executorService); + futures[i] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + // Capture the start time + startTime.updateAndGet( + currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterationsPerThread; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + releaser.release(); + } + }, + executorService); } CompletableFuture.allOf(futures).join(); @@ -199,13 +216,18 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re final Duration duration = Duration.between(startTime.get(), Instant.now()); final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - - System.out.println("All Finished" - + ", stripes: " + stripes - + ", threads: " + threadCount - + ", iterationsPerThread: " + iterationsPerThread - + ", duration: " + duration - + ", iterationsPerSec: " + NumberFormat.getInstance().format(iterationsPerSec)); + System.out.println( + "All Finished" + + ", stripes: " + + stripes + + ", threads: " + + threadCount + + ", iterationsPerThread: " + + iterationsPerThread + + ", duration: " + + duration + + ", iterationsPerSec: " + + NumberFormat.getInstance().format(iterationsPerSec)); } private void countDownThenAwait(final CountDownLatch latch) { diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index da013d21..fa6e57ee 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -44,11 +44,9 @@ void acquire() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); // Acquire twice final RefCounter.RefCounterReleaser releaser1 = stripedRefCounter.acquire(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(1); + assertThat(stripedRefCounter.getCount()).isEqualTo(1); final RefCounter.RefCounterReleaser releaser2 = stripedRefCounter.acquire(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(2); + assertThat(stripedRefCounter.getCount()).isEqualTo(2); final AtomicInteger onCloseCallCount = new AtomicInteger(); @@ -59,13 +57,11 @@ void acquire() { }) .isInstanceOf(Env.EnvInUseException.class) .hasMessageContaining(" 2 "); - assertThat(onCloseCallCount) - .hasValue(0); + assertThat(onCloseCallCount).hasValue(0); // Release 1st releaser releaser1.release(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(1); + assertThat(stripedRefCounter.getCount()).isEqualTo(1); // Close not called as 1 un-released Assertions.assertThatThrownBy( @@ -74,33 +70,27 @@ void acquire() { }) .isInstanceOf(Env.EnvInUseException.class) .hasMessageContaining(" 1 "); - assertThat(onCloseCallCount) - .hasValue(0); + assertThat(onCloseCallCount).hasValue(0); // Release 2nd releaser releaser2.release(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(0); + assertThat(stripedRefCounter.getCount()).isEqualTo(0); // no-op if already released releaser1.release(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(0); + assertThat(stripedRefCounter.getCount()).isEqualTo(0); // no-op if already released releaser2.release(); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(0); + assertThat(stripedRefCounter.getCount()).isEqualTo(0); // onClose is called now stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount) - .hasValue(1); + assertThat(onCloseCallCount).hasValue(1); // no-op as onClose already called stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount) - .hasValue(1); + assertThat(onCloseCallCount).hasValue(1); } @Test @@ -114,21 +104,22 @@ void multipleThreads() { IntStream.range(0, threadCount) .boxed() - .map(i -> CompletableFuture.runAsync(() -> { - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); - callCounts[i].getAndIncrement(); - releaser.release(); - } - })) + .map( + i -> + CompletableFuture.runAsync( + () -> { + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); + callCounts[i].getAndIncrement(); + releaser.release(); + } + })) .forEach(CompletableFuture::join); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(0); + assertThat(stripedRefCounter.getCount()).isEqualTo(0); for (AtomicInteger callCount : callCounts) { - assertThat(callCount) - .hasValue(iterations); + assertThat(callCount).hasValue(iterations); } } @@ -152,72 +143,71 @@ void multipleThreads_delayedRelease() { IntStream.range(0, threads) .boxed() - .map(i -> CompletableFuture.runAsync(() -> { - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); - releasers.add(releaser); - callCounts[i].getAndIncrement(); - futures.add(CompletableFuture.runAsync(() -> { - final long count = stripedRefCounter.getCount(); - // System.out.println(Thread.currentThread() + " - getting count: " + count); - assertThat(count) - .isNotEqualTo(0); - }, executor2)); - } - }, executor)) + .map( + i -> + CompletableFuture.runAsync( + () -> { + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = + stripedRefCounter.acquire(); + releasers.add(releaser); + callCounts[i].getAndIncrement(); + futures.add( + CompletableFuture.runAsync( + () -> { + final long count = stripedRefCounter.getCount(); + // System.out.println(Thread.currentThread() + " + // - getting count: " + count); + assertThat(count).isNotEqualTo(0); + }, + executor2)); + } + }, + executor)) .forEach(CompletableFuture::join); } } - assertThat(stripedRefCounter.getCount()) - .isEqualTo((long) threads * iterations); + assertThat(stripedRefCounter.getCount()).isEqualTo((long) threads * iterations); for (AtomicInteger callCount : callCounts) { - assertThat(callCount) - .hasValue(iterations); + assertThat(callCount).hasValue(iterations); } releasers.forEach(RefCounter.RefCounterReleaser::release); futures.forEach(CompletableFuture::join); - assertThat(stripedRefCounter.getCount()) - .isEqualTo(0); + assertThat(stripedRefCounter.getCount()).isEqualTo(0); } @Test void testImmediateClose() { final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - assertThat(stripedRefCounter.isClosed()) - .isEqualTo(false); + assertThat(stripedRefCounter.isClosed()).isEqualTo(false); final AtomicInteger onCloseCallCount = new AtomicInteger(); stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount) - .hasValue(1); - assertThat(stripedRefCounter.isClosed()) - .isEqualTo(true); + assertThat(onCloseCallCount).hasValue(1); + assertThat(stripedRefCounter.isClosed()).isEqualTo(true); assertThatThrownBy(stripedRefCounter::checkNotClosed) .isInstanceOf(Env.AlreadyClosedException.class); // Check again as idempotent stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount) - .hasValue(1); - assertThat(stripedRefCounter.isClosed()) - .isEqualTo(true); + assertThat(onCloseCallCount).hasValue(1); + assertThat(stripedRefCounter.isClosed()).isEqualTo(true); assertThatThrownBy(stripedRefCounter::checkNotClosed) .isInstanceOf(Env.AlreadyClosedException.class); } /** - * Lots of threads all doing acquire/release in a loop, then the main thread - * tries to call refCounter.close(...), which will throw an - * {@link org.lmdbjava.Env.EnvInUseException}. It then makes all worker threads - * stop their looping and calls refCounter.close(...) again, successfully this - * time. + * Lots of threads all doing acquire/release in a loop, then the main thread tries to call + * refCounter.close(...), which will throw an {@link org.lmdbjava.Env.EnvInUseException}. It then + * makes all worker threads stop their looping and calls refCounter.close(...) again, successfully + * this time. */ @Test void testBehaviour() throws InterruptedException { @@ -246,34 +236,38 @@ void testBehaviour() throws InterruptedException { for (int i = 0; i < threadCount; i++) { final int threadIdx = i; - futures[threadIdx] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); -// System.out.println(Thread.currentThread() + " - Starting"); - for (int j = 0; j < iterations; j++) { - if (abortThreads.get()) { - break; - } - - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx].incrementAndGet(); - } catch (Env.AlreadyClosedException e) { - System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time - sleep(random.nextInt(5)); - // env is null after closure - Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); - } finally { - releaser.release(); - } - } -// System.out.println(Thread.currentThread() + " - Done"); - }, executorService); + futures[threadIdx] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + // System.out.println(Thread.currentThread() + " - Starting"); + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + break; + } + + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx].incrementAndGet(); + } catch (Env.AlreadyClosedException e) { + System.out.println( + Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + sleep(random.nextInt(5)); + // env is null after closure + Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + } + // System.out.println(Thread.currentThread() + " - Done"); + }, + executorService); } // Wait for all threads to start using the ref counter @@ -286,25 +280,23 @@ void testBehaviour() throws InterruptedException { final AtomicInteger onCloseCallCount = new AtomicInteger(); while (!didClose.get()) { try { - assertThat(mockEnv.get()) - .isNotNull(); + assertThat(mockEnv.get()).isNotNull(); System.out.println("close called " + ++closeCallCount); - refCounter.close(() -> { - onCloseCallCount.incrementAndGet(); - System.out.println("onClose called " + onCloseCallCount.get()); - // Imitate closing the env - mockEnv.set(null); - didClose.set(true); - }); + refCounter.close( + () -> { + onCloseCallCount.incrementAndGet(); + System.out.println("onClose called " + onCloseCallCount.get()); + // Imitate closing the env + mockEnv.set(null); + didClose.set(true); + }); if (didClose.get()) { // We closed, so env should be null - assertThat(mockEnv) - .hasNullValue(); + assertThat(mockEnv).hasNullValue(); } } catch (Env.EnvInUseException e) { // Failed to close as there are un-released items, so env still alive - assertThat(mockEnv.get()) - .isNotNull(); + assertThat(mockEnv.get()).isNotNull(); // Now poke all the treads to make them cleanly finish what they are doing so we // can try close() again abortThreads.set(true); @@ -315,27 +307,20 @@ void testBehaviour() throws InterruptedException { // Wait for all workers to finish CompletableFuture.allOf(futures).join(); - System.out.println("Acquire call count: " + Arrays.stream(counts) - .mapToLong(AtomicLong::get) - .sum()); + System.out.println( + "Acquire call count: " + Arrays.stream(counts).mapToLong(AtomicLong::get).sum()); // Make sure the mock env is all closed down - assertThat(mockEnv) - .hasNullValue(); - assertThat(refCounter.isClosed()) - .isEqualTo(true); - assertThat(refCounter.getCount()) - .isZero(); - assertThatThrownBy(refCounter::acquire) - .isInstanceOf(Env.AlreadyClosedException.class); - assertThat(onCloseCallCount) - .hasValue(1); + assertThat(mockEnv).hasNullValue(); + assertThat(refCounter.isClosed()).isEqualTo(true); + assertThat(refCounter.getCount()).isZero(); + assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); + assertThat(onCloseCallCount).hasValue(1); } } /** - * Ensure we can call getCount when multiple threads are all calling acquire/release - * in a loop. + * Ensure we can call getCount when multiple threads are all calling acquire/release in a loop. */ @Test void testGetCount() throws InterruptedException { @@ -348,7 +333,7 @@ void testGetCount() throws InterruptedException { final AtomicBoolean abortThreads = new AtomicBoolean(false); for (int k = 0; k < rounds; k++) { -// final int round = k; + // final int round = k; System.out.printf("Round %s ----------------------------------------%n", k); // Reset the env @@ -361,37 +346,41 @@ void testGetCount() throws InterruptedException { for (int i = 0; i < threadCount; i++) { final int threadIdx = i; - futures[threadIdx] = CompletableFuture.runAsync(() -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); -// System.out.println(Thread.currentThread() + " - Starting"); - - for (int j = 0; j < iterations; j++) { - if (abortThreads.get()) { - break; - } - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx]++; - } catch (Env.AlreadyClosedException e) { -// System.out.println(Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time - sleep(random.nextInt(5)); - // env is null after closure - Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); - } finally { - releaser.release(); - } - // Random sleep after releasing so there is a time when the thread - // is not using the 'env' - sleep(5 + random.nextInt(5)); - } -// System.out.println(Thread.currentThread() + " - Done"); - }, executorService); + futures[threadIdx] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + countDownThenAwait(startLatch); + // System.out.println(Thread.currentThread() + " - Starting"); + + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + break; + } + final RefCounter.RefCounterReleaser releaser; + try { + releaser = refCounter.acquire(); + counts[threadIdx]++; + } catch (Env.AlreadyClosedException e) { + // System.out.println(Thread.currentThread() + ", round: " + + // round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + sleep(random.nextInt(5)); + // env is null after closure + Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + // Random sleep after releasing so there is a time when the thread + // is not using the 'env' + sleep(5 + random.nextInt(5)); + } + // System.out.println(Thread.currentThread() + " - Done"); + }, + executorService); } // Wait for all threads to start using the ref counter @@ -426,17 +415,20 @@ void getCountRacingWithCloseDoesNotReturnZeroAfterClose() { refCounter.close(onCloseCallCount::incrementAndGet); - assertThat(refCounter.getCount()) - .isZero(); + assertThat(refCounter.getCount()).isZero(); } @Test void failedOnCloseDoesNotCloseOrCorruptCounter() { final StripedRefCounter refCounter = new StripedRefCounter(); - assertThatThrownBy(() -> refCounter.close(() -> { - throw new RuntimeException("boom"); - })).isInstanceOf(RuntimeException.class); + assertThatThrownBy( + () -> + refCounter.close( + () -> { + throw new RuntimeException("boom"); + })) + .isInstanceOf(RuntimeException.class); assertThat(refCounter.isClosed()).isFalse(); @@ -453,54 +445,44 @@ void concurrentCloseIsIdempotent() { final CountDownLatch startLatch = new CountDownLatch(2); - final CompletableFuture first = CompletableFuture.runAsync(() -> { - countDownThenAwait(startLatch); - refCounter.close(onCloseCallCount::incrementAndGet); - }); - final CompletableFuture second = CompletableFuture.runAsync(() -> { - countDownThenAwait(startLatch); - refCounter.close(onCloseCallCount::incrementAndGet); - }); + final CompletableFuture first = + CompletableFuture.runAsync( + () -> { + countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); + final CompletableFuture second = + CompletableFuture.runAsync( + () -> { + countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); CompletableFuture.allOf(first, second).join(); assertThat(onCloseCallCount).hasValue(1); assertThat(refCounter.isClosed()).isTrue(); - assertThatThrownBy(refCounter::acquire) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); } @Test void lowestPowerOfTwoGreaterThanOrEqualTo() { // Test powers of two - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1)) - .isEqualTo(1); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(2)) - .isEqualTo(2); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(4)) - .isEqualTo(4); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(8)) - .isEqualTo(8); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(16)) - .isEqualTo(16); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1024)) - .isEqualTo(1024); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1)).isEqualTo(1); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(2)).isEqualTo(2); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(4)).isEqualTo(4); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(8)).isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(16)).isEqualTo(16); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1024)).isEqualTo(1024); // Test non-powers of two - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(3)) - .isEqualTo(4); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(5)) - .isEqualTo(8); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(7)) - .isEqualTo(8); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(15)) - .isEqualTo(16); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(24)) - .isEqualTo(32); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(100)) - .isEqualTo(128); - assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1000)) - .isEqualTo(1024); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(3)).isEqualTo(4); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(5)).isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(7)).isEqualTo(8); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(15)).isEqualTo(16); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(24)).isEqualTo(32); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(100)).isEqualTo(128); + assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(1000)).isEqualTo(1024); // Test edge cases assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(536870912)) diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index d02084b9..7a9ed705 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -50,9 +50,7 @@ import org.lmdbjava.Txn.ReadWriteRequiredException; import org.lmdbjava.Txn.ResetException; -/** - * Test {@link Txn}. - */ +/** Test {@link Txn}. */ public final class TxnTest { private Path file; @@ -64,7 +62,8 @@ public final class TxnTest { void beforeEach() { tempDir = new TempDir(); file = tempDir.createTempFile(); - env = create() + env = + create() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) @@ -81,17 +80,17 @@ void afterEach() { @Test void largeKeysRejected() { assertThatThrownBy( - () -> { - final Dbi dbi = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); - key.limit(key.capacity()); - dbi.put(key, bb(2)); - }) + () -> { + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); + key.limit(key.capacity()); + dbi.put(key, bb(2)); + }) .isInstanceOf(BadValueSizeException.class); } @@ -134,7 +133,7 @@ void rangeSearch() { void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); } @@ -144,52 +143,52 @@ void readOnlyTxnAllowedInReadOnlyEnv() { @Test void readWriteTxnDeniedInReadOnlyEnv() { assertThatThrownBy( - () -> { - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - env.close(); - try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { - roEnv.txnWrite(); // error - } - }) + () -> { + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + env.close(); + try (Env roEnv = + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + roEnv.txnWrite(); // error + } + }) .isInstanceOf(EnvIsReadOnly.class); } @Test void testCheckNotCommitted() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.commit(); - txn.checkReady(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.commit(); + txn.checkReady(); + } + }) .isInstanceOf(NotReadyException.class); } @Test void testCheckReadOnly() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnWrite()) { - txn.checkReadOnly(); - } - }) + () -> { + try (Txn txn = env.txnWrite()) { + txn.checkReadOnly(); + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void testCheckWritesAllowed() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.checkWritesAllowed(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.checkWritesAllowed(); + } + }) .isInstanceOf(ReadWriteRequiredException.class); } @@ -230,8 +229,7 @@ void txCannotAbortIfAlreadyCommitted() { txn.commit(); assertThat(txn.getState()).isEqualTo(DONE); - assertThatThrownBy(txn::abort) - .isInstanceOf(NotReadyException.class); + assertThatThrownBy(txn::abort).isInstanceOf(NotReadyException.class); } } @@ -239,16 +237,14 @@ void txCannotAbortIfAlreadyCommitted() { void txCannotCommitTwice() { try (Txn txn = env.txnRead()) { txn.commit(); - assertThatThrownBy(txn::commit) - .isInstanceOf(NotReadyException.class); + assertThatThrownBy(txn::commit).isInstanceOf(NotReadyException.class); } } @Test void txConstructionDeniedIfEnvClosed() { env.close(); - assertThatThrownBy(env::txnRead) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(env::txnRead).isInstanceOf(AlreadyClosedException.class); } @Test @@ -256,46 +252,41 @@ void txRenewDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); txnRead.close(); env.close(); - assertThatThrownBy(txnRead::renew) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(txnRead::renew).isInstanceOf(AlreadyClosedException.class); } @Test void txCloseDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); env.close(); - assertThatThrownBy(txnRead::close) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(txnRead::close).isInstanceOf(AlreadyClosedException.class); } @Test void txCommitDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); env.close(); - assertThatThrownBy(txnRead::commit) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(txnRead::commit).isInstanceOf(AlreadyClosedException.class); } @Test void txAbortDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); env.close(); - assertThatThrownBy(txnRead::abort) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(txnRead::abort).isInstanceOf(AlreadyClosedException.class); } @Test void txResetDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); env.close(); - assertThatThrownBy(txnRead::reset) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(txnRead::reset).isInstanceOf(AlreadyClosedException.class); } @Test public void txParent() { try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { + Txn txChild = env.txn(txRoot)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } @@ -305,9 +296,9 @@ public void txParent() { public void txParent2() { try (Txn txRoot = env.txnWrite()) { assertThatThrownBy( - () -> { - env.txn(txRoot, (TxnFlagSet) null); - }) + () -> { + env.txn(txRoot, (TxnFlagSet) null); + }) .isInstanceOf(NullPointerException.class); } } @@ -315,7 +306,7 @@ public void txParent2() { @Test public void txParent3() { try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { + Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { assertThat(txRoot.getParent()).isNull(); assertThat(txChild.getParent()).isEqualTo(txRoot); } @@ -324,35 +315,35 @@ public void txParent3() { @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { - env.close(); - assertThat(txChild.getParent()).isEqualTo(txRoot); - } - }) + () -> { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot)) { + env.close(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + }) .isInstanceOf(AlreadyClosedException.class); } @Test void txParentROChildRWIncompatible() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnRead()) { - env.txn(txRoot); // error - } - }) + () -> { + try (Txn txRoot = env.txnRead()) { + env.txn(txRoot); // error + } + }) .isInstanceOf(IncompatibleParent.class); } @Test void txParentRWChildROIncompatible() { assertThatThrownBy( - () -> { - try (Txn txRoot = env.txnWrite()) { - env.txn(txRoot, MDB_RDONLY_TXN); // error - } - }) + () -> { + try (Txn txRoot = env.txnWrite()) { + env.txn(txRoot, MDB_RDONLY_TXN); // error + } + }) .isInstanceOf(IncompatibleParent.class); } @@ -392,54 +383,54 @@ void txReadWrite() { @Test void txRenewDeniedWithoutPriorReset() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.renew(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.renew(); + } + }) .isInstanceOf(NotResetException.class); } @Test void txResetDeniedForAlreadyResetTransaction() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnRead()) { - txn.reset(); - txn.renew(); - txn.reset(); - txn.reset(); - } - }) + () -> { + try (Txn txn = env.txnRead()) { + txn.reset(); + txn.renew(); + txn.reset(); + txn.reset(); + } + }) .isInstanceOf(ResetException.class); } @Test void txResetDeniedForReadWriteTransaction() { assertThatThrownBy( - () -> { - try (Txn txn = env.txnWrite()) { - txn.reset(); - } - }) + () -> { + try (Txn txn = env.txnWrite()) { + txn.reset(); + } + }) .isInstanceOf(ReadOnlyRequiredException.class); } @Test void zeroByteKeysRejected() { assertThatThrownBy( - () -> { - final Dbi dbi = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - final ByteBuffer key = allocateDirect(4); - key.putInt(1); - assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped - dbi.put(key, bb(2)); - }) + () -> { + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + final ByteBuffer key = allocateDirect(4); + key.putInt(1); + assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped + dbi.put(key, bb(2)); + }) .isInstanceOf(BadValueSizeException.class); } } From 16999f0cc86a9a939be024691db54faa8cfc4059 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:53:14 +0100 Subject: [PATCH 32/61] gh-279 Change tests to use Env.safeClose Disable tests that try to use the Env after closure. --- src/main/java/org/lmdbjava/Env.java | 32 +- src/main/java/org/lmdbjava/Txn.java | 16 +- .../org/lmdbjava/ByteBufferProxyTest.java | 5 +- .../org/lmdbjava/CursorDeprecatedTest.java | 7 +- .../CursorIterableIntegerKeyTest.java | 56 +- .../org/lmdbjava/CursorIterablePerfTest.java | 3 +- .../org/lmdbjava/CursorIterableRangeTest.java | 96 +-- .../java/org/lmdbjava/CursorIterableTest.java | 6 + .../java/org/lmdbjava/CursorParamTest.java | 6 +- src/test/java/org/lmdbjava/CursorTest.java | 2 + .../java/org/lmdbjava/DbiBuilderTest.java | 3 +- .../java/org/lmdbjava/DbiDeprecatedTest.java | 16 +- src/test/java/org/lmdbjava/DbiTest.java | 17 +- .../java/org/lmdbjava/EnvDeprecatedTest.java | 36 +- src/test/java/org/lmdbjava/EnvTest.java | 671 ++++++++++-------- .../org/lmdbjava/GarbageCollectionTest.java | 5 +- src/test/java/org/lmdbjava/TutorialTest.java | 8 +- .../java/org/lmdbjava/TxnDeprecatedTest.java | 6 +- src/test/java/org/lmdbjava/TxnTest.java | 11 +- src/test/java/org/lmdbjava/VerifierTest.java | 4 +- 20 files changed, 569 insertions(+), 437 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 9b03554a..72e917b5 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -118,20 +118,20 @@ private RefCounter initRefCounter(boolean isSingleThreaded) { } /** - * Create an {@link Env} using the {@link ByteBufferProxy#PROXY_OPTIMAL}. + * Create an {@link Env.Builder} using the {@link ByteBufferProxy#PROXY_OPTIMAL}. * - * @return the environment (never null) + * @return the builder for creating an environment. */ public static Builder create() { return new Builder<>(PROXY_OPTIMAL); } /** - * Create an {@link Env} using the passed {@link BufferProxy}. + * Create an {@link Env.Builder} using the passed {@link BufferProxy}. * * @param buffer type * @param proxy the proxy to use (required) - * @return the environment (never null) + * @return the builder for creating an environment. */ public static Builder create(final BufferProxy proxy) { return new Builder<>(proxy); @@ -159,10 +159,10 @@ public static Env open(final File path, final int size, final EnvFla * *

    Will silently return if already closed or never opened. * - *

    Before and during this call, the caller MUST ensure that: + *

    Before and during this call, the caller MUST ensure that: * *

      - *
    • every {@link Txn}, {@link Cursor} obtained from this environment has already been closed; + *
    • every {@link Txn} and {@link Cursor} obtained from this environment has already been closed; * and *
    • no other thread is executing any operation on this environment or on a handle * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as @@ -181,10 +181,11 @@ public static Env open(final File path, final int size, final EnvFla * the read lock for the entire duration of its transaction and {@code close()} holds the write * lock, so the map is never unmapped while a read is in flight. * - *

      If safeClose has been enabled, {@link Env#close()} will throw a {@link EnvInUseException} if - * transactions or cursors are still active. + *

      If safeClose has been enabled on the {@link Env}, then this method will throw a + * {@link EnvInUseException} if transactions or cursors are still active. * - * @throws EnvInUseException if a {@link Txn} or {@link Cursor} is still open on this {@link Env}. + * @throws EnvInUseException If safeClose has been set and {@link Txn} or {@link Cursor} is still + * open on this {@link Env} */ @Override public void close() { @@ -1056,7 +1057,18 @@ public Builder singleThreaded(final boolean singleThreaded) { return this; } - /** See {@link Env.Builder#setSafeClose(boolean)} */ + /** + * Enables the opt-in "safe close" for the resulting {@link Env}. + * + *

      When enabled, the environment tracks its live transactions and cursors so that closure of + * the {@link Env} is prevented if transactions or cursors are active. This adds a small amount + * of bookkeeping on transaction start/close; it is disabled by default so + * applications that already manage their own threading (the common low-latency case) pay + * nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if + * transactions or cursors are active. + * + * @return the builder + */ public Builder setSafeClose() { checkEnvNotOpened(); return setSafeClose(true); diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 70052b60..c6a35274 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -87,12 +87,15 @@ public void abort() { LIB.mdb_txn_abort(ptr); // TODO It is not clear whether this method should call refCounterReleaser.release() like close - // does + // does } /** * Closes this transaction. Any uncommitted work will be aborted first. * + *

      If any {@link Cursor}s have been opened on this transaction, they MUST be closed + * first, else you will not be able to close the cursor after its transaction has been closed. + * *

      Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for * each read-only buffer (ie the key and value). */ @@ -113,7 +116,10 @@ public void close() { refCounterReleaser.release(); } - /** Commits this transaction. */ + /** + * Commits this transaction. + *

      If you have an open cursor using this transaction, you must close the cursor before committing. + * */ public void commit() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -138,7 +144,7 @@ public long getId() { /** * Obtains this transaction's parent. * - * @return the parent transaction (may be null) + * @return the parent transaction (if present, i.e. may be null) */ public Txn getParent() { return parent; @@ -190,6 +196,7 @@ public void renew() { /** * Aborts this read-only transaction and resets the transaction handle, so it can be reused upon * calling {@link #renew()}. + *

      Not applicable to write transactions. */ public void reset() { if (SHOULD_CHECK) { @@ -311,7 +318,8 @@ public static final class NotReadyException extends LmdbException { public NotReadyException() { super( "Transaction is not in ready state, i.e. it has been closed/committed/aborted/reset. " - + "You may see this if have you tried to close a cursor after committing the transaction?"); + + "You may see this if have you tried to close a cursor after committing the transaction, " + + "or if you have tried to use a cursor after closing its transaction."); } } diff --git a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java index 2e4ed823..d5853f8c 100644 --- a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java +++ b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.lang.Integer.BYTES; @@ -63,7 +62,7 @@ void buffersMustBeDirect() { () -> { try (final TempDir tempDir = new TempDir()) { final Path dir = tempDir.createTempDir(); - try (Env env = create().setMaxReaders(1).open(dir)) { + try (Env env = create().setSafeClose().setSafeClose().setMaxReaders(1).open(dir)) { final Dbi db = env.createDbi() .setDbName(DB_1) diff --git a/src/test/java/org/lmdbjava/CursorDeprecatedTest.java b/src/test/java/org/lmdbjava/CursorDeprecatedTest.java index a4528577..a395397e 100644 --- a/src/test/java/org/lmdbjava/CursorDeprecatedTest.java +++ b/src/test/java/org/lmdbjava/CursorDeprecatedTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import java.nio.file.Path; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.ReadOnlyRequiredException; @@ -62,6 +63,7 @@ void beforeEach() { Path file = tempDir.createTempFile(); env = create(PROXY_OPTIMAL) + .setSafeClose() .setMapSize(MEBIBYTES.toBytes(1)) .setMaxReaders(1) .setMaxDbs(1) @@ -90,13 +92,14 @@ void count() { } } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void cursorCannotCloseIfTransactionCommitted() { assertThatThrownBy( () -> { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); try (Txn txn = env.txnWrite()) { - try (Cursor c = db.openCursor(txn); ) { + try (Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), new PutFlags[] {MDB_APPENDDUP}); assertThat(c.count()).isEqualTo(1L); c.put(bb(1), bb(4), new PutFlags[] {MDB_APPENDDUP}); diff --git a/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java index c562ed15..175cd5d3 100644 --- a/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,6 +102,7 @@ public void before() throws IOException { final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; env = Env.create(bufferProxy) + .setSafeClose() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(3) @@ -122,17 +123,18 @@ public void testNumericOrderLong() { final Dbi dbi = dbiFactory.factory.apply(env); try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - long i = 1; - while (true) { - // System.out.println("putting " + i); - c.put(bbNative(i), bb(i + "-long")); - final long i2 = i * 10; - if (i2 < i) { - // Overflowed - break; + try (Cursor c = dbi.openCursor(txn)) { + long i = 1; + while (true) { + // System.out.println("putting " + i); + c.put(bbNative(i), bb(i + "-long")); + final long i2 = i * 10; + if (i2 < i) { + // Overflowed + break; + } + i = i2; } - i = i2; } txn.commit(); } @@ -165,17 +167,18 @@ public void testNumericOrderInt() { final Dbi dbi = dbiFactory.factory.apply(env); try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - int i = 1; - while (true) { - // System.out.println("putting " + i); - c.put(bbNative(i), bb(i + "-int")); - final int i2 = i * 10; - if (i2 < i) { - // Overflowed - break; + try (Cursor c = dbi.openCursor(txn)) { + int i = 1; + while (true) { + // System.out.println("putting " + i); + c.put(bbNative(i), bb(i + "-int")); + final int i2 = i * 10; + if (i2 < i) { + // Overflowed + break; + } + i = i2; } - i = i2; } txn.commit(); } @@ -279,11 +282,12 @@ private void populateTestDataList() { private void populateDatabase(final Dbi dbi) { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bbNative(2), bb(3), MDB_NOOVERWRITE); - c.put(bbNative(4), bb(5)); - c.put(bbNative(6), bb(7)); - c.put(bbNative(8), bb(9)); + try (Cursor c = dbi.openCursor(txn)) { + c.put(bbNative(2), bb(3), MDB_NOOVERWRITE); + c.put(bbNative(4), bb(5)); + c.put(bbNative(6), bb(7)); + c.put(bbNative(8), bb(9)); + } txn.commit(); } } diff --git a/src/test/java/org/lmdbjava/CursorIterablePerfTest.java b/src/test/java/org/lmdbjava/CursorIterablePerfTest.java index 198ffd28..dcf69e59 100644 --- a/src/test/java/org/lmdbjava/CursorIterablePerfTest.java +++ b/src/test/java/org/lmdbjava/CursorIterablePerfTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,7 @@ public void before() { final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; env = create(bufferProxy) + .setSafeClose() .setMapSize(1, ByteUnit.GIBIBYTES) .setMaxReaders(1) .setMaxDbs(3) diff --git a/src/test/java/org/lmdbjava/CursorIterableRangeTest.java b/src/test/java/org/lmdbjava/CursorIterableRangeTest.java index ab76d3fc..b068b554 100644 --- a/src/test/java/org/lmdbjava/CursorIterableRangeTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableRangeTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; @@ -261,6 +260,7 @@ private void testCSV( final Path file = tempDir.createTempFile(); try (final Env env = create() + .setSafeClose() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(1) @@ -329,13 +329,14 @@ private long getLong(final ByteBuffer byteBuffer, final ByteOrder byteOrder) { private BiConsumer, Dbi> createBasicDBPopulator() { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bb(0), bb(1)); - c.put(bb(2), bb(3)); - c.put(bb(4), bb(5)); - c.put(bb(6), bb(7)); - c.put(bb(8), bb(9)); - c.put(bb(-2), bb(-1)); + try (Cursor c = dbi.openCursor(txn)) { + c.put(bb(0), bb(1)); + c.put(bb(2), bb(3)); + c.put(bb(4), bb(5)); + c.put(bb(6), bb(7)); + c.put(bb(8), bb(9)); + c.put(bb(-2), bb(-1)); + } txn.commit(); } }; @@ -344,14 +345,15 @@ private BiConsumer, Dbi> createBasicDBPopulator() { private BiConsumer, Dbi> createMultiDBPopulator(final int copies) { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - for (int i = 0; i < copies; i++) { - c.put(bb(0), bb(1 + i)); - c.put(bb(2), bb(3 + i)); - c.put(bb(4), bb(5 + i)); - c.put(bb(6), bb(7 + i)); - c.put(bb(8), bb(9 + i)); - c.put(bb(-2), bb(-1 + i)); + try (Cursor c = dbi.openCursor(txn)) { + for (int i = 0; i < copies; i++) { + c.put(bb(0), bb(1 + i)); + c.put(bb(2), bb(3 + i)); + c.put(bb(4), bb(5 + i)); + c.put(bb(6), bb(7 + i)); + c.put(bb(8), bb(9 + i)); + c.put(bb(-2), bb(-1 + i)); + } } txn.commit(); } @@ -362,14 +364,15 @@ private BiConsumer, Dbi> createMultiIntegerDBPopulat final int copies) { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - for (int i = 0; i < copies; i++) { - c.put(bbNative(0), bb(1 + i)); - c.put(bbNative(2), bb(3 + i)); - c.put(bbNative(4), bb(5 + i)); - c.put(bbNative(6), bb(7 + i)); - c.put(bbNative(8), bb(9 + i)); - c.put(bbNative(-2), bb(-1 + i)); + try (Cursor c = dbi.openCursor(txn)) { + for (int i = 0; i < copies; i++) { + c.put(bbNative(0), bb(1 + i)); + c.put(bbNative(2), bb(3 + i)); + c.put(bbNative(4), bb(5 + i)); + c.put(bbNative(6), bb(7 + i)); + c.put(bbNative(8), bb(9 + i)); + c.put(bbNative(-2), bb(-1 + i)); + } } txn.commit(); } @@ -380,14 +383,15 @@ private BiConsumer, Dbi> createMultiLongDBPopulator( final int copies) { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - for (int i = 0; i < copies; i++) { - c.put(bbNative(0L), bb(1 + i)); - c.put(bbNative(2L), bb(3 + i)); - c.put(bbNative(4L), bb(5 + i)); - c.put(bbNative(6L), bb(7 + i)); - c.put(bbNative(8L), bb(9 + i)); - c.put(bbNative(-2L), bb(-1 + i)); + try (Cursor c = dbi.openCursor(txn)) { + for (int i = 0; i < copies; i++) { + c.put(bbNative(0L), bb(1 + i)); + c.put(bbNative(2L), bb(3 + i)); + c.put(bbNative(4L), bb(5 + i)); + c.put(bbNative(6L), bb(7 + i)); + c.put(bbNative(8L), bb(9 + i)); + c.put(bbNative(-2L), bb(-1 + i)); + } } txn.commit(); } @@ -397,12 +401,13 @@ private BiConsumer, Dbi> createMultiLongDBPopulator( private BiConsumer, Dbi> createIntegerDBPopulator() { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bbNative(0), bb(1)); - c.put(bbNative(1000), bb(2)); - c.put(bbNative(1000000), bb(3)); - c.put(bbNative(-1000000), bb(4)); - c.put(bbNative(-1000), bb(5)); + try (Cursor c = dbi.openCursor(txn)) { + c.put(bbNative(0), bb(1)); + c.put(bbNative(1000), bb(2)); + c.put(bbNative(1000000), bb(3)); + c.put(bbNative(-1000000), bb(4)); + c.put(bbNative(-1000), bb(5)); + } txn.commit(); } }; @@ -411,12 +416,13 @@ private BiConsumer, Dbi> createIntegerDBPopulator() private BiConsumer, Dbi> createLongDBPopulator() { return (env, dbi) -> { try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bbNative(0L), bb(1)); - c.put(bbNative(1000L), bb(2)); - c.put(bbNative(1000000L), bb(3)); - c.put(bbNative(-1000000L), bb(4)); - c.put(bbNative(-1000L), bb(5)); + try (Cursor c = dbi.openCursor(txn)) { + c.put(bbNative(0L), bb(1)); + c.put(bbNative(1000L), bb(2)); + c.put(bbNative(1000000L), bb(3)); + c.put(bbNative(-1000000L), bb(4)); + c.put(bbNative(-1000L), bb(5)); + } txn.commit(); } }; diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 3dca93f6..1ab5e1d7 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -59,6 +59,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.Parameter; @@ -93,6 +94,7 @@ void beforeEach() { final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; env = create(bufferProxy) + .setSafeClose() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(3) @@ -352,6 +354,7 @@ void removeOddElements() { verify(db, all(), 4, 8); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void nextWithClosedEnvTest() { assertThatThrownBy( @@ -369,6 +372,7 @@ void nextWithClosedEnvTest() { .isInstanceOf(Env.AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void removeWithClosedEnvTest() { assertThatThrownBy( @@ -389,6 +393,7 @@ void removeWithClosedEnvTest() { .isInstanceOf(Env.AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void hasNextWithClosedEnvTest() { assertThatThrownBy( @@ -406,6 +411,7 @@ void hasNextWithClosedEnvTest() { .isInstanceOf(Env.AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void forEachRemainingWithClosedEnvTest() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/CursorParamTest.java b/src/test/java/org/lmdbjava/CursorParamTest.java index 28f60419..ec5c93ca 100644 --- a/src/test/java/org/lmdbjava/CursorParamTest.java +++ b/src/test/java/org/lmdbjava/CursorParamTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.lang.Long.BYTES; @@ -92,7 +91,7 @@ protected AbstractBufferRunner(final BufferProxy proxy) { @Override public final void execute(final Path tmp) { - try (Env env = env(tmp)) { + try (final Env env = env(tmp)) { assertThat(env.getDbiNames()).isEmpty(); final Dbi db = env.createDbi() @@ -170,6 +169,7 @@ public final void execute(final Path tmp) { private Env env(final Path tmp) { return create(proxy) + .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxReaders(1) .setMaxDbs(1) diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 9094adaa..6ab58a81 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -60,6 +60,7 @@ void beforeEach() { Path file = tempDir.createTempFile(); env = create(PROXY_OPTIMAL) + .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxReaders(1) .setMaxDbs(1) @@ -74,6 +75,7 @@ void afterEach() { tempDir.cleanup(); } + @Test void closedCursorRejectsSubsequentGets() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/DbiBuilderTest.java b/src/test/java/org/lmdbjava/DbiBuilderTest.java index c06c3dc9..7685cc10 100644 --- a/src/test/java/org/lmdbjava/DbiBuilderTest.java +++ b/src/test/java/org/lmdbjava/DbiBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ public void before() { tempDir = new TempDir(); env = create() + .setSafeClose() .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(2) .setMaxDbs(2) diff --git a/src/test/java/org/lmdbjava/DbiDeprecatedTest.java b/src/test/java/org/lmdbjava/DbiDeprecatedTest.java index 7156b963..b7c4c422 100644 --- a/src/test/java/org/lmdbjava/DbiDeprecatedTest.java +++ b/src/test/java/org/lmdbjava/DbiDeprecatedTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,7 @@ import java.util.function.ToIntFunction; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.CursorIterable.KeyVal; import org.lmdbjava.Dbi.DbFullException; @@ -78,6 +79,7 @@ void beforeEach() { final Path file = tempDir.createTempFile(); env = create() + .setSafeClose() .setMapSize(MEBIBYTES.toBytes(64)) .setMaxReaders(2) .setMaxDbs(2) @@ -86,6 +88,7 @@ void beforeEach() { final Path fileBa = tempDirBa.createTempFile(); envBa = create(PROXY_BA) + .setSafeClose() .setMapSize(MEBIBYTES.toBytes(64)) .setMaxReaders(2) .setMaxDbs(2) @@ -372,6 +375,7 @@ void putCommitGetByteArray() { final Path file = tempDir.createTempFile(); try (Env envBa = create(PROXY_BA) + .setSafeClose() .setMapSize(MEBIBYTES.toBytes(64)) .setMaxReaders(1) .setMaxDbs(2) @@ -550,6 +554,7 @@ void closedEnvRejectsOpenCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsCloseCall() { assertThatThrownBy( @@ -559,6 +564,7 @@ void closedEnvRejectsCloseCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsGetCall() { assertThatThrownBy( @@ -573,6 +579,7 @@ void closedEnvRejectsGetCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsPutCall() { assertThatThrownBy( @@ -582,6 +589,7 @@ void closedEnvRejectsPutCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsPutWithTxnCall() { assertThatThrownBy( @@ -595,6 +603,7 @@ void closedEnvRejectsPutWithTxnCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsIterateCall() { assertThatThrownBy( @@ -604,6 +613,7 @@ void closedEnvRejectsIterateCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsDropCall() { assertThatThrownBy( @@ -613,6 +623,7 @@ void closedEnvRejectsDropCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsDropAndDeleteCall() { assertThatThrownBy( @@ -622,6 +633,7 @@ void closedEnvRejectsDropAndDeleteCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsOpenCursorCall() { assertThatThrownBy( @@ -631,6 +643,7 @@ void closedEnvRejectsOpenCursorCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsReserveCall() { assertThatThrownBy( @@ -640,6 +653,7 @@ void closedEnvRejectsReserveCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsStatCall() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/DbiTest.java b/src/test/java/org/lmdbjava/DbiTest.java index 575937f5..28834bb0 100644 --- a/src/test/java/org/lmdbjava/DbiTest.java +++ b/src/test/java/org/lmdbjava/DbiTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.lang.Long.MAX_VALUE; @@ -61,6 +60,7 @@ import java.util.function.ToIntFunction; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.CursorIterable.KeyVal; import org.lmdbjava.Dbi.DbFullException; @@ -81,6 +81,7 @@ void beforeEach() { final Path file = tempDir.createTempFile(); env = create() + .setSafeClose() .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(2) .setMaxDbs(2) @@ -89,6 +90,7 @@ void beforeEach() { final Path fileBa = tempDir.createTempFile(); envBa = create(PROXY_BA) + .setSafeClose() .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(2) .setMaxDbs(2) @@ -492,6 +494,7 @@ void putCommitGetByteArray() { final Path file = tempDir.createTempFile(); try (Env envBa = create(PROXY_BA) + .setSafeClose() .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(1) .setMaxDbs(2) @@ -697,6 +700,7 @@ void closedEnvRejectsOpenCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsCloseCall() { assertThatThrownBy( @@ -706,6 +710,7 @@ void closedEnvRejectsCloseCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsGetCall() { assertThatThrownBy( @@ -721,6 +726,7 @@ void closedEnvRejectsGetCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsPutCall() { assertThatThrownBy( @@ -730,6 +736,7 @@ void closedEnvRejectsPutCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsPutWithTxnCall() { assertThatThrownBy( @@ -743,6 +750,7 @@ void closedEnvRejectsPutWithTxnCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsIterateCall() { assertThatThrownBy( @@ -752,6 +760,7 @@ void closedEnvRejectsIterateCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsDropCall() { assertThatThrownBy( @@ -761,6 +770,7 @@ void closedEnvRejectsDropCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsDropAndDeleteCall() { assertThatThrownBy( @@ -770,6 +780,7 @@ void closedEnvRejectsDropAndDeleteCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsOpenCursorCall() { assertThatThrownBy( @@ -779,6 +790,7 @@ void closedEnvRejectsOpenCursorCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsReserveCall() { assertThatThrownBy( @@ -788,6 +800,7 @@ void closedEnvRejectsReserveCall() { .isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void closedEnvRejectsStatCall() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/EnvDeprecatedTest.java b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java index da004cc8..2db29c9f 100644 --- a/src/test/java/org/lmdbjava/EnvDeprecatedTest.java +++ b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,6 +69,7 @@ void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = Env.create() + .setSafeClose() .setMaxReaders(1) .setMapSize(MEBIBYTES.toBytes(1)) .open(file.toFile(), MDB_NOSUBDIR)) { @@ -82,7 +83,7 @@ void cannotChangeMapSizeAfterOpen() { assertThatThrownBy( () -> { final Path file = tempDir.createTempFile(); - final Builder builder = Env.create().setMaxReaders(1); + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { builder.setMapSize(1); } @@ -95,7 +96,7 @@ void cannotChangeMaxDbsAfterOpen() { assertThatThrownBy( () -> { final Path file = tempDir.createTempFile(); - final Builder builder = Env.create().setMaxReaders(1); + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { builder.setMaxDbs(1); } @@ -108,7 +109,7 @@ void cannotChangeMaxReadersAfterOpen() { assertThatThrownBy( () -> { final Path file = tempDir.createTempFile(); - final Builder builder = Env.create().setMaxReaders(1); + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { builder.setMaxReaders(1); } @@ -122,7 +123,7 @@ void cannotInfoOnceClosed() { () -> { final Path file = tempDir.createTempFile(); final Env env = - Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + Env.create().setSafeClose().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); env.close(); env.info(); }) @@ -134,7 +135,7 @@ void cannotOpenTwice() { assertThatThrownBy( () -> { final Path file = tempDir.createTempFile(); - final Builder builder = Env.create().setMaxReaders(1); + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); builder.open(file.toFile(), MDB_NOSUBDIR).close(); builder.open(file.toFile(), MDB_NOSUBDIR); }) @@ -147,7 +148,7 @@ void cannotStatOnceClosed() { () -> { final Path file = tempDir.createTempFile(); final Env env = - Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + Env.create().setSafeClose().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); env.close(); env.stat(); }) @@ -160,7 +161,7 @@ void cannotSyncOnceClosed() { () -> { final Path file = tempDir.createTempFile(); final Env env = - Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + Env.create().setSafeClose().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); env.close(); env.sync(false); }) @@ -174,7 +175,7 @@ void copyDirectoryBased() { assertThat(Files.exists(dest)).isTrue(); assertThat(Files.isDirectory(dest)).isTrue(); assertThat(FileUtil.count(dest)).isEqualTo(0); - try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); assertThat(FileUtil.count(dest)).isEqualTo(1); } @@ -187,7 +188,7 @@ void copyDirectoryRejectsFileDestination() { final Path dest = tempDir.createTempDir(); final Path src = tempDir.createTempDir(); FileUtil.deleteDir(dest); - try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } }) @@ -202,7 +203,7 @@ void copyDirectoryRejectsMissingDestination() { () -> { try { Files.delete(dest); - try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } } catch (final IOException e) { @@ -222,7 +223,7 @@ void copyDirectoryRejectsNonEmptyDestination() { final Path subDir = dest.resolve("hello"); Files.createDirectory(subDir); assertThat(Files.isDirectory(subDir)).isTrue(); - try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } } catch (final IOException e) { @@ -237,7 +238,7 @@ void copyFileBased() { final Path dest = tempDir.createTempFile(); final Path src = tempDir.createTempFile(); assertThat(Files.exists(dest)).isFalse(); - try (Env env = Env.create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { env.copy(dest.toFile(), MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); @@ -252,7 +253,7 @@ void copyFileRejectsExistingDestination() { Files.createFile(dest); assertThat(Files.exists(dest)).isTrue(); try (Env env = - Env.create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { + Env.create().setSafeClose().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { env.copy(dest.toFile(), MDB_CP_COMPACT); } }) @@ -264,6 +265,7 @@ void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = Env.create() + .setSafeClose() .setMapSize(MEBIBYTES.toBytes(1)) .setMaxDbs(1) .setMaxReaders(1) @@ -284,6 +286,7 @@ void mapFull() { final Random rnd = new Random(); try (Env env = Env.create() + .setSafeClose() .setMaxReaders(1) .setMapSize(MEBIBYTES.toBytes(8)) .setMaxDbs(1) @@ -304,11 +307,11 @@ void mapFull() { @Test void readOnlySupported() { final Path dir = tempDir.createTempDir(); - try (Env rwEnv = Env.create().setMaxReaders(1).open(dir.toFile())) { + try (Env rwEnv = Env.create().setSafeClose().setMaxReaders(1).open(dir.toFile())) { final Dbi rwDb = rwEnv.openDbi(DB_1, MDB_CREATE); rwDb.put(bb(1), bb(42)); } - try (Env roEnv = Env.create().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { + try (Env roEnv = Env.create().setSafeClose().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { final Dbi roDb = roEnv.openDbi(DB_1); try (Txn roTxn = roEnv.txnRead()) { assertThat(roDb.get(roTxn, bb(1))).isNotNull(); @@ -325,6 +328,7 @@ void setMapSize() { final Random rnd = new Random(); try (Env env = Env.create() + .setSafeClose() .setMaxReaders(1) .setMapSize(KIBIBYTES.toBytes(256)) .setMaxDbs(1) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index cbd0423b..ca9031ea 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -49,7 +49,9 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** Test {@link Env}. */ +/** + * Test {@link Env}. + */ public final class EnvTest { private TempDir tempDir; @@ -68,155 +70,152 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } } @Test - void cannotChangeMapSizeAfterOpen() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMapSize(1); - } - }) - .isInstanceOf(AlreadyOpenException.class); - } - - @Test - void cannotChangePermissionsAfterOpen() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setFilePermissions(0664); - } - }) - .isInstanceOf(AlreadyOpenException.class); - } - - @Test - void cannotChangeMaxDbsAfterOpen() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxDbs(1); - } - }) - .isInstanceOf(AlreadyOpenException.class); - } + void cannotChangeBuilderAfterOpen() { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.open(file)) { - @Test - void cannotChangeMaxReadersAfterOpen() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { - builder.setMaxReaders(1); - } - }) - .isInstanceOf(AlreadyOpenException.class); + // Now try to modify the builder after it has been used to open an Env + assertThatThrownBy( + () -> builder.setMapSize(1)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + builder::setSafeClose) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setSafeClose(true)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR))) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setMaxReaders(1)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setFilePermissions(0666)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setMaxDbs(1)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setMapSize(1, ByteUnit.MEBIBYTES)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.addEnvFlag(MDB_NOSYNC)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.addEnvFlags(EnvFlagSet.of(MDB_NOSYNC))) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.addEnvFlags(Collections.singleton(MDB_NOSYNC))) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setEnvFlags(MDB_NOSYNC)) + .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy( + () -> builder.setEnvFlags(Collections.singleton(MDB_NOSYNC))) + .isInstanceOf(AlreadyOpenException.class); + //noinspection resource + assertThatThrownBy( + () -> builder.open(file)) + .isInstanceOf(AlreadyOpenException.class); + } } @Test void cannotInfoOnceClosed() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.info(); - }) + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + assertThatThrownBy(env::info) .isInstanceOf(AlreadyClosedException.class); } - @Test - void cannotOpenTwice() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Builder builder = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); - builder.open(file).close(); - //noinspection resource // This will fail to open - builder.open(file); - }) - .isInstanceOf(AlreadyOpenException.class); - } - @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void cannotStatOnceClosed() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.stat(); - }) + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + assertThatThrownBy(env::stat) .isInstanceOf(AlreadyClosedException.class); } @Test void cannotSyncOnceClosed() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - final Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); - env.close(); - env.sync(false); - }) + () -> env.sync(false)) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void cannotOpenReadTxnOnceClosed() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + assertThatThrownBy(env::txnRead) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void cannotOpenWriteTxnOnceClosed() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + assertThatThrownBy(env::txnWrite) .isInstanceOf(AlreadyClosedException.class); } @@ -227,7 +226,7 @@ void copyDirectoryBased() { assertThat(Files.isDirectory(dest)).isTrue(); assertThat(FileUtil.count(dest)).isEqualTo(0); final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { env.copy(dest, MDB_CP_COMPACT); assertThat(FileUtil.count(dest)).isEqualTo(1); } @@ -240,7 +239,7 @@ void copyDirectoryBased_noFlags() { assertThat(Files.isDirectory(dest)).isTrue(); assertThat(FileUtil.count(dest)).isEqualTo(0); final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { env.copy(dest); assertThat(FileUtil.count(dest)).isEqualTo(1); } @@ -248,54 +247,44 @@ void copyDirectoryBased_noFlags() { @Test void copyDirectoryRejectsFileDestination() { - assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - FileUtil.deleteDir(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) - .isInstanceOf(InvalidCopyDestination.class); + final Path dest = tempDir.createTempDir(); + FileUtil.deleteDir(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { + assertThatThrownBy( + () -> env.copy(dest, MDB_CP_COMPACT)) + .isInstanceOf(InvalidCopyDestination.class); + } } @Test void copyDirectoryRejectsMissingDestination() { - assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - Files.delete(dest); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) - .isInstanceOf(InvalidCopyDestination.class); + final Path dest = tempDir.createTempDir(); + try { + Files.delete(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { + assertThatThrownBy( + () -> env.copy(dest, MDB_CP_COMPACT)) + .isInstanceOf(InvalidCopyDestination.class); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } } @Test - void copyDirectoryRejectsNonEmptyDestination() { - assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempDir(); - try { - final Path subDir = dest.resolve("hello"); - Files.createDirectory(subDir); - assertThat(Files.isDirectory(subDir)).isTrue(); - final Path src = tempDir.createTempDir(); - try (Env env = Env.create().setMaxReaders(1).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }) - .isInstanceOf(InvalidCopyDestination.class); + void copyDirectoryRejectsNonEmptyDestination() throws IOException { + final Path dest = tempDir.createTempDir(); + final Path subDir = dest.resolve("hello"); + Files.createDirectory(subDir); + assertThat(Files.isDirectory(subDir)).isTrue(); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { + assertThatThrownBy( + () -> env.copy(dest, MDB_CP_COMPACT)) + .isInstanceOf(InvalidCopyDestination.class); + } } @Test @@ -303,32 +292,30 @@ void copyFileBased() { final Path dest = tempDir.createTempFile(); assertThat(Files.exists(dest)).isFalse(); final Path src = tempDir.createTempFile(); - try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { env.copy(dest, MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); } @Test - void copyFileRejectsExistingDestination() { - assertThatThrownBy( - () -> { - final Path dest = tempDir.createTempFile(); - Files.createFile(dest); - assertThat(Files.exists(dest)).isTrue(); - final Path src = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } - }) - .isInstanceOf(InvalidCopyDestination.class); + void copyFileRejectsExistingDestination() throws IOException { + final Path dest = tempDir.createTempFile(); + Files.createFile(dest); + assertThat(Files.exists(dest)).isTrue(); + final Path src = tempDir.createTempFile(); + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + assertThatThrownBy( + () -> env.copy(dest, MDB_CP_COMPACT)) + .isInstanceOf(InvalidCopyDestination.class); + } } @Test void createAsDirectory() { final Path dest = tempDir.createTempDir(); - final Env env = Env.create().setMaxReaders(1).open(dest); + final Env env = Env.create().setSafeClose().setMaxReaders(1).open(dest); assertThat(Files.isDirectory(dest)).isTrue(); env.sync(false); env.close(); @@ -340,12 +327,13 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -353,28 +341,42 @@ void createAsFile() { @Test void detectTransactionThreadViolation() { - assertThatThrownBy( - () -> { - final Path file = tempDir.createTempFile(); - try (Env env = - Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { - env.txnRead(); - env.txnRead(); - } - }) - .isInstanceOf(BadReaderLockException.class); + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { + try (Txn ignored = env.txnRead()) { + // When NOT using MDB_NOTLS flag, you cannot open a second read txn on the same thread + assertThatThrownBy(env::txnRead) + .isInstanceOf(BadReaderLockException.class); + } + } + } + + @Test + void multipleReadTxnsOnSameThread() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS).open(file)) { + try (Txn ignored1 = env.txnRead()) { + // MDB_NOTLS flag allows us to open multiple read txns on the same thread + //noinspection EmptyTryBlock + try (Txn ignored2 = env.txnRead()) { + } + } + } } @Test void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -390,48 +392,49 @@ void info() { @Test void mapFull() { - assertThatThrownBy( - () -> { - final Path dir = tempDir.createTempDir(); - final byte[] k = new byte[500]; - final ByteBuffer key = allocateDirect(500); - final ByteBuffer val = allocateDirect(1_024); - final Random rnd = new Random(); - try (Env env = - Env.create() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } - }) - .isInstanceOf(MapFullException.class); + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + assertThatThrownBy( + () -> { + // Fill the env until MapFullException is thrown + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + }) + .isInstanceOf(MapFullException.class); + } } @Test void readOnlySupported() { final Path dir = tempDir.createTempDir(); - try (Env rwEnv = Env.create().setMaxReaders(1).open(dir)) { + try (Env rwEnv = Env.create().setSafeClose().setMaxReaders(1).open(dir)) { final Dbi rwDb = rwEnv.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -453,7 +456,7 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { + Env.create().setSafeClose().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -473,15 +476,11 @@ void setMapSize() { assertThat(mapFullExThrown).isTrue(); assertThatThrownBy( - () -> { - env.setMapSize(-1, ByteUnit.KIBIBYTES); - }) + () -> env.setMapSize(-1, ByteUnit.KIBIBYTES)) .isInstanceOf(IllegalArgumentException.class); assertThatThrownBy( - () -> { - env.setMapSize(-1); - }) + () -> env.setMapSize(-1)) .isInstanceOf(IllegalArgumentException.class); env.setMapSize(1024, ByteUnit.KIBIBYTES); @@ -511,7 +510,7 @@ void setMapSize() { @Test void stats() { final Path file = tempDir.createTempFile(); - try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + try (Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { final Stat stat = env.stat(); assertThat(stat).isNotNull(); assertThat(stat.branchPages).isEqualTo(0L); @@ -527,7 +526,7 @@ void stats() { @Test void testDefaultOpen() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -539,7 +538,7 @@ void testDefaultOpen() { @Test void testDefaultOpenNoName1() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -567,7 +566,7 @@ void testDefaultOpenNoName1() { @Test void testDefaultOpenNoName2() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -590,14 +589,15 @@ void testDefaultOpenNoName2() { @Test void addEnvFlag() { final Path file = tempDir.createTempFile(); - try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + try (final Env env = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -609,16 +609,17 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -630,13 +631,14 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -649,17 +651,18 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -671,13 +674,14 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -687,19 +691,23 @@ void setEnvFlags2() { @Test void setEnvFlags_null1() { final Path file = tempDir.createTempFile(); - // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir + // MDB_NOSUBDIR is cleared out, so it will error as file is a file not a dir Assertions.assertThatThrownBy( () -> { - try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) {} + //noinspection EmptyTryBlock + try (final Env ignored = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) { + } }) - .isInstanceOf(LmdbNativeException.class); + .isInstanceOf(LmdbNativeException.class) + .hasMessageContaining("No such file or directory"); } @Test @@ -708,14 +716,17 @@ void setEnvFlags_null2() { // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir Assertions.assertThatThrownBy( () -> { - try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) {} + //noinspection EmptyTryBlock + try (Env ignored = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -726,14 +737,17 @@ void setEnvFlags_null3() { // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir Assertions.assertThatThrownBy( () -> { - try (Env env = - Env.create() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) {} + //noinspection EmptyTryBlock + try (Env ignored = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -741,9 +755,30 @@ void setEnvFlags_null3() { @Test void closeWithOpenReadTxn() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") - final Env env = + final Env env = Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); + + // Open but don't close + final Txn readTxn = env.txnWrite(); + + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + + readTxn.close(); + env.close(); + } + + @Test + void closeWithOpenWriteTxn() { + final Path file = tempDir.createTempFile(); + @SuppressWarnings("resource") final Env env = Env.create() + .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxDbs(1) .setMaxReaders(1) @@ -752,20 +787,20 @@ void closeWithOpenReadTxn() { .open(file); // Open but don't close - final Txn readTxn = env.txnWrite(); + final Txn writeTxn = env.txnWrite(); Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); - readTxn.close(); + writeTxn.close(); env.close(); } @Test - void closeWithOpenWriteTxn() { + void closeWithOpenCursor() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") - final Env env = + @SuppressWarnings("resource") final Env env = Env.create() + .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxDbs(1) .setMaxReaders(1) @@ -773,12 +808,24 @@ void closeWithOpenWriteTxn() { .setSafeClose() .open(file); + final Dbi dbi = env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + // Open but don't close final Txn writeTxn = env.txnWrite(); + final Cursor cursor = dbi.openCursor(writeTxn); + // Close the txn but not the cursor + writeTxn.close(); - Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + Assertions.assertThatThrownBy(env::close) + .isInstanceOf(Env.EnvInUseException.class); - writeTxn.close(); - env.close(); + Assertions.assertThatThrownBy(cursor::close) + .isInstanceOf(Txn.NotReadyException.class); + + // can't close the env as we are unable to close the cursor } } diff --git a/src/test/java/org/lmdbjava/GarbageCollectionTest.java b/src/test/java/org/lmdbjava/GarbageCollectionTest.java index 4aa1245f..e587a422 100644 --- a/src/test/java/org/lmdbjava/GarbageCollectionTest.java +++ b/src/test/java/org/lmdbjava/GarbageCollectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static java.nio.ByteBuffer.allocateDirect; @@ -37,7 +36,7 @@ class GarbageCollectionTest { void buffersNotGarbageCollectedTest() { try (final TempDir tempDir = new TempDir()) { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setMapSize(2_085_760_999).setMaxDbs(1).open(dir)) { + try (Env env = Env.create().setSafeClose().setMapSize(2_085_760_999).setMaxDbs(1).open(dir)) { final Dbi db = env.createDbi() .setDbName(DB_NAME) diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index f631895d..611ff81e 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -88,6 +88,9 @@ void tutorial1() { .setMapSize(10_485_760) // LMDB also needs to know how many DBs (Dbi) we want to store in this Env. .setMaxDbs(1) + // Add additional checks to ensure the env is not close while in use. Adds + // some performance overhead + .setSafeClose() // Now let's open the Env. The same path can be concurrently opened and // used in different processes, but do not open the same path twice in // the same process at the same time. @@ -413,6 +416,7 @@ void tutorial6() { Env.create(PROXY_OPTIMAL) .setMapSize(10, ByteUnit.MEBIBYTES) .setMaxDbs(Verifier.DBI_COUNT) + .setSafeClose() .open(dir); // Create a Verifier (it's a Callable for those needing full control). @@ -433,7 +437,7 @@ void tutorial7() { // There's also a PROXY_SAFE if you want to stop ByteBuffer's Unsafe use. // Aside from that and a different type argument, it's the same as usual... final Env env = - Env.create(PROXY_DB).setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).open(dir); + Env.create(PROXY_DB).setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setSafeClose().open(dir); final Dbi db = env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -614,6 +618,6 @@ void tutorial9() { // or reverse ordered keys, using Env.DISABLE_CHECKS_PROP etc), but you now // know enough to tackle the JavaDocs with confidence. Have fun! private Env createSimpleEnv(final Path path) { - return Env.create().setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setMaxReaders(1).open(path); + return Env.create().setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setMaxReaders(1).setSafeClose().open(path); } } diff --git a/src/test/java/org/lmdbjava/TxnDeprecatedTest.java b/src/test/java/org/lmdbjava/TxnDeprecatedTest.java index 387e9fef..f3ef7ce1 100644 --- a/src/test/java/org/lmdbjava/TxnDeprecatedTest.java +++ b/src/test/java/org/lmdbjava/TxnDeprecatedTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; @@ -26,6 +25,7 @@ import java.nio.file.Path; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Txn.IncompatibleParent; @@ -50,6 +50,7 @@ void beforeEach() { file = tempDir.createTempFile(); env = create() + .setSafeClose() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) @@ -81,6 +82,7 @@ public void txParent2() { } } + @Disabled // We shouldn't be trying to close the env with open txns/cursors @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 7a9ed705..7f45eab8 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.lmdbjava.Dbi.BadValueSizeException; import org.lmdbjava.Env.AlreadyClosedException; @@ -64,6 +65,7 @@ void beforeEach() { file = tempDir.createTempFile(); env = create() + .setSafeClose() .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) @@ -133,7 +135,7 @@ void rangeSearch() { void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); } @@ -151,7 +153,7 @@ void readWriteTxnDeniedInReadOnlyEnv() { .open(); env.close(); try (Env roEnv = - create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { roEnv.txnWrite(); // error } }) @@ -255,6 +257,7 @@ void txRenewDeniedIfEnvClosed() { assertThatThrownBy(txnRead::renew).isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txCloseDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); @@ -262,6 +265,7 @@ void txCloseDeniedIfEnvClosed() { assertThatThrownBy(txnRead::close).isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txCommitDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); @@ -269,6 +273,7 @@ void txCommitDeniedIfEnvClosed() { assertThatThrownBy(txnRead::commit).isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txAbortDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); @@ -276,6 +281,7 @@ void txAbortDeniedIfEnvClosed() { assertThatThrownBy(txnRead::abort).isInstanceOf(AlreadyClosedException.class); } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txResetDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); @@ -312,6 +318,7 @@ public void txParent3() { } } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/VerifierTest.java b/src/test/java/org/lmdbjava/VerifierTest.java index ee396084..6364ea9c 100644 --- a/src/test/java/org/lmdbjava/VerifierTest.java +++ b/src/test/java/org/lmdbjava/VerifierTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +34,7 @@ void verification() { final Path file = tempDir.createTempFile(); try (Env env = create() + .setSafeClose() .setMaxReaders(1) .setMaxDbs(Verifier.DBI_COUNT) .setMapSize(10, ByteUnit.MEBIBYTES) From 6e8a6463bd17c87418bd9504ebbc0068404351ba Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:08:17 +0100 Subject: [PATCH 33/61] Refactor tests, rename singleThreaded builder method --- src/main/java/org/lmdbjava/Env.java | 8 +- .../java/org/lmdbjava/SimpleRefCounter.java | 13 +- .../org/lmdbjava/SynchronisedRefCounter.java | 9 +- .../java/org/lmdbjava/RefCounterTest.java | 510 +++++++++++++++++- .../org/lmdbjava/StripedRefCounterTest.java | 463 ---------------- src/test/java/org/lmdbjava/TestUtils.java | 21 + 6 files changed, 547 insertions(+), 477 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 72e917b5..a8aaa3c6 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -1032,11 +1032,11 @@ public Builder addEnvFlags(final Collection envFlags) { * If set, the caller is asserting that the Env will only be used by a single thread throughout * its entire life. This allows the {@link Env} to make minor optimisations that are not * thread-safe, e.g. using primitives rather than thread-safe objects. By default, an Env is - * considered thread-safe. + * assumed to be used by multiple threads. * * @return this builder instance. */ - public Builder singleThreaded() { + public Builder setSingleThreaded() { checkEnvNotOpened(); singleThreaded = true; return this; @@ -1046,12 +1046,12 @@ public Builder singleThreaded() { * If set to true, the caller is asserting that the Env will only be used by a single thread * throughout its entire life. This allows the {@link Env} to make minor optimisations that are * not thread-safe, e.g. using primitives rather than thread-safe objects. By default, an Env is - * considered thread-safe. + * assumed to be used by multiple threads. * * @param singleThreaded Set to true if the Env will only ever be used by a single thread. * @return this builder instance. */ - public Builder singleThreaded(final boolean singleThreaded) { + public Builder setSingleThreaded(final boolean singleThreaded) { checkEnvNotOpened(); this.singleThreaded = singleThreaded; return this; diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 8d6fd0b9..520e14eb 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -16,6 +16,7 @@ package org.lmdbjava; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; class SimpleRefCounter implements RefCounter { @@ -29,11 +30,19 @@ public boolean isClosed() { public RefCounterReleaser acquire() { final int newVal = - counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal + 1); + counter.updateAndGet(currVal -> + currVal == CLOSED_VALUE ? currVal : currVal + 1); if (newVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } - return this::release; + + final AtomicBoolean hasReleased = new AtomicBoolean(false); + return () -> { + // Prevent duplicate release calls + if (hasReleased.compareAndSet(false, true)) { + release(); + } + }; } @Override diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index 0abd2ab4..d4eafe5c 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -16,6 +16,7 @@ package org.lmdbjava; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; class SynchronisedRefCounter implements RefCounter { private static final int CLOSED_VALUE = Integer.MIN_VALUE; @@ -36,7 +37,13 @@ public RefCounterReleaser acquire() { } counter++; } - return this::release; + final AtomicBoolean hasReleased = new AtomicBoolean(false); + return () -> { + // Prevent duplicate release calls + if (hasReleased.compareAndSet(false, true)) { + release(); + } + }; } @Override diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 2c8a82e2..b06eb365 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -15,18 +15,33 @@ */ package org.lmdbjava; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.text.NumberFormat; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.Objects; +import java.util.Queue; +import java.util.Random; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class RefCounterTest { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); @@ -110,6 +125,486 @@ public void perfTest() { } } + /** + * @return A {@link Stream} of all {@link RefCounter}s + */ + static Stream allRefCounterProvider() { + return Stream.of( + new StripedRefCounter(), + new SingleThreadedRefCounter(), + new SimpleRefCounter(), + new SynchronisedRefCounter(), + new NoOpRefCounter()) + .map(refCounter -> Arguments.argumentSet( + refCounter.getClass().getSimpleName(), + refCounter)); + } + + /** + * @return A {@link Stream} of {@link RefCounter}s that support multi-threaded use + */ + static Stream multiThreadedRefCounterProvider() { + return Stream.of( + new StripedRefCounter(), + new SimpleRefCounter(), + new SynchronisedRefCounter()) + .map(refCounter -> Arguments.argumentSet( + refCounter.getClass().getSimpleName(), + refCounter)); + } + + private void assertRefCount(final RefCounter refCounter, final int expectedCount) { + // NoOpRefCounter does no reference counting, so we can't assert the count + if (!(refCounter instanceof NoOpRefCounter)) { + assertThat(refCounter.getCount()).isEqualTo(expectedCount); + } + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void testRefCounters(final RefCounter refCounter) { + // Acquire twice + final RefCounter.RefCounterReleaser releaser1 = refCounter.acquire(); + assertRefCount(refCounter, 1); + final RefCounter.RefCounterReleaser releaser2 = refCounter.acquire(); + assertRefCount(refCounter, 2); + + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + if (!(refCounter instanceof NoOpRefCounter)) { + // Close not called as 2 are un-released + Assertions.assertThatThrownBy( + () -> { + refCounter.close(onCloseCallCount::incrementAndGet); + }) + .isInstanceOf(Env.EnvInUseException.class) + .hasMessageContaining(" 2 "); + } + assertThat(onCloseCallCount).hasValue(0); + + // Release 1st releaser + releaser1.release(); + assertRefCount(refCounter, 1); + + if (!(refCounter instanceof NoOpRefCounter)) { + // Close not called as 1 un-released + Assertions.assertThatThrownBy( + () -> { + refCounter.close(onCloseCallCount::incrementAndGet); + }) + .isInstanceOf(Env.EnvInUseException.class) + .hasMessageContaining(" 1 "); + } + assertThat(onCloseCallCount).hasValue(0); + + // Release 2nd releaser + releaser2.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // no-op if already released + releaser1.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // no-op if already released + releaser2.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // onClose is called now + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount).hasValue(1); + + // no-op as onClose already called + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount).hasValue(1); + } + + @ParameterizedTest + @MethodSource("multiThreadedRefCounterProvider") + void multipleThreads(final RefCounter refCounter) { + final int iterations = 1000; + final AtomicInteger[] callCounts = new AtomicInteger[threadCount]; + for (int i = 0; i < threadCount; i++) { + callCounts[i] = new AtomicInteger(); + } + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + + final CompletableFuture[] futures = IntStream.range(0, threadCount) + .boxed() + .map( + i -> + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(countDownLatch); + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + callCounts[i].getAndIncrement(); + releaser.release(); + } + }, + executorService)) + .toArray(CompletableFuture[]::new); + + CompletableFuture.allOf(futures).join(); + + assertThat(refCounter.getCount()).isEqualTo(0); + + for (AtomicInteger callCount : callCounts) { + assertThat(callCount).hasValue(iterations); + } + } + } + + @ParameterizedTest + @MethodSource("multiThreadedRefCounterProvider") + void multipleThreads_delayedRelease(final RefCounter refCounter) { + final int iterations = 1000; + final AtomicInteger[] callCounts; + final Queue releasers; + + try (ExecutorService executor = Executors.newFixedThreadPool(threadCount)) { + try (ExecutorService executor2 = Executors.newFixedThreadPool(threadCount)) { + callCounts = new AtomicInteger[threadCount]; + for (int i = 0; i < threadCount; i++) { + callCounts[i] = new AtomicInteger(); + } + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + + releasers = new ConcurrentLinkedQueue<>(); + final Queue> futures = new ConcurrentLinkedQueue<>(); + + IntStream.range(0, threadCount) + .boxed() + .map( + i -> + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(countDownLatch); + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = + refCounter.acquire(); + releasers.add(releaser); + callCounts[i].getAndIncrement(); + futures.add( + CompletableFuture.runAsync( + () -> { + final long count = refCounter.getCount(); + assertThat(count).isNotEqualTo(0); + }, + executor2)); + } + }, + executor)) + .forEach(futures::add); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + } + + assertRefCount(refCounter, threadCount * iterations); + + for (AtomicInteger callCount : callCounts) { + assertThat(callCount).hasValue(iterations); + } + + releasers.forEach(RefCounter.RefCounterReleaser::release); + + assertThat(refCounter.getCount()).isEqualTo(0); + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void testImmediateClose(final RefCounter refCounter) { + assertThat(refCounter.isClosed()).isEqualTo(false); + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount).hasValue(1); + assertThat(refCounter.isClosed()).isEqualTo(true); + + assertThatThrownBy(refCounter::checkNotClosed) + .isInstanceOf(Env.AlreadyClosedException.class); + + // Check again as idempotent + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount).hasValue(1); + assertThat(refCounter.isClosed()).isEqualTo(true); + + assertThatThrownBy(refCounter::checkNotClosed) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + + /** + * Lots of threads all doing acquire/release in a loop, then the main thread tries to call + * refCounter.close(...), which will throw an {@link org.lmdbjava.Env.EnvInUseException}. Main thread then + * makes all worker threads stop their looping and calls refCounter.close(...) again, successfully + * this time. + */ + @ParameterizedTest + @MethodSource("multiThreadedRefCounterProvider") + void testBehaviour(final RefCounter refCounter) throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + final int rounds = 5; + final int iterations = 10_000_000; + final AtomicReference mockEnv = new AtomicReference<>(); + + for (int k = 0; k < rounds; k++) { + final int round = k; + System.out.printf("Round %s ----------------------------------------%n", round); + + // Reset the env + mockEnv.set(new Object()); + final RefCounter roundRefCounter = createNewRefCounter(refCounter); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final AtomicLong[] counts = new AtomicLong[threadCount]; + for (int i = 0; i < threadCount; i++) { + counts[i] = new AtomicLong(); + } + + final AtomicBoolean abortThreads = new AtomicBoolean(false); + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + TestUtils.countDownThenAwait(startLatch); + // System.out.println(Thread.currentThread() + " - Starting"); + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + System.out.println( + Thread.currentThread() + ", round: " + round + ", j: " + j + ", abortThreads is true"); + break; + } + + final RefCounter.RefCounterReleaser releaser; + try { + releaser = roundRefCounter.acquire(); + counts[threadIdx].incrementAndGet(); + } catch (Env.AlreadyClosedException e) { + System.out.println( + Thread.currentThread() + ", round: " + round + ", j: " + j + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + TestUtils.sleep(random.nextInt(5)); + // env is null after closure + assertThat(mockEnv.get()).isNotNull(); + // Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + } + // System.out.println(Thread.currentThread() + " - Done"); + }, + executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + TestUtils.sleep(200 + random.nextInt(200)); + final AtomicBoolean didClose = new AtomicBoolean(false); + int closeCallCount = 0; + final AtomicInteger onCloseCallCount = new AtomicInteger(); + while (!didClose.get()) { + try { + assertThat(mockEnv.get()).isNotNull(); + System.out.println("close called " + ++closeCallCount); + roundRefCounter.close( + () -> { + onCloseCallCount.incrementAndGet(); + System.out.println("onClose called " + onCloseCallCount.get()); + // Imitate closing the env + mockEnv.set(null); + didClose.set(true); + }); + if (didClose.get()) { + // We closed, so env should be null + assertThat(mockEnv).hasNullValue(); + } + } catch (Env.EnvInUseException e) { + // Failed to close as there are un-released items, so env still alive + assertThat(mockEnv.get()).isNotNull(); + // Now poke all the treads to make them cleanly finish what they are doing so we + // can try close() again + abortThreads.set(true); + TestUtils.sleep(500); + } + } + + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println( + "Acquire call count: " + Arrays.stream(counts).mapToLong(AtomicLong::get).sum()); + + // Make sure the mock env is all closed down + assertThat(mockEnv).hasNullValue(); + assertThat(roundRefCounter.isClosed()).isEqualTo(true); + assertThat(roundRefCounter.getCount()).isZero(); + assertThatThrownBy(roundRefCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); + assertThat(onCloseCallCount).hasValue(1); + } + } + } + + /** + * Ensure we can call getCount when multiple threads are all calling acquire/release in a loop. + */ + @ParameterizedTest + @MethodSource("multiThreadedRefCounterProvider") + void testGetCount(final RefCounter refCounter) throws InterruptedException { + final Random random = new Random(); + final int threadCount = this.threadCount - 1; + try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + final int rounds = 5; + final int iterations = 10_000_000; + final AtomicReference mockEnv = new AtomicReference<>(); + final AtomicBoolean abortThreads = new AtomicBoolean(false); + + for (int k = 0; k < rounds; k++) { + // final int round = k; + System.out.printf("Round %s ----------------------------------------%n", k); + + // Reset the env + mockEnv.set(new Object()); + abortThreads.set(false); + final RefCounter roundRefCounter = createNewRefCounter(refCounter); + final CountDownLatch startLatch = new CountDownLatch(threadCount); + final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final long[] counts = new long[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int threadIdx = i; + futures[threadIdx] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + TestUtils.countDownThenAwait(startLatch); + // System.out.println(Thread.currentThread() + " - Starting"); + + for (int j = 0; j < iterations; j++) { + if (abortThreads.get()) { + break; + } + final RefCounter.RefCounterReleaser releaser; + try { + releaser = roundRefCounter.acquire(); + counts[threadIdx]++; + } catch (Env.AlreadyClosedException e) { + // System.out.println(Thread.currentThread() + ", round: " + + // round + ", Env closed, aborting"); + break; + } + try { + // Make the work between acquire and release take some time + TestUtils.sleep(random.nextInt(5)); + // env is null after closure + Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + } finally { + releaser.release(); + } + // Random sleep after releasing so there is a time when the thread + // is not using the 'env' + TestUtils.sleep(5 + random.nextInt(5)); + } + // System.out.println(Thread.currentThread() + " - Done"); + }, + executorService); + } + + // Wait for all threads to start using the ref counter + startLatch.await(); + + // Give the other threads a chance to get underway + TestUtils.sleep(100 + random.nextInt(200)); + + for (int i = 0; i < 10; i++) { + try { + System.out.println("count: " + roundRefCounter.getCount()); + } catch (Env.EnvInUseException e) { + TestUtils.sleep(100 + random.nextInt(200)); + } + } + abortThreads.set(true); + // Wait for all workers to finish + CompletableFuture.allOf(futures).join(); + + System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); + + if (roundRefCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + roundRefCounter.getCount()); + } + } + } + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void immediateClose(final RefCounter refCounter) { + final AtomicInteger onCloseCallCount = new AtomicInteger(); + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(refCounter.getCount()).isZero(); + assertThat(onCloseCallCount.get()).isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void failedOnCloseDoesNotCloseOrCorruptCounter() { + final StripedRefCounter refCounter = new StripedRefCounter(); + + assertThatThrownBy( + () -> + refCounter.close( + () -> { + throw new RuntimeException("boom"); + })) + .isInstanceOf(RuntimeException.class); + + assertThat(refCounter.isClosed()).isFalse(); + + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + assertThat(refCounter.getCount()).isEqualTo(1); + releaser.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void concurrentCloseIsIdempotent() { + final StripedRefCounter refCounter = new StripedRefCounter(); + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + final CountDownLatch startLatch = new CountDownLatch(2); + + final CompletableFuture first = + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); + final CompletableFuture second = + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(startLatch); + refCounter.close(onCloseCallCount::incrementAndGet); + }); + + CompletableFuture.allOf(first, second).join(); + + assertThat(onCloseCallCount).hasValue(1); + assertThat(refCounter.isClosed()).isTrue(); + assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); + } + @Test public void noOpRefCounter() { // Do multiple rounds to let it warm up @@ -132,7 +627,7 @@ private void doNoOpRefCounter() { CompletableFuture.runAsync( () -> { // Wait for all threads to be ready - countDownThenAwait(startLatch); + TestUtils.countDownThenAwait(startLatch); // Capture the start time startTime.updateAndGet( @@ -172,6 +667,7 @@ private void doNoOpRefCounter() { + duration + ", iterationsPerSec: " + NumberFormat.getInstance().format(iterationsPerSec)); + executorService.close(); } private void runPerfTest(int stripes, final RefCounter refCounter) { @@ -189,7 +685,7 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re CompletableFuture.runAsync( () -> { // Wait for all threads to be ready - countDownThenAwait(startLatch); + TestUtils.countDownThenAwait(startLatch); // Capture the start time startTime.updateAndGet( currVal -> { @@ -230,13 +726,13 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re + NumberFormat.getInstance().format(iterationsPerSec)); } - private void countDownThenAwait(final CountDownLatch latch) { - latch.countDown(); + private static RefCounter createNewRefCounter(RefCounter refCounter) { + // Assumes all RefCounters have a no-arg constructor try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + return refCounter.getClass().getDeclaredConstructor().newInstance(); + } catch (Exception e) { throw new RuntimeException(e); } } + } diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index fa6e57ee..d4e94d46 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -16,455 +16,11 @@ package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.Arrays; -import java.util.Objects; -import java.util.Queue; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.IntStream; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; class StripedRefCounterTest { - private final int threadCount = Runtime.getRuntime().availableProcessors(); - - @Test - void acquire() { - final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - // Acquire twice - final RefCounter.RefCounterReleaser releaser1 = stripedRefCounter.acquire(); - assertThat(stripedRefCounter.getCount()).isEqualTo(1); - final RefCounter.RefCounterReleaser releaser2 = stripedRefCounter.acquire(); - assertThat(stripedRefCounter.getCount()).isEqualTo(2); - - final AtomicInteger onCloseCallCount = new AtomicInteger(); - - // Close not called as 2 un-released - Assertions.assertThatThrownBy( - () -> { - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - }) - .isInstanceOf(Env.EnvInUseException.class) - .hasMessageContaining(" 2 "); - assertThat(onCloseCallCount).hasValue(0); - - // Release 1st releaser - releaser1.release(); - assertThat(stripedRefCounter.getCount()).isEqualTo(1); - - // Close not called as 1 un-released - Assertions.assertThatThrownBy( - () -> { - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - }) - .isInstanceOf(Env.EnvInUseException.class) - .hasMessageContaining(" 1 "); - assertThat(onCloseCallCount).hasValue(0); - - // Release 2nd releaser - releaser2.release(); - assertThat(stripedRefCounter.getCount()).isEqualTo(0); - - // no-op if already released - releaser1.release(); - assertThat(stripedRefCounter.getCount()).isEqualTo(0); - - // no-op if already released - releaser2.release(); - assertThat(stripedRefCounter.getCount()).isEqualTo(0); - - // onClose is called now - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount).hasValue(1); - - // no-op as onClose already called - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount).hasValue(1); - } - - @Test - void multipleThreads() { - final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - final int iterations = 100; - final AtomicInteger[] callCounts = new AtomicInteger[threadCount]; - for (int i = 0; i < threadCount; i++) { - callCounts[i] = new AtomicInteger(); - } - - IntStream.range(0, threadCount) - .boxed() - .map( - i -> - CompletableFuture.runAsync( - () -> { - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = stripedRefCounter.acquire(); - callCounts[i].getAndIncrement(); - releaser.release(); - } - })) - .forEach(CompletableFuture::join); - - assertThat(stripedRefCounter.getCount()).isEqualTo(0); - - for (AtomicInteger callCount : callCounts) { - assertThat(callCount).hasValue(iterations); - } - } - - @Test - void multipleThreads_delayedRelease() { - final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - final int threads = Runtime.getRuntime().availableProcessors() - 2; - final int iterations = 100; - final AtomicInteger[] callCounts; - final Queue releasers; - final Queue> futures; - try (ExecutorService executor = Executors.newFixedThreadPool(threads)) { - try (ExecutorService executor2 = Executors.newFixedThreadPool(1)) { - callCounts = new AtomicInteger[threads]; - for (int i = 0; i < threads; i++) { - callCounts[i] = new AtomicInteger(); - } - - releasers = new ConcurrentLinkedQueue<>(); - futures = new ConcurrentLinkedQueue<>(); - - IntStream.range(0, threads) - .boxed() - .map( - i -> - CompletableFuture.runAsync( - () -> { - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = - stripedRefCounter.acquire(); - releasers.add(releaser); - callCounts[i].getAndIncrement(); - futures.add( - CompletableFuture.runAsync( - () -> { - final long count = stripedRefCounter.getCount(); - // System.out.println(Thread.currentThread() + " - // - getting count: " + count); - assertThat(count).isNotEqualTo(0); - }, - executor2)); - } - }, - executor)) - .forEach(CompletableFuture::join); - } - } - - assertThat(stripedRefCounter.getCount()).isEqualTo((long) threads * iterations); - - for (AtomicInteger callCount : callCounts) { - assertThat(callCount).hasValue(iterations); - } - - releasers.forEach(RefCounter.RefCounterReleaser::release); - - futures.forEach(CompletableFuture::join); - - assertThat(stripedRefCounter.getCount()).isEqualTo(0); - } - - @Test - void testImmediateClose() { - final StripedRefCounter stripedRefCounter = new StripedRefCounter(); - assertThat(stripedRefCounter.isClosed()).isEqualTo(false); - final AtomicInteger onCloseCallCount = new AtomicInteger(); - - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount).hasValue(1); - assertThat(stripedRefCounter.isClosed()).isEqualTo(true); - - assertThatThrownBy(stripedRefCounter::checkNotClosed) - .isInstanceOf(Env.AlreadyClosedException.class); - - // Check again as idempotent - stripedRefCounter.close(onCloseCallCount::incrementAndGet); - assertThat(onCloseCallCount).hasValue(1); - assertThat(stripedRefCounter.isClosed()).isEqualTo(true); - - assertThatThrownBy(stripedRefCounter::checkNotClosed) - .isInstanceOf(Env.AlreadyClosedException.class); - } - - /** - * Lots of threads all doing acquire/release in a loop, then the main thread tries to call - * refCounter.close(...), which will throw an {@link org.lmdbjava.Env.EnvInUseException}. It then - * makes all worker threads stop their looping and calls refCounter.close(...) again, successfully - * this time. - */ - @Test - void testBehaviour() throws InterruptedException { - final Random random = new Random(); - final int threadCount = this.threadCount - 1; - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int rounds = 10; - final int iterations = 10_000_000; - final AtomicReference mockEnv = new AtomicReference<>(); - - for (int k = 0; k < rounds; k++) { - final int round = k; - System.out.printf("Round %s ----------------------------------------%n", round); - - // Reset the env - mockEnv.set(new Object()); - final RefCounter refCounter = new StripedRefCounter(); - final CountDownLatch startLatch = new CountDownLatch(threadCount); - final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final AtomicLong[] counts = new AtomicLong[threadCount]; - for (int i = 0; i < threadCount; i++) { - counts[i] = new AtomicLong(); - } - - final AtomicBoolean abortThreads = new AtomicBoolean(false); - - for (int i = 0; i < threadCount; i++) { - final int threadIdx = i; - futures[threadIdx] = - CompletableFuture.runAsync( - () -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); - // System.out.println(Thread.currentThread() + " - Starting"); - for (int j = 0; j < iterations; j++) { - if (abortThreads.get()) { - break; - } - - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx].incrementAndGet(); - } catch (Env.AlreadyClosedException e) { - System.out.println( - Thread.currentThread() + ", round: " + round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time - sleep(random.nextInt(5)); - // env is null after closure - Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); - } finally { - releaser.release(); - } - } - // System.out.println(Thread.currentThread() + " - Done"); - }, - executorService); - } - - // Wait for all threads to start using the ref counter - startLatch.await(); - - // Give the other threads a chance to get underway - sleep(200 + random.nextInt(200)); - final AtomicBoolean didClose = new AtomicBoolean(false); - int closeCallCount = 0; - final AtomicInteger onCloseCallCount = new AtomicInteger(); - while (!didClose.get()) { - try { - assertThat(mockEnv.get()).isNotNull(); - System.out.println("close called " + ++closeCallCount); - refCounter.close( - () -> { - onCloseCallCount.incrementAndGet(); - System.out.println("onClose called " + onCloseCallCount.get()); - // Imitate closing the env - mockEnv.set(null); - didClose.set(true); - }); - if (didClose.get()) { - // We closed, so env should be null - assertThat(mockEnv).hasNullValue(); - } - } catch (Env.EnvInUseException e) { - // Failed to close as there are un-released items, so env still alive - assertThat(mockEnv.get()).isNotNull(); - // Now poke all the treads to make them cleanly finish what they are doing so we - // can try close() again - abortThreads.set(true); - sleep(500); - } - } - - // Wait for all workers to finish - CompletableFuture.allOf(futures).join(); - - System.out.println( - "Acquire call count: " + Arrays.stream(counts).mapToLong(AtomicLong::get).sum()); - - // Make sure the mock env is all closed down - assertThat(mockEnv).hasNullValue(); - assertThat(refCounter.isClosed()).isEqualTo(true); - assertThat(refCounter.getCount()).isZero(); - assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); - assertThat(onCloseCallCount).hasValue(1); - } - } - - /** - * Ensure we can call getCount when multiple threads are all calling acquire/release in a loop. - */ - @Test - void testGetCount() throws InterruptedException { - final Random random = new Random(); - final int threadCount = this.threadCount - 1; - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int rounds = 5; - final int iterations = 10_000_000; - final AtomicReference mockEnv = new AtomicReference<>(); - final AtomicBoolean abortThreads = new AtomicBoolean(false); - - for (int k = 0; k < rounds; k++) { - // final int round = k; - System.out.printf("Round %s ----------------------------------------%n", k); - - // Reset the env - mockEnv.set(new Object()); - abortThreads.set(false); - final RefCounter refCounter = new StripedRefCounter(); - final CountDownLatch startLatch = new CountDownLatch(threadCount); - final CompletableFuture[] futures = new CompletableFuture[threadCount]; - final long[] counts = new long[threadCount]; - - for (int i = 0; i < threadCount; i++) { - final int threadIdx = i; - futures[threadIdx] = - CompletableFuture.runAsync( - () -> { - // Wait for all threads to be ready - countDownThenAwait(startLatch); - // System.out.println(Thread.currentThread() + " - Starting"); - - for (int j = 0; j < iterations; j++) { - if (abortThreads.get()) { - break; - } - final RefCounter.RefCounterReleaser releaser; - try { - releaser = refCounter.acquire(); - counts[threadIdx]++; - } catch (Env.AlreadyClosedException e) { - // System.out.println(Thread.currentThread() + ", round: " + - // round + ", Env closed, aborting"); - break; - } - try { - // Make the work between acquire and release take some time - sleep(random.nextInt(5)); - // env is null after closure - Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); - } finally { - releaser.release(); - } - // Random sleep after releasing so there is a time when the thread - // is not using the 'env' - sleep(5 + random.nextInt(5)); - } - // System.out.println(Thread.currentThread() + " - Done"); - }, - executorService); - } - - // Wait for all threads to start using the ref counter - startLatch.await(); - - // Give the other threads a chance to get underway - sleep(100 + random.nextInt(200)); - - for (int i = 0; i < 10; i++) { - try { - System.out.println("count: " + refCounter.getCount()); - } catch (Env.EnvInUseException e) { - sleep(100 + random.nextInt(200)); - } - } - abortThreads.set(true); - // Wait for all workers to finish - CompletableFuture.allOf(futures).join(); - - System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); - - if (refCounter.getCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getCount()); - } - } - } - - @Test - void getCountRacingWithCloseDoesNotReturnZeroAfterClose() { - final StripedRefCounter refCounter = new StripedRefCounter(); - final AtomicInteger onCloseCallCount = new AtomicInteger(); - - refCounter.close(onCloseCallCount::incrementAndGet); - - assertThat(refCounter.getCount()).isZero(); - } - - @Test - void failedOnCloseDoesNotCloseOrCorruptCounter() { - final StripedRefCounter refCounter = new StripedRefCounter(); - - assertThatThrownBy( - () -> - refCounter.close( - () -> { - throw new RuntimeException("boom"); - })) - .isInstanceOf(RuntimeException.class); - - assertThat(refCounter.isClosed()).isFalse(); - - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - assertThat(refCounter.getCount()).isEqualTo(1); - releaser.release(); - assertThat(refCounter.getCount()).isEqualTo(0); - } - - @Test - void concurrentCloseIsIdempotent() { - final StripedRefCounter refCounter = new StripedRefCounter(); - final AtomicInteger onCloseCallCount = new AtomicInteger(); - - final CountDownLatch startLatch = new CountDownLatch(2); - - final CompletableFuture first = - CompletableFuture.runAsync( - () -> { - countDownThenAwait(startLatch); - refCounter.close(onCloseCallCount::incrementAndGet); - }); - final CompletableFuture second = - CompletableFuture.runAsync( - () -> { - countDownThenAwait(startLatch); - refCounter.close(onCloseCallCount::incrementAndGet); - }); - - CompletableFuture.allOf(first, second).join(); - - assertThat(onCloseCallCount).hasValue(1); - assertThat(refCounter.isClosed()).isTrue(); - assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); - } - @Test void lowestPowerOfTwoGreaterThanOrEqualTo() { // Test powers of two @@ -490,23 +46,4 @@ void lowestPowerOfTwoGreaterThanOrEqualTo() { assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(536870913)) .isEqualTo(1073741824); } - - private void countDownThenAwait(final CountDownLatch latch) { - latch.countDown(); - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - - private static void sleep(final int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } } diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index a15dc6b2..9b2bfcb5 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.function.Function; import org.agrona.MutableDirectBuffer; @@ -202,4 +203,24 @@ static ComparatorResult compare(final Comparator comparator, final T o1, final int result = comparator.compare(o1, o2); return ComparatorResult.get(result); } + + public static void countDownThenAwait(final CountDownLatch latch) { + Objects.requireNonNull(latch); + latch.countDown(); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + public static void sleep(final int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } } From 1e19b1a92a01e31c4255c550c075ab02f8b228e5 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:55:26 +0100 Subject: [PATCH 34/61] Tidy up tests --- src/main/java/org/lmdbjava/Env.java | 10 +- src/main/java/org/lmdbjava/RefCounter.java | 5 +- .../java/org/lmdbjava/SimpleRefCounter.java | 7 +- .../lmdbjava/SingleThreadedRefCounter.java | 2 +- .../java/org/lmdbjava/StripedRefCounter.java | 11 + .../org/lmdbjava/SynchronisedRefCounter.java | 5 +- src/main/java/org/lmdbjava/Txn.java | 16 +- .../org/lmdbjava/ByteBufferProxyTest.java | 3 +- src/test/java/org/lmdbjava/CursorTest.java | 1 - .../java/org/lmdbjava/EnvDeprecatedTest.java | 15 +- src/test/java/org/lmdbjava/EnvTest.java | 415 ++++++++---------- .../org/lmdbjava/GarbageCollectionTest.java | 3 +- .../java/org/lmdbjava/RefCounterTest.java | 154 ++++--- src/test/java/org/lmdbjava/TestUtils.java | 2 +- src/test/java/org/lmdbjava/TutorialTest.java | 13 +- src/test/java/org/lmdbjava/TxnTest.java | 12 +- 16 files changed, 341 insertions(+), 333 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index a8aaa3c6..08b88785 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -162,8 +162,8 @@ public static Env open(final File path, final int size, final EnvFla *

      Before and during this call, the caller MUST ensure that: * *

        - *
      • every {@link Txn} and {@link Cursor} obtained from this environment has already been closed; - * and + *
      • every {@link Txn} and {@link Cursor} obtained from this environment has already been + * closed; and *
      • no other thread is executing any operation on this environment or on a handle * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as * {@code Dbi.get}. @@ -181,11 +181,11 @@ public static Env open(final File path, final int size, final EnvFla * the read lock for the entire duration of its transaction and {@code close()} holds the write * lock, so the map is never unmapped while a read is in flight. * - *

        If safeClose has been enabled on the {@link Env}, then this method will throw a - * {@link EnvInUseException} if transactions or cursors are still active. + *

        If safeClose has been enabled on the {@link Env}, then this method will throw a {@link + * EnvInUseException} if transactions or cursors are still active. * * @throws EnvInUseException If safeClose has been set and {@link Txn} or {@link Cursor} is still - * open on this {@link Env} + * open on this {@link Env} */ @Override public void close() { diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index d3fcb514..a95521fc 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -15,7 +15,10 @@ */ package org.lmdbjava; -/** Used to prevent the closure of a thing while other threads are actively using that thing. */ +/** + * Used to prevent the closure of a resource while other threads are actively using that resource. + * Achieves this via reference counting. + */ interface RefCounter { /** diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 520e14eb..e2304a43 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -19,6 +19,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +/** + * An implementation of {@link RefCounter} that uses an {@link AtomicInteger} to track the number of + * references to a resource. + */ class SimpleRefCounter implements RefCounter { private static final int CLOSED_VALUE = Integer.MIN_VALUE; private final AtomicInteger counter = new AtomicInteger(0); @@ -30,8 +34,7 @@ public boolean isClosed() { public RefCounterReleaser acquire() { final int newVal = - counter.updateAndGet(currVal -> - currVal == CLOSED_VALUE ? currVal : currVal + 1); + counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal + 1); if (newVal == CLOSED_VALUE) { throw new Env.AlreadyClosedException(); } diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 59e85aea..65a59075 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -17,7 +17,7 @@ import java.util.Objects; -/** A {@link RefCounter} intented for use only in single threaded environments. */ +/** A {@link RefCounter} intented for use only in single-threaded environments. */ public class SingleThreadedRefCounter implements RefCounter { private int refCount; diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 1551155f..4e6ffe31 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -19,6 +19,17 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +/** + * An implementation of {@link RefCounter} that uses an array of {@link AtomicInteger}s to track the + * number of references to a resource. Offers better concurrency performance than {@link + * SimpleRefCounter} which used a single {@link AtomicInteger}, at the cost of more memory due to + * the additional {@link AtomicInteger}s. + * + *

        Each thread will use the {@link AtomicInteger} at an array offset determined by a hash of the + * thread's id. + * + *

        The number of stripes configurable but immutable once set. + */ class StripedRefCounter implements RefCounter { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index d4eafe5c..cbcce9be 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -18,8 +18,11 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +/** + * An implementation of {@link RefCounter} that uses synchronisation to track the number of + * references to a resource. + */ class SynchronisedRefCounter implements RefCounter { - private static final int CLOSED_VALUE = Integer.MIN_VALUE; private boolean isClosed = false; private int counter = 0; diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index c6a35274..797e47ce 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -93,8 +93,9 @@ public void abort() { /** * Closes this transaction. Any uncommitted work will be aborted first. * - *

        If any {@link Cursor}s have been opened on this transaction, they MUST be closed - * first, else you will not be able to close the cursor after its transaction has been closed. + *

        If any {@link Cursor}s have been opened on this transaction, they MUST be + * closed first, else you will not be able to close the cursor after its transaction has been + * closed. * *

        Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for * each read-only buffer (ie the key and value). @@ -118,8 +119,10 @@ public void close() { /** * Commits this transaction. - *

        If you have an open cursor using this transaction, you must close the cursor before committing. - * */ + * + *

        If you have an open cursor using this transaction, you must close the cursor before + * committing. + */ public void commit() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -196,6 +199,7 @@ public void renew() { /** * Aborts this read-only transaction and resets the transaction handle, so it can be reused upon * calling {@link #renew()}. + * *

        Not applicable to write transactions. */ public void reset() { @@ -318,8 +322,8 @@ public static final class NotReadyException extends LmdbException { public NotReadyException() { super( "Transaction is not in ready state, i.e. it has been closed/committed/aborted/reset. " - + "You may see this if have you tried to close a cursor after committing the transaction, " + - "or if you have tried to use a cursor after closing its transaction."); + + "You may see this if have you tried to close a cursor after committing the transaction, " + + "or if you have tried to use a cursor after closing its transaction."); } } diff --git a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java index d5853f8c..575ce346 100644 --- a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java +++ b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java @@ -62,7 +62,8 @@ void buffersMustBeDirect() { () -> { try (final TempDir tempDir = new TempDir()) { final Path dir = tempDir.createTempDir(); - try (Env env = create().setSafeClose().setSafeClose().setMaxReaders(1).open(dir)) { + try (Env env = + create().setSafeClose().setSafeClose().setMaxReaders(1).open(dir)) { final Dbi db = env.createDbi() .setDbName(DB_1) diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 6ab58a81..d37234c8 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -75,7 +75,6 @@ void afterEach() { tempDir.cleanup(); } - @Test void closedCursorRejectsSubsequentGets() { assertThatThrownBy( diff --git a/src/test/java/org/lmdbjava/EnvDeprecatedTest.java b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java index 2db29c9f..9e959ee1 100644 --- a/src/test/java/org/lmdbjava/EnvDeprecatedTest.java +++ b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java @@ -188,7 +188,8 @@ void copyDirectoryRejectsFileDestination() { final Path dest = tempDir.createTempDir(); final Path src = tempDir.createTempDir(); FileUtil.deleteDir(dest); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } }) @@ -203,7 +204,8 @@ void copyDirectoryRejectsMissingDestination() { () -> { try { Files.delete(dest); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } } catch (final IOException e) { @@ -223,7 +225,8 @@ void copyDirectoryRejectsNonEmptyDestination() { final Path subDir = dest.resolve("hello"); Files.createDirectory(subDir); assertThat(Files.isDirectory(subDir)).isTrue(); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).open(src.toFile())) { env.copy(dest.toFile(), MDB_CP_COMPACT); } } catch (final IOException e) { @@ -238,7 +241,8 @@ void copyFileBased() { final Path dest = tempDir.createTempFile(); final Path src = tempDir.createTempFile(); assertThat(Files.exists(dest)).isFalse(); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { env.copy(dest.toFile(), MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); @@ -311,7 +315,8 @@ void readOnlySupported() { final Dbi rwDb = rwEnv.openDbi(DB_1, MDB_CREATE); rwDb.put(bb(1), bb(42)); } - try (Env roEnv = Env.create().setSafeClose().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { + try (Env roEnv = + Env.create().setSafeClose().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { final Dbi roDb = roEnv.openDbi(DB_1); try (Txn roTxn = roEnv.txnRead()) { assertThat(roDb.get(roTxn, bb(1))).isNotNull(); diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index ca9031ea..bc692d0f 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -49,9 +49,7 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** - * Test {@link Env}. - */ +/** Test {@link Env}. */ public final class EnvTest { private TempDir tempDir; @@ -70,12 +68,12 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } @@ -89,49 +87,29 @@ void cannotChangeBuilderAfterOpen() { try (Env ignored = builder.open(file)) { // Now try to modify the builder after it has been used to open an Env - assertThatThrownBy( - () -> builder.setMapSize(1)) + assertThatThrownBy(() -> builder.setMapSize(1)).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(builder::setSafeClose).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.setSafeClose(true)).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.setEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR))) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - builder::setSafeClose) + assertThatThrownBy(() -> builder.setMaxReaders(1)).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.setFilePermissions(0666)) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setSafeClose(true)) + assertThatThrownBy(() -> builder.setMaxDbs(1)).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.setMapSize(1, ByteUnit.MEBIBYTES)) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR))) + assertThatThrownBy(() -> builder.addEnvFlag(MDB_NOSYNC)) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setMaxReaders(1)) + assertThatThrownBy(() -> builder.addEnvFlags(EnvFlagSet.of(MDB_NOSYNC))) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setFilePermissions(0666)) + assertThatThrownBy(() -> builder.addEnvFlags(Collections.singleton(MDB_NOSYNC))) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setMaxDbs(1)) + assertThatThrownBy(() -> builder.setEnvFlags(MDB_NOSYNC)) .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setMapSize(1, ByteUnit.MEBIBYTES)) - .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.addEnvFlag(MDB_NOSYNC)) - .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.addEnvFlags(EnvFlagSet.of(MDB_NOSYNC))) - .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.addEnvFlags(Collections.singleton(MDB_NOSYNC))) - .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setEnvFlags(MDB_NOSYNC)) - .isInstanceOf(AlreadyOpenException.class); - assertThatThrownBy( - () -> builder.setEnvFlags(Collections.singleton(MDB_NOSYNC))) + assertThatThrownBy(() -> builder.setEnvFlags(Collections.singleton(MDB_NOSYNC))) .isInstanceOf(AlreadyOpenException.class); //noinspection resource - assertThatThrownBy( - () -> builder.open(file)) - .isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.open(file)).isInstanceOf(AlreadyOpenException.class); } } @@ -141,40 +119,39 @@ void cannotInfoOnceClosed() { final Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); env.close(); - assertThatThrownBy(env::info) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(env::info).isInstanceOf(AlreadyClosedException.class); } @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @@ -184,8 +161,7 @@ void cannotStatOnceClosed() { final Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); env.close(); - assertThatThrownBy(env::stat) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(env::stat).isInstanceOf(AlreadyClosedException.class); } @Test @@ -194,9 +170,7 @@ void cannotSyncOnceClosed() { final Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); env.close(); - assertThatThrownBy( - () -> env.sync(false)) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(() -> env.sync(false)).isInstanceOf(AlreadyClosedException.class); } @Test @@ -205,8 +179,7 @@ void cannotOpenReadTxnOnceClosed() { final Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); env.close(); - assertThatThrownBy(env::txnRead) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(env::txnRead).isInstanceOf(AlreadyClosedException.class); } @Test @@ -215,8 +188,7 @@ void cannotOpenWriteTxnOnceClosed() { final Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); env.close(); - assertThatThrownBy(env::txnWrite) - .isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(env::txnWrite).isInstanceOf(AlreadyClosedException.class); } @Test @@ -251,8 +223,7 @@ void copyDirectoryRejectsFileDestination() { FileUtil.deleteDir(dest); final Path src = tempDir.createTempDir(); try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { - assertThatThrownBy( - () -> env.copy(dest, MDB_CP_COMPACT)) + assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } } @@ -264,8 +235,7 @@ void copyDirectoryRejectsMissingDestination() { Files.delete(dest); final Path src = tempDir.createTempDir(); try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { - assertThatThrownBy( - () -> env.copy(dest, MDB_CP_COMPACT)) + assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } } catch (final IOException e) { @@ -281,8 +251,7 @@ void copyDirectoryRejectsNonEmptyDestination() throws IOException { assertThat(Files.isDirectory(subDir)).isTrue(); final Path src = tempDir.createTempDir(); try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { - assertThatThrownBy( - () -> env.copy(dest, MDB_CP_COMPACT)) + assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } } @@ -292,7 +261,8 @@ void copyFileBased() { final Path dest = tempDir.createTempFile(); assertThat(Files.exists(dest)).isFalse(); final Path src = tempDir.createTempFile(); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { env.copy(dest, MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); @@ -305,9 +275,8 @@ void copyFileRejectsExistingDestination() throws IOException { assertThat(Files.exists(dest)).isTrue(); final Path src = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { - assertThatThrownBy( - () -> env.copy(dest, MDB_CP_COMPACT)) + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } } @@ -327,13 +296,13 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -343,11 +312,10 @@ void createAsFile() { void detectTransactionThreadViolation() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { + Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { try (Txn ignored = env.txnRead()) { // When NOT using MDB_NOTLS flag, you cannot open a second read txn on the same thread - assertThatThrownBy(env::txnRead) - .isInstanceOf(BadReaderLockException.class); + assertThatThrownBy(env::txnRead).isInstanceOf(BadReaderLockException.class); } } } @@ -356,12 +324,15 @@ void detectTransactionThreadViolation() { void multipleReadTxnsOnSameThread() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS).open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(3) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .open(file)) { try (Txn ignored1 = env.txnRead()) { // MDB_NOTLS flag allows us to open multiple read txns on the same thread //noinspection EmptyTryBlock - try (Txn ignored2 = env.txnRead()) { - } + try (Txn ignored2 = env.txnRead()) {} } } } @@ -370,13 +341,13 @@ void multipleReadTxnsOnSameThread() { void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -398,29 +369,25 @@ void mapFull() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); assertThatThrownBy( - () -> { - // Fill the env until MapFullException is thrown - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - }) + () -> { + // Fill the env until MapFullException is thrown + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + }) .isInstanceOf(MapFullException.class); } } @@ -434,7 +401,7 @@ void readOnlySupported() { rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -456,7 +423,12 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -475,13 +447,10 @@ void setMapSize() { } assertThat(mapFullExThrown).isTrue(); - assertThatThrownBy( - () -> env.setMapSize(-1, ByteUnit.KIBIBYTES)) + assertThatThrownBy(() -> env.setMapSize(-1, ByteUnit.KIBIBYTES)) .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy( - () -> env.setMapSize(-1)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> env.setMapSize(-1)).isInstanceOf(IllegalArgumentException.class); env.setMapSize(1024, ByteUnit.KIBIBYTES); @@ -510,7 +479,8 @@ void setMapSize() { @Test void stats() { final Path file = tempDir.createTempFile(); - try (Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { final Stat stat = env.stat(); assertThat(stat).isNotNull(); assertThat(stat.branchPages).isEqualTo(0L); @@ -526,7 +496,8 @@ void stats() { @Test void testDefaultOpen() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -538,7 +509,8 @@ void testDefaultOpen() { @Test void testDefaultOpenNoName1() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -566,7 +538,8 @@ void testDefaultOpenNoName1() { @Test void testDefaultOpenNoName2() { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + try (Env env = + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -590,14 +563,14 @@ void testDefaultOpenNoName2() { void addEnvFlag() { final Path file = tempDir.createTempFile(); try (final Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -609,17 +582,17 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -631,14 +604,14 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -651,18 +624,18 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -674,14 +647,14 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -696,15 +669,14 @@ void setEnvFlags_null1() { () -> { //noinspection EmptyTryBlock try (final Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class) .hasMessageContaining("No such file or directory"); @@ -718,15 +690,14 @@ void setEnvFlags_null2() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -739,15 +710,14 @@ void setEnvFlags_null3() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -755,14 +725,15 @@ void setEnvFlags_null3() { @Test void closeWithOpenReadTxn() { final Path file = tempDir.createTempFile(); - final Env env = Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() - .open(file); + final Env env = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose() + .open(file); // Open but don't close final Txn readTxn = env.txnWrite(); @@ -776,7 +747,8 @@ void closeWithOpenReadTxn() { @Test void closeWithOpenWriteTxn() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") final Env env = + @SuppressWarnings("resource") + final Env env = Env.create() .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) @@ -798,7 +770,7 @@ void closeWithOpenWriteTxn() { @Test void closeWithOpenCursor() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") final Env env = + final Env env = Env.create() .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) @@ -808,11 +780,8 @@ void closeWithOpenCursor() { .setSafeClose() .open(file); - final Dbi dbi = env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); + final Dbi dbi = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); // Open but don't close final Txn writeTxn = env.txnWrite(); @@ -820,11 +789,9 @@ void closeWithOpenCursor() { // Close the txn but not the cursor writeTxn.close(); - Assertions.assertThatThrownBy(env::close) - .isInstanceOf(Env.EnvInUseException.class); + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); - Assertions.assertThatThrownBy(cursor::close) - .isInstanceOf(Txn.NotReadyException.class); + Assertions.assertThatThrownBy(cursor::close).isInstanceOf(Txn.NotReadyException.class); // can't close the env as we are unable to close the cursor } diff --git a/src/test/java/org/lmdbjava/GarbageCollectionTest.java b/src/test/java/org/lmdbjava/GarbageCollectionTest.java index e587a422..ffd8f56d 100644 --- a/src/test/java/org/lmdbjava/GarbageCollectionTest.java +++ b/src/test/java/org/lmdbjava/GarbageCollectionTest.java @@ -36,7 +36,8 @@ class GarbageCollectionTest { void buffersNotGarbageCollectedTest() { try (final TempDir tempDir = new TempDir()) { final Path dir = tempDir.createTempDir(); - try (Env env = Env.create().setSafeClose().setMapSize(2_085_760_999).setMaxDbs(1).open(dir)) { + try (Env env = + Env.create().setSafeClose().setMapSize(2_085_760_999).setMaxDbs(1).open(dir)) { final Dbi db = env.createDbi() .setDbName(DB_NAME) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index b06eb365..22a36623 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -47,7 +47,29 @@ public class RefCounterTest { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); private final int iterations = 20_000_000; private final int threadCount = PROCESSOR_COUNT; - private volatile Object env = new Object(); + + /** + * @return A {@link Stream} of all {@link RefCounter}s for {@link ParameterizedTest}s. + */ + private static Stream allRefCounterProvider() { + return Stream.concat( + multiThreadedRefCounterProvider(), + Stream.of(new SingleThreadedRefCounter(), new NoOpRefCounter()) + .map(RefCounterTest::createArguments)); + } + + /** + * @return A {@link Stream} of {@link RefCounter}s that support multithreaded use for {@link + * ParameterizedTest}s. + */ + private static Stream multiThreadedRefCounterProvider() { + return Stream.of(new StripedRefCounter(), new SimpleRefCounter(), new SynchronisedRefCounter()) + .map(RefCounterTest::createArguments); + } + + private static Arguments createArguments(final RefCounter refCounter) { + return Arguments.argumentSet(refCounter.getClass().getSimpleName(), refCounter); + } @Disabled // Manual performance test @Test @@ -125,41 +147,6 @@ public void perfTest() { } } - /** - * @return A {@link Stream} of all {@link RefCounter}s - */ - static Stream allRefCounterProvider() { - return Stream.of( - new StripedRefCounter(), - new SingleThreadedRefCounter(), - new SimpleRefCounter(), - new SynchronisedRefCounter(), - new NoOpRefCounter()) - .map(refCounter -> Arguments.argumentSet( - refCounter.getClass().getSimpleName(), - refCounter)); - } - - /** - * @return A {@link Stream} of {@link RefCounter}s that support multi-threaded use - */ - static Stream multiThreadedRefCounterProvider() { - return Stream.of( - new StripedRefCounter(), - new SimpleRefCounter(), - new SynchronisedRefCounter()) - .map(refCounter -> Arguments.argumentSet( - refCounter.getClass().getSimpleName(), - refCounter)); - } - - private void assertRefCount(final RefCounter refCounter, final int expectedCount) { - // NoOpRefCounter does no reference counting, so we can't assert the count - if (!(refCounter instanceof NoOpRefCounter)) { - assertThat(refCounter.getCount()).isEqualTo(expectedCount); - } - } - @ParameterizedTest @MethodSource("allRefCounterProvider") void testRefCounters(final RefCounter refCounter) { @@ -172,7 +159,7 @@ void testRefCounters(final RefCounter refCounter) { final AtomicInteger onCloseCallCount = new AtomicInteger(); if (!(refCounter instanceof NoOpRefCounter)) { - // Close not called as 2 are un-released + // Close() not called as ref count is two. Assertions.assertThatThrownBy( () -> { refCounter.close(onCloseCallCount::incrementAndGet); @@ -187,7 +174,7 @@ void testRefCounters(final RefCounter refCounter) { assertRefCount(refCounter, 1); if (!(refCounter instanceof NoOpRefCounter)) { - // Close not called as 1 un-released + // Close() not called as ref count is one. Assertions.assertThatThrownBy( () -> { refCounter.close(onCloseCallCount::incrementAndGet); @@ -229,21 +216,22 @@ void multipleThreads(final RefCounter refCounter) { final CountDownLatch countDownLatch = new CountDownLatch(threadCount); try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { - final CompletableFuture[] futures = IntStream.range(0, threadCount) - .boxed() - .map( - i -> - CompletableFuture.runAsync( - () -> { - TestUtils.countDownThenAwait(countDownLatch); - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - callCounts[i].getAndIncrement(); - releaser.release(); - } - }, - executorService)) - .toArray(CompletableFuture[]::new); + final CompletableFuture[] futures = + IntStream.range(0, threadCount) + .boxed() + .map( + i -> + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(countDownLatch); + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + callCounts[i].getAndIncrement(); + releaser.release(); + } + }, + executorService)) + .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures).join(); @@ -281,8 +269,7 @@ void multipleThreads_delayedRelease(final RefCounter refCounter) { () -> { TestUtils.countDownThenAwait(countDownLatch); for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = - refCounter.acquire(); + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); releasers.add(releaser); callCounts[i].getAndIncrement(); futures.add( @@ -322,24 +309,21 @@ void testImmediateClose(final RefCounter refCounter) { assertThat(onCloseCallCount).hasValue(1); assertThat(refCounter.isClosed()).isEqualTo(true); - assertThatThrownBy(refCounter::checkNotClosed) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(refCounter::checkNotClosed).isInstanceOf(Env.AlreadyClosedException.class); // Check again as idempotent refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount).hasValue(1); assertThat(refCounter.isClosed()).isEqualTo(true); - assertThatThrownBy(refCounter::checkNotClosed) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(refCounter::checkNotClosed).isInstanceOf(Env.AlreadyClosedException.class); } - /** * Lots of threads all doing acquire/release in a loop, then the main thread tries to call - * refCounter.close(...), which will throw an {@link org.lmdbjava.Env.EnvInUseException}. Main thread then - * makes all worker threads stop their looping and calls refCounter.close(...) again, successfully - * this time. + * refCounter.close(...), which will throw an {@link org.lmdbjava.Env.EnvInUseException}. The main + * thread then makes all worker threads stop their looping and calls refCounter.close(...) again, + * successfully this time. */ @ParameterizedTest @MethodSource("multiThreadedRefCounterProvider") @@ -378,7 +362,12 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { for (int j = 0; j < iterations; j++) { if (abortThreads.get()) { System.out.println( - Thread.currentThread() + ", round: " + round + ", j: " + j + ", abortThreads is true"); + Thread.currentThread() + + ", round: " + + round + + ", j: " + + j + + ", abortThreads is true"); break; } @@ -388,7 +377,12 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { counts[threadIdx].incrementAndGet(); } catch (Env.AlreadyClosedException e) { System.out.println( - Thread.currentThread() + ", round: " + round + ", j: " + j + ", Env closed, aborting"); + Thread.currentThread() + + ", round: " + + round + + ", j: " + + j + + ", Env closed, aborting"); break; } try { @@ -396,7 +390,8 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { TestUtils.sleep(random.nextInt(5)); // env is null after closure assertThat(mockEnv.get()).isNotNull(); - // Objects.requireNonNull(mockEnv.get(), "Attempt to use a null env"); + // Objects.requireNonNull(mockEnv.get(), "Attempt to + // use a null env"); } finally { releaser.release(); } @@ -562,11 +557,11 @@ void failedOnCloseDoesNotCloseOrCorruptCounter() { final StripedRefCounter refCounter = new StripedRefCounter(); assertThatThrownBy( - () -> - refCounter.close( - () -> { - throw new RuntimeException("boom"); - })) + () -> + refCounter.close( + () -> { + throw new RuntimeException("boom"); + })) .isInstanceOf(RuntimeException.class); assertThat(refCounter.isClosed()).isFalse(); @@ -614,8 +609,6 @@ public void noOpRefCounter() { } private void doNoOpRefCounter() { - // System.out.println("Running test for " + stripes + " stripes"); - final AtomicReference startTime = new AtomicReference<>(null); final CompletableFuture[] futures = new CompletableFuture[threadCount]; final NoOpRefCounter refCounter = new NoOpRefCounter(); @@ -640,15 +633,10 @@ private void doNoOpRefCounter() { }); for (int j = 0; j < iterationsPerThread; j++) { + // Just acquire then release final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - try { - // Make sure we have an env that is not 'closed' - Objects.requireNonNull(env); - } finally { - releaser.release(); - } + releaser.release(); } - // System.out.println(Thread.currentThread() + " - Done"); }, executorService); } @@ -735,4 +723,10 @@ private static RefCounter createNewRefCounter(RefCounter refCounter) { } } + private void assertRefCount(final RefCounter refCounter, final int expectedCount) { + // NoOpRefCounter does no reference counting, so we can't assert the count + if (!(refCounter instanceof NoOpRefCounter)) { + assertThat(refCounter.getCount()).isEqualTo(expectedCount); + } + } } diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index 9b2bfcb5..da7908f0 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index 611ff81e..ac853de7 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -437,7 +437,11 @@ void tutorial7() { // There's also a PROXY_SAFE if you want to stop ByteBuffer's Unsafe use. // Aside from that and a different type argument, it's the same as usual... final Env env = - Env.create(PROXY_DB).setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setSafeClose().open(dir); + Env.create(PROXY_DB) + .setMapSize(10, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setSafeClose() + .open(dir); final Dbi db = env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -618,6 +622,11 @@ void tutorial9() { // or reverse ordered keys, using Env.DISABLE_CHECKS_PROP etc), but you now // know enough to tackle the JavaDocs with confidence. Have fun! private Env createSimpleEnv(final Path path) { - return Env.create().setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setMaxReaders(1).setSafeClose().open(path); + return Env.create() + .setMapSize(10, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setSafeClose() + .open(path); } } diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 7f45eab8..9170c255 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -135,7 +135,11 @@ void rangeSearch() { void readOnlyTxnAllowedInReadOnlyEnv() { env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = - create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create() + .setSafeClose() + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV) + .open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); } @@ -153,7 +157,11 @@ void readWriteTxnDeniedInReadOnlyEnv() { .open(); env.close(); try (Env roEnv = - create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + create() + .setSafeClose() + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV) + .open(file)) { roEnv.txnWrite(); // error } }) From 344d7983b1afcd382a632a2bd99cb3ab1ca6ec8f Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:31:18 +0100 Subject: [PATCH 35/61] gh-279 Fix compilation failures under java 8 --- src/test/java/org/lmdbjava/EnvTest.java | 4 +- .../java/org/lmdbjava/RefCounterTest.java | 314 +++++++++--------- 2 files changed, 158 insertions(+), 160 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index bc692d0f..9ce375e3 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -772,12 +772,12 @@ void closeWithOpenCursor() { final Path file = tempDir.createTempFile(); final Env env = Env.create() - .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxDbs(1) .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() + .setSafeClose(true) + .setSingleThreaded(true) .open(file); final Dbi dbi = diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 22a36623..9e37ea64 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -21,7 +21,6 @@ import java.text.NumberFormat; import java.time.Duration; import java.time.Instant; -import java.util.Arrays; import java.util.Objects; import java.util.Queue; import java.util.Random; @@ -53,9 +52,9 @@ public class RefCounterTest { */ private static Stream allRefCounterProvider() { return Stream.concat( - multiThreadedRefCounterProvider(), - Stream.of(new SingleThreadedRefCounter(), new NoOpRefCounter()) - .map(RefCounterTest::createArguments)); + multiThreadedRefCounterProvider(), + Stream.of(new SingleThreadedRefCounter(), new NoOpRefCounter()) + .map(RefCounterTest::createArguments)); } /** @@ -214,7 +213,9 @@ void multipleThreads(final RefCounter refCounter) { callCounts[i] = new AtomicInteger(); } final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + //noinspection resource ExecutorService does not implement AutoCloseable in Java8 + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + try { final CompletableFuture[] futures = IntStream.range(0, threadCount) @@ -240,6 +241,9 @@ void multipleThreads(final RefCounter refCounter) { for (AtomicInteger callCount : callCounts) { assertThat(callCount).hasValue(iterations); } + } finally { + // ExecutorService does not implement AutoCloseable in Java8 + executorService.shutdown(); } } @@ -250,42 +254,47 @@ void multipleThreads_delayedRelease(final RefCounter refCounter) { final AtomicInteger[] callCounts; final Queue releasers; - try (ExecutorService executor = Executors.newFixedThreadPool(threadCount)) { - try (ExecutorService executor2 = Executors.newFixedThreadPool(threadCount)) { - callCounts = new AtomicInteger[threadCount]; - for (int i = 0; i < threadCount; i++) { - callCounts[i] = new AtomicInteger(); - } - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - - releasers = new ConcurrentLinkedQueue<>(); - final Queue> futures = new ConcurrentLinkedQueue<>(); - - IntStream.range(0, threadCount) - .boxed() - .map( - i -> - CompletableFuture.runAsync( - () -> { - TestUtils.countDownThenAwait(countDownLatch); - for (int j = 0; j < iterations; j++) { - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - releasers.add(releaser); - callCounts[i].getAndIncrement(); - futures.add( - CompletableFuture.runAsync( - () -> { - final long count = refCounter.getCount(); - assertThat(count).isNotEqualTo(0); - }, - executor2)); - } - }, - executor)) - .forEach(futures::add); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + //noinspection resource ExecutorService does not implement AutoCloseable in Java8 + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final ExecutorService executorService2 = Executors.newFixedThreadPool(threadCount); + + try { + callCounts = new AtomicInteger[threadCount]; + for (int i = 0; i < threadCount; i++) { + callCounts[i] = new AtomicInteger(); } + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + + releasers = new ConcurrentLinkedQueue<>(); + final Queue> futures = new ConcurrentLinkedQueue<>(); + + IntStream.range(0, threadCount) + .boxed() + .map( + i -> + CompletableFuture.runAsync( + () -> { + TestUtils.countDownThenAwait(countDownLatch); + for (int j = 0; j < iterations; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + releasers.add(releaser); + callCounts[i].getAndIncrement(); + futures.add( + CompletableFuture.runAsync( + () -> { + final long count = refCounter.getCount(); + assertThat(count).isNotEqualTo(0); + }, + executorService2)); + } + }, + executorService)) + .forEach(futures::add); + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } finally { + executorService2.shutdown(); + executorService.shutdown(); } assertRefCount(refCounter, threadCount * iterations); @@ -330,14 +339,15 @@ void testImmediateClose(final RefCounter refCounter) { void testBehaviour(final RefCounter refCounter) throws InterruptedException { final Random random = new Random(); final int threadCount = this.threadCount - 1; - try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + //noinspection resource ExecutorService does not implement AutoCloseable in Java8 + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + try { final int rounds = 5; final int iterations = 10_000_000; final AtomicReference mockEnv = new AtomicReference<>(); for (int k = 0; k < rounds; k++) { final int round = k; - System.out.printf("Round %s ----------------------------------------%n", round); // Reset the env mockEnv.set(new Object()); @@ -358,16 +368,8 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { () -> { // Wait for all threads to be ready TestUtils.countDownThenAwait(startLatch); - // System.out.println(Thread.currentThread() + " - Starting"); for (int j = 0; j < iterations; j++) { if (abortThreads.get()) { - System.out.println( - Thread.currentThread() - + ", round: " - + round - + ", j: " - + j - + ", abortThreads is true"); break; } @@ -376,13 +378,6 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { releaser = roundRefCounter.acquire(); counts[threadIdx].incrementAndGet(); } catch (Env.AlreadyClosedException e) { - System.out.println( - Thread.currentThread() - + ", round: " - + round - + ", j: " - + j - + ", Env closed, aborting"); break; } try { @@ -396,7 +391,6 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { releaser.release(); } } - // System.out.println(Thread.currentThread() + " - Done"); }, executorService); } @@ -412,11 +406,9 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { while (!didClose.get()) { try { assertThat(mockEnv.get()).isNotNull(); - System.out.println("close called " + ++closeCallCount); roundRefCounter.close( () -> { onCloseCallCount.incrementAndGet(); - System.out.println("onClose called " + onCloseCallCount.get()); // Imitate closing the env mockEnv.set(null); didClose.set(true); @@ -438,9 +430,6 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { // Wait for all workers to finish CompletableFuture.allOf(futures).join(); - System.out.println( - "Acquire call count: " + Arrays.stream(counts).mapToLong(AtomicLong::get).sum()); - // Make sure the mock env is all closed down assertThat(mockEnv).hasNullValue(); assertThat(roundRefCounter.isClosed()).isEqualTo(true); @@ -448,6 +437,9 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { assertThatThrownBy(roundRefCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); assertThat(onCloseCallCount).hasValue(1); } + } finally { + // ExecutorService does not implement AutoCloseable in Java8 + executorService.shutdown(); } } @@ -459,16 +451,15 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { void testGetCount(final RefCounter refCounter) throws InterruptedException { final Random random = new Random(); final int threadCount = this.threadCount - 1; - try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) { + //noinspection resource ExecutorService does not implement AutoCloseable in Java8 + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + try { final int rounds = 5; final int iterations = 10_000_000; final AtomicReference mockEnv = new AtomicReference<>(); final AtomicBoolean abortThreads = new AtomicBoolean(false); for (int k = 0; k < rounds; k++) { - // final int round = k; - System.out.printf("Round %s ----------------------------------------%n", k); - // Reset the env mockEnv.set(new Object()); abortThreads.set(false); @@ -484,7 +475,6 @@ void testGetCount(final RefCounter refCounter) throws InterruptedException { () -> { // Wait for all threads to be ready TestUtils.countDownThenAwait(startLatch); - // System.out.println(Thread.currentThread() + " - Starting"); for (int j = 0; j < iterations; j++) { if (abortThreads.get()) { @@ -495,8 +485,6 @@ void testGetCount(final RefCounter refCounter) throws InterruptedException { releaser = roundRefCounter.acquire(); counts[threadIdx]++; } catch (Env.AlreadyClosedException e) { - // System.out.println(Thread.currentThread() + ", round: " + - // round + ", Env closed, aborting"); break; } try { @@ -511,7 +499,6 @@ void testGetCount(final RefCounter refCounter) throws InterruptedException { // is not using the 'env' TestUtils.sleep(5 + random.nextInt(5)); } - // System.out.println(Thread.currentThread() + " - Done"); }, executorService); } @@ -524,7 +511,8 @@ void testGetCount(final RefCounter refCounter) throws InterruptedException { for (int i = 0; i < 10; i++) { try { - System.out.println("count: " + roundRefCounter.getCount()); + // Makes sure we can acquire the ref counter count + roundRefCounter.getCount(); } catch (Env.EnvInUseException e) { TestUtils.sleep(100 + random.nextInt(200)); } @@ -533,12 +521,13 @@ void testGetCount(final RefCounter refCounter) throws InterruptedException { // Wait for all workers to finish CompletableFuture.allOf(futures).join(); - System.out.println("Acquire call count: " + Arrays.stream(counts).sum()); - if (roundRefCounter.getCount() != 0) { throw new IllegalStateException("Ref count is " + roundRefCounter.getCount()); } } + } finally { + // ExecutorService does not implement AutoCloseable in Java8 + executorService.shutdown(); } } @@ -614,48 +603,52 @@ private void doNoOpRefCounter() { final NoOpRefCounter refCounter = new NoOpRefCounter(); final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int iterationsPerThread = iterations / threadCount; - for (int i = 0; i < threadCount; i++) { - futures[i] = - CompletableFuture.runAsync( - () -> { - // Wait for all threads to be ready - TestUtils.countDownThenAwait(startLatch); - - // Capture the start time - startTime.updateAndGet( - currVal -> { - if (currVal == null) { - return Instant.now(); - } else { - return currVal; - } - }); - - for (int j = 0; j < iterationsPerThread; j++) { - // Just acquire then release - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - releaser.release(); - } - }, - executorService); + try { + final int iterationsPerThread = iterations / threadCount; + for (int i = 0; i < threadCount; i++) { + futures[i] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + TestUtils.countDownThenAwait(startLatch); + + // Capture the start time + startTime.updateAndGet( + currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterationsPerThread; j++) { + // Just acquire then release + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + releaser.release(); + } + }, + executorService); + } + CompletableFuture.allOf(futures).join(); + + final Duration duration = Duration.between(startTime.get(), Instant.now()); + final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); + +// System.out.println( +// "All Finished" +// + ", threads: " +// + threadCount +// + ", iterationsPerThread: " +// + iterationsPerThread +// + ", duration: " +// + duration +// + ", iterationsPerSec: " +// + NumberFormat.getInstance().format(iterationsPerSec)); + } finally { + // ExecutorService does not implement AutoCloseable in Java8 + executorService.shutdown(); } - CompletableFuture.allOf(futures).join(); - - final Duration duration = Duration.between(startTime.get(), Instant.now()); - final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - - System.out.println( - "All Finished" - + ", threads: " - + threadCount - + ", iterationsPerThread: " - + iterationsPerThread - + ", duration: " - + duration - + ", iterationsPerSec: " - + NumberFormat.getInstance().format(iterationsPerSec)); - executorService.close(); } private void runPerfTest(int stripes, final RefCounter refCounter) { @@ -667,51 +660,56 @@ private void runPerfTest(int stripes, final int threadCount, final RefCounter re final CompletableFuture[] futures = new CompletableFuture[threadCount]; final CountDownLatch startLatch = new CountDownLatch(threadCount); final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final int iterationsPerThread = iterations / threadCount; - for (int i = 0; i < threadCount; i++) { - futures[i] = - CompletableFuture.runAsync( - () -> { - // Wait for all threads to be ready - TestUtils.countDownThenAwait(startLatch); - // Capture the start time - startTime.updateAndGet( - currVal -> { - if (currVal == null) { - return Instant.now(); - } else { - return currVal; - } - }); - - for (int j = 0; j < iterationsPerThread; j++) { - final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); - releaser.release(); - } - }, - executorService); - } - CompletableFuture.allOf(futures).join(); + try { + final int iterationsPerThread = iterations / threadCount; + for (int i = 0; i < threadCount; i++) { + futures[i] = + CompletableFuture.runAsync( + () -> { + // Wait for all threads to be ready + TestUtils.countDownThenAwait(startLatch); + // Capture the start time + startTime.updateAndGet( + currVal -> { + if (currVal == null) { + return Instant.now(); + } else { + return currVal; + } + }); + + for (int j = 0; j < iterationsPerThread; j++) { + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + releaser.release(); + } + }, + executorService); + } + CompletableFuture.allOf(futures).join(); - if (refCounter.getCount() != 0) { - throw new IllegalStateException("Ref count is " + refCounter.getCount()); - } + if (refCounter.getCount() != 0) { + throw new IllegalStateException("Ref count is " + refCounter.getCount()); + } + + final Duration duration = Duration.between(startTime.get(), Instant.now()); + final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - final Duration duration = Duration.between(startTime.get(), Instant.now()); - final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - - System.out.println( - "All Finished" - + ", stripes: " - + stripes - + ", threads: " - + threadCount - + ", iterationsPerThread: " - + iterationsPerThread - + ", duration: " - + duration - + ", iterationsPerSec: " - + NumberFormat.getInstance().format(iterationsPerSec)); + System.out.println( + "All Finished" + + ", stripes: " + + stripes + + ", threads: " + + threadCount + + ", iterationsPerThread: " + + iterationsPerThread + + ", duration: " + + duration + + ", iterationsPerSec: " + + NumberFormat.getInstance().format(iterationsPerSec)); + } finally { + // ExecutorService does not implement AutoCloseable in Java8 + executorService.shutdown(); + } } private static RefCounter createNewRefCounter(RefCounter refCounter) { From 0831fc9da29b8eaafceb30770fc86d8ad425cff7 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:11:23 +0100 Subject: [PATCH 36/61] gh-279 Add test --- src/test/java/org/lmdbjava/EnvTest.java | 69 ++++++++++++++++++- .../java/org/lmdbjava/RefCounterTest.java | 20 +++--- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 9ce375e3..222dd164 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -25,6 +25,7 @@ import static org.lmdbjava.EnvFlags.MDB_NOSYNC; import static org.lmdbjava.EnvFlags.MDB_NOTLS; import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; +import static org.lmdbjava.PutFlags.MDB_APPENDDUP; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.bb; @@ -42,6 +43,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Env.AlreadyOpenException; import org.lmdbjava.Env.Builder; @@ -90,6 +93,9 @@ void cannotChangeBuilderAfterOpen() { assertThatThrownBy(() -> builder.setMapSize(1)).isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(builder::setSafeClose).isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(() -> builder.setSafeClose(true)).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(builder::setSingleThreaded).isInstanceOf(AlreadyOpenException.class); + assertThatThrownBy(() -> builder.setSingleThreaded(true)) + .isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(() -> builder.setEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR))) .isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(() -> builder.setMaxReaders(1)).isInstanceOf(AlreadyOpenException.class); @@ -747,7 +753,6 @@ void closeWithOpenReadTxn() { @Test void closeWithOpenWriteTxn() { final Path file = tempDir.createTempFile(); - @SuppressWarnings("resource") final Env env = Env.create() .setSafeClose() @@ -795,4 +800,66 @@ void closeWithOpenCursor() { // can't close the env as we are unable to close the cursor } + + @ParameterizedTest + @CsvSource({ + "true, true, false", + "true, false, false", + "false, true, false", + "false, false, false", + "false, false, true" + }) + void singleThreaded(final boolean safeClose, final boolean singleThreaded, final boolean noArgs) { + testEnvUse(safeClose, singleThreaded, noArgs); + } + + private void testEnvUse( + final boolean safeClose, final boolean singleThreaded, final boolean noArgs) { + final Path file = tempDir.createTempFile(); + + final Builder builder = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR); + + if (noArgs) { + builder.setSafeClose().setSingleThreaded(); + } else { + builder.setSafeClose(safeClose).setSingleThreaded(singleThreaded); + } + + try (Env env = builder.open(file)) { + final Dbi dbi = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txn = env.txnWrite()) { + for (int i = 0; i < 10; i++) { + dbi.put(txn, bb(i), bb(100 + i), MDB_APPENDDUP); + + if (safeClose) { + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + } + } + txn.commit(); + } + + for (int i = 0; i < 5; i++) { + try (Txn txn = env.txnRead(); + Cursor cursor = dbi.openCursor(txn)) { + int j = 0; + while (cursor.next()) { + final KeyVal keyVal = cursor.keyVal(); + Assertions.assertThat(keyVal.key().getInt()).isEqualTo(j); + Assertions.assertThat(keyVal.val().getInt()).isEqualTo(100 + j); + if (safeClose) { + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + } + j++; + } + } + } + } + } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 9e37ea64..ff1e5fe7 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -635,16 +635,16 @@ private void doNoOpRefCounter() { final Duration duration = Duration.between(startTime.get(), Instant.now()); final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); -// System.out.println( -// "All Finished" -// + ", threads: " -// + threadCount -// + ", iterationsPerThread: " -// + iterationsPerThread -// + ", duration: " -// + duration -// + ", iterationsPerSec: " -// + NumberFormat.getInstance().format(iterationsPerSec)); + // System.out.println( + // "All Finished" + // + ", threads: " + // + threadCount + // + ", iterationsPerThread: " + // + iterationsPerThread + // + ", duration: " + // + duration + // + ", iterationsPerSec: " + // + NumberFormat.getInstance().format(iterationsPerSec)); } finally { // ExecutorService does not implement AutoCloseable in Java8 executorService.shutdown(); From 8c001e602d3917ae987cf6cb00f7d9182c043550 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:20:50 +0100 Subject: [PATCH 37/61] gh-279 Address GH code fix suggestions --- .../java/org/lmdbjava/SimpleRefCounter.java | 1 + .../org/lmdbjava/SynchronisedRefCounter.java | 1 + .../java/org/lmdbjava/CursorIterableTest.java | 2 +- .../java/org/lmdbjava/RefCounterTest.java | 51 +++++++++---------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index e2304a43..21edc484 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -32,6 +32,7 @@ public boolean isClosed() { return counter.get() == CLOSED_VALUE; } + @Override public RefCounterReleaser acquire() { final int newVal = counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal + 1); diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index cbcce9be..420d281c 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -33,6 +33,7 @@ public boolean isClosed() { } } + @Override public RefCounterReleaser acquire() { synchronized (this) { if (isClosed) { diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 1ab5e1d7..0311ecd2 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -129,7 +129,7 @@ private void populateDatabase(final Dbi dbi) { @Test void testPopulate() { - final Dbi db = getDb(); + getDb(); } @Test diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index ff1e5fe7..f2c936ec 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -45,7 +45,7 @@ public class RefCounterTest { private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors(); private final int iterations = 20_000_000; - private final int threadCount = PROCESSOR_COUNT; + private final int processorCount = PROCESSOR_COUNT; /** * @return A {@link Stream} of all {@link RefCounter}s for {@link ParameterizedTest}s. @@ -78,7 +78,7 @@ public void perfTest() { final int round = i; // Run tests with all available processors System.out.println( - "Multi-threaded (" + threadCount + " threads) tests ---------------------------------"); + "Multi-threaded (" + processorCount + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 16, 32, 64, 128, 256) @@ -208,17 +208,17 @@ void testRefCounters(final RefCounter refCounter) { @MethodSource("multiThreadedRefCounterProvider") void multipleThreads(final RefCounter refCounter) { final int iterations = 1000; - final AtomicInteger[] callCounts = new AtomicInteger[threadCount]; - for (int i = 0; i < threadCount; i++) { + final AtomicInteger[] callCounts = new AtomicInteger[processorCount]; + for (int i = 0; i < processorCount; i++) { callCounts[i] = new AtomicInteger(); } - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + final CountDownLatch countDownLatch = new CountDownLatch(processorCount); //noinspection resource ExecutorService does not implement AutoCloseable in Java8 - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final ExecutorService executorService = Executors.newFixedThreadPool(processorCount); try { final CompletableFuture[] futures = - IntStream.range(0, threadCount) + IntStream.range(0, processorCount) .boxed() .map( i -> @@ -255,20 +255,20 @@ void multipleThreads_delayedRelease(final RefCounter refCounter) { final Queue releasers; //noinspection resource ExecutorService does not implement AutoCloseable in Java8 - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final ExecutorService executorService2 = Executors.newFixedThreadPool(threadCount); + final ExecutorService executorService = Executors.newFixedThreadPool(processorCount); + final ExecutorService executorService2 = Executors.newFixedThreadPool(processorCount); try { - callCounts = new AtomicInteger[threadCount]; - for (int i = 0; i < threadCount; i++) { + callCounts = new AtomicInteger[processorCount]; + for (int i = 0; i < processorCount; i++) { callCounts[i] = new AtomicInteger(); } - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + final CountDownLatch countDownLatch = new CountDownLatch(processorCount); releasers = new ConcurrentLinkedQueue<>(); final Queue> futures = new ConcurrentLinkedQueue<>(); - IntStream.range(0, threadCount) + IntStream.range(0, processorCount) .boxed() .map( i -> @@ -297,7 +297,7 @@ void multipleThreads_delayedRelease(final RefCounter refCounter) { executorService.shutdown(); } - assertRefCount(refCounter, threadCount * iterations); + assertRefCount(refCounter, processorCount * iterations); for (AtomicInteger callCount : callCounts) { assertThat(callCount).hasValue(iterations); @@ -338,7 +338,7 @@ void testImmediateClose(final RefCounter refCounter) { @MethodSource("multiThreadedRefCounterProvider") void testBehaviour(final RefCounter refCounter) throws InterruptedException { final Random random = new Random(); - final int threadCount = this.threadCount - 1; + final int threadCount = this.processorCount - 1; //noinspection resource ExecutorService does not implement AutoCloseable in Java8 final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); try { @@ -347,7 +347,6 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { final AtomicReference mockEnv = new AtomicReference<>(); for (int k = 0; k < rounds; k++) { - final int round = k; // Reset the env mockEnv.set(new Object()); @@ -401,7 +400,6 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { // Give the other threads a chance to get underway TestUtils.sleep(200 + random.nextInt(200)); final AtomicBoolean didClose = new AtomicBoolean(false); - int closeCallCount = 0; final AtomicInteger onCloseCallCount = new AtomicInteger(); while (!didClose.get()) { try { @@ -450,7 +448,7 @@ void testBehaviour(final RefCounter refCounter) throws InterruptedException { @MethodSource("multiThreadedRefCounterProvider") void testGetCount(final RefCounter refCounter) throws InterruptedException { final Random random = new Random(); - final int threadCount = this.threadCount - 1; + final int threadCount = this.processorCount - 1; //noinspection resource ExecutorService does not implement AutoCloseable in Java8 final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); try { @@ -599,13 +597,13 @@ public void noOpRefCounter() { private void doNoOpRefCounter() { final AtomicReference startTime = new AtomicReference<>(null); - final CompletableFuture[] futures = new CompletableFuture[threadCount]; + final CompletableFuture[] futures = new CompletableFuture[processorCount]; final NoOpRefCounter refCounter = new NoOpRefCounter(); - final CountDownLatch startLatch = new CountDownLatch(threadCount); - final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final CountDownLatch startLatch = new CountDownLatch(processorCount); + final ExecutorService executorService = Executors.newFixedThreadPool(processorCount); try { - final int iterationsPerThread = iterations / threadCount; - for (int i = 0; i < threadCount; i++) { + final int iterationsPerThread = iterations / processorCount; + for (int i = 0; i < processorCount; i++) { futures[i] = CompletableFuture.runAsync( () -> { @@ -632,9 +630,8 @@ private void doNoOpRefCounter() { } CompletableFuture.allOf(futures).join(); - final Duration duration = Duration.between(startTime.get(), Instant.now()); - final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); - +// final Duration duration = Duration.between(startTime.get(), Instant.now()); +// final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); // System.out.println( // "All Finished" // + ", threads: " @@ -652,7 +649,7 @@ private void doNoOpRefCounter() { } private void runPerfTest(int stripes, final RefCounter refCounter) { - runPerfTest(stripes, threadCount, refCounter); + runPerfTest(stripes, processorCount, refCounter); } private void runPerfTest(int stripes, final int threadCount, final RefCounter refCounter) { From 290522dc5d1579ab0f98303707ce751c71665a42 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:35:52 +0100 Subject: [PATCH 38/61] gh-279 Add coverage --- src/main/java/org/lmdbjava/RefCounter.java | 3 +- .../java/org/lmdbjava/RefCounterTest.java | 74 +++++++++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index a95521fc..38f8415f 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -73,7 +73,8 @@ default void checkNotClosed() { @FunctionalInterface interface RefCounterReleaser { - /** Call this after using the {@link RefCounter} controlled object. */ + /** Call this after using the {@link RefCounter} controlled object. + * Subsequent calls to this method are a no-op.*/ void release(); } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index f2c936ec..3d0ac2c6 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -59,7 +59,7 @@ private static Stream allRefCounterProvider() { /** * @return A {@link Stream} of {@link RefCounter}s that support multithreaded use for {@link - * ParameterizedTest}s. + * ParameterizedTest}s. */ private static Stream multiThreadedRefCounterProvider() { return Stream.of(new StripedRefCounter(), new SimpleRefCounter(), new SynchronisedRefCounter()) @@ -538,17 +538,79 @@ void immediateClose(final RefCounter refCounter) { assertThat(onCloseCallCount.get()).isEqualTo(1); } + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void acquireAfterClose(final RefCounter refCounter) { + final AtomicInteger onCloseCallCount = new AtomicInteger(); + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount.get()).isEqualTo(1); + assertThatThrownBy(refCounter::acquire) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void releaseAfterClose(final RefCounter refCounter) { + final AtomicInteger onCloseCallCount = new AtomicInteger(); + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + // Need to release to allow the close + releaser.release(); + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount.get()).isEqualTo(1); + + assertThatThrownBy(releaser::release) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void use(final RefCounter refCounter) { + final AtomicInteger onCloseCallCount = new AtomicInteger(); + final AtomicInteger useCallCount = new AtomicInteger(); + refCounter.use(() -> { + useCallCount.incrementAndGet(); + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(() -> + refCounter.close(onCloseCallCount::incrementAndGet)) + .isInstanceOf(Env.EnvInUseException.class); + } + }); + + refCounter.use(() -> { + useCallCount.incrementAndGet(); + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(() -> + refCounter.close(onCloseCallCount::incrementAndGet)) + .isInstanceOf(Env.EnvInUseException.class); + } + }); + + assertThat(useCallCount.get()).isEqualTo(2); + assertThat(onCloseCallCount.get()).isEqualTo(0); + + assertThat(refCounter.getCount()).isEqualTo(0); + + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount.get()).isEqualTo(1); + + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(() -> + refCounter.use(useCallCount::incrementAndGet)) + .isInstanceOf(Env.AlreadyClosedException.class); + } + } + @ParameterizedTest @MethodSource("allRefCounterProvider") void failedOnCloseDoesNotCloseOrCorruptCounter() { final StripedRefCounter refCounter = new StripedRefCounter(); assertThatThrownBy( - () -> - refCounter.close( - () -> { - throw new RuntimeException("boom"); - })) + () -> + refCounter.close( + () -> { + throw new RuntimeException("boom"); + })) .isInstanceOf(RuntimeException.class); assertThat(refCounter.isClosed()).isFalse(); From cccf9dd589afccafacd932361a0e8e00aa526ac0 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:41:16 +0100 Subject: [PATCH 39/61] gh-279 Format --- src/main/java/org/lmdbjava/RefCounter.java | 6 +- .../java/org/lmdbjava/RefCounterTest.java | 64 +++++++++---------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index 38f8415f..b1f8cf94 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -73,8 +73,10 @@ default void checkNotClosed() { @FunctionalInterface interface RefCounterReleaser { - /** Call this after using the {@link RefCounter} controlled object. - * Subsequent calls to this method are a no-op.*/ + /** + * Call this after using the {@link RefCounter} controlled object. Subsequent calls to this + * method are a no-op. + */ void release(); } } diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 3d0ac2c6..c35c3a99 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -59,7 +59,7 @@ private static Stream allRefCounterProvider() { /** * @return A {@link Stream} of {@link RefCounter}s that support multithreaded use for {@link - * ParameterizedTest}s. + * ParameterizedTest}s. */ private static Stream multiThreadedRefCounterProvider() { return Stream.of(new StripedRefCounter(), new SimpleRefCounter(), new SynchronisedRefCounter()) @@ -78,7 +78,9 @@ public void perfTest() { final int round = i; // Run tests with all available processors System.out.println( - "Multi-threaded (" + processorCount + " threads) tests ---------------------------------"); + "Multi-threaded (" + + processorCount + + " threads) tests ---------------------------------"); System.out.println("Round: " + round + " " + StripedRefCounter.class.getSimpleName()); IntStream.of(1, 16, 32, 64, 128, 256) @@ -544,8 +546,7 @@ void acquireAfterClose(final RefCounter refCounter) { final AtomicInteger onCloseCallCount = new AtomicInteger(); refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount.get()).isEqualTo(1); - assertThatThrownBy(refCounter::acquire) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); } @ParameterizedTest @@ -558,8 +559,7 @@ void releaseAfterClose(final RefCounter refCounter) { refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount.get()).isEqualTo(1); - assertThatThrownBy(releaser::release) - .isInstanceOf(Env.AlreadyClosedException.class); + assertThatThrownBy(releaser::release).isInstanceOf(Env.AlreadyClosedException.class); } @ParameterizedTest @@ -567,23 +567,23 @@ void releaseAfterClose(final RefCounter refCounter) { void use(final RefCounter refCounter) { final AtomicInteger onCloseCallCount = new AtomicInteger(); final AtomicInteger useCallCount = new AtomicInteger(); - refCounter.use(() -> { - useCallCount.incrementAndGet(); - if (!(refCounter instanceof NoOpRefCounter)) { - assertThatThrownBy(() -> - refCounter.close(onCloseCallCount::incrementAndGet)) - .isInstanceOf(Env.EnvInUseException.class); - } - }); - - refCounter.use(() -> { - useCallCount.incrementAndGet(); - if (!(refCounter instanceof NoOpRefCounter)) { - assertThatThrownBy(() -> - refCounter.close(onCloseCallCount::incrementAndGet)) - .isInstanceOf(Env.EnvInUseException.class); - } - }); + refCounter.use( + () -> { + useCallCount.incrementAndGet(); + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(() -> refCounter.close(onCloseCallCount::incrementAndGet)) + .isInstanceOf(Env.EnvInUseException.class); + } + }); + + refCounter.use( + () -> { + useCallCount.incrementAndGet(); + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(() -> refCounter.close(onCloseCallCount::incrementAndGet)) + .isInstanceOf(Env.EnvInUseException.class); + } + }); assertThat(useCallCount.get()).isEqualTo(2); assertThat(onCloseCallCount.get()).isEqualTo(0); @@ -594,8 +594,7 @@ void use(final RefCounter refCounter) { assertThat(onCloseCallCount.get()).isEqualTo(1); if (!(refCounter instanceof NoOpRefCounter)) { - assertThatThrownBy(() -> - refCounter.use(useCallCount::incrementAndGet)) + assertThatThrownBy(() -> refCounter.use(useCallCount::incrementAndGet)) .isInstanceOf(Env.AlreadyClosedException.class); } } @@ -606,11 +605,11 @@ void failedOnCloseDoesNotCloseOrCorruptCounter() { final StripedRefCounter refCounter = new StripedRefCounter(); assertThatThrownBy( - () -> - refCounter.close( - () -> { - throw new RuntimeException("boom"); - })) + () -> + refCounter.close( + () -> { + throw new RuntimeException("boom"); + })) .isInstanceOf(RuntimeException.class); assertThat(refCounter.isClosed()).isFalse(); @@ -692,8 +691,9 @@ private void doNoOpRefCounter() { } CompletableFuture.allOf(futures).join(); -// final Duration duration = Duration.between(startTime.get(), Instant.now()); -// final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * 1000); + // final Duration duration = Duration.between(startTime.get(), Instant.now()); + // final long iterationsPerSec = Math.round((double) iterations / duration.toMillis() * + // 1000); // System.out.println( // "All Finished" // + ", threads: " From 819881a7d62dd722b481b785a9507457ec7c2588 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:47:23 +0100 Subject: [PATCH 40/61] gh-279 Adding in tests written by @bernardladenthin --- src/test/java/org/lmdbjava/EnvTest.java | 154 ++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 222dd164..ef30f8ec 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -34,11 +34,15 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -801,6 +805,156 @@ void closeWithOpenCursor() { // can't close the env as we are unable to close the cursor } + /** + * Regression for the intermittent close-during-read SIGSEGV (lmdbjava#253 / lmdbjava#279). With + * safe close enabled, {@link Env#close()} must never unmap the memory map while another thread is + * still inside a live read transaction; instead it fails fast with {@link Env.EnvInUseException}. + * + *

        Unlike the {@code RefCounter} unit tests, this exercises the real {@code Env}/{@code Txn} + * wiring against native LMDB: many threads hammer {@code txnRead()}/{@code Dbi.get} while another + * thread races {@link Env#close()}. On {@code master} (no safe close) this reliably crashes the + * JVM in {@code mdb_txn_renew0}; with safe close the close is rejected while reads are in flight, + * readers only ever observe {@link Env.AlreadyClosedException}, and the env closes cleanly once + * the readers stop. + */ + @Test + void closeDuringConcurrentReads_isRejectedWhileReadersLiveAndSurvives() throws Exception { + final Path dir = tempDir.createTempDir(); + final Env env = + Env.create().setSafeClose().setMaxReaders(64).setMaxDbs(1).open(dir); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + for (int i = 0; i < 32; i++) { + db.put(bb(i), bb(i)); + } + + final int readerCount = 16; + final AtomicBoolean stop = new AtomicBoolean(false); + final AtomicLong reads = new AtomicLong(); + final List unexpected = new CopyOnWriteArrayList<>(); + final List readers = new ArrayList<>(readerCount); + + for (int i = 0; i < readerCount; i++) { + final int seed = i; + final Thread reader = + new Thread( + () -> { + int k = seed; + while (!stop.get()) { + try (Txn txn = env.txnRead()) { + db.get(txn, bb(k & 31)); + reads.incrementAndGet(); + k++; + } catch (final AlreadyClosedException expected) { + return; // benign: env is closing/closed + } catch (final Throwable t) { + unexpected.add(t); + return; + } + } + }, + "reader-" + seed); + reader.setDaemon(true); + reader.start(); + readers.add(reader); + } + + // A transaction held on this thread guarantees the count is non-zero, so the racing close() + // below deterministically fails fast rather than unmapping. The hammer threads meanwhile race + // real native txn begin/renew against that close(). + final Txn heldReader = env.txnRead(); + try { + Thread.sleep(100); // let the reader threads saturate the native read path + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + assertThat(env.isClosed()).isFalse(); // must NOT have unmapped with live readers + } finally { + heldReader.close(); + } + + // Stop the hammer threads and wait for every in-flight transaction to be released. + stop.set(true); + for (final Thread reader : readers) { + reader.join(5_000); + } + + // With no live transactions the env now closes cleanly. + env.close(); + + assertThat(reads.get()).isGreaterThan(0L); + assertThat(unexpected).isEmpty(); + assertThat(env.isClosed()).isTrue(); + } + + /** + * As {@link #closeDuringConcurrentReads_isRejectedWhileReadersLiveAndSurvives()} but the readers + * additionally open a {@link Cursor} on each transaction. Safe close newly tracks cursors as well + * as transactions, so this covers the cursor acquire/release wiring under a concurrent close + * race, which the existing single-threaded cursor test does not. + */ + @Test + void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() throws Exception { + final Path dir = tempDir.createTempDir(); + final Env env = + Env.create().setSafeClose().setMaxReaders(64).setMaxDbs(1).open(dir); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + for (int i = 0; i < 32; i++) { + db.put(bb(i), bb(i)); + } + + final int readerCount = 16; + final AtomicBoolean stop = new AtomicBoolean(false); + final AtomicLong reads = new AtomicLong(); + final List unexpected = new CopyOnWriteArrayList<>(); + final List readers = new ArrayList<>(readerCount); + + for (int i = 0; i < readerCount; i++) { + final Thread reader = + new Thread( + () -> { + while (!stop.get()) { + try (Txn txn = env.txnRead(); + Cursor cursor = db.openCursor(txn)) { + cursor.first(); + reads.incrementAndGet(); + } catch (final AlreadyClosedException expected) { + return; // benign: env is closing/closed + } catch (final Throwable t) { + unexpected.add(t); + return; + } + } + }, + "cursor-reader-" + i); + reader.setDaemon(true); + reader.start(); + readers.add(reader); + } + + // A cursor held on this thread guarantees a non-zero count during the racing close(). + final Txn heldReader = env.txnRead(); + final Cursor heldCursor = db.openCursor(heldReader); + try { + Thread.sleep(100); + Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + assertThat(env.isClosed()).isFalse(); + } finally { + heldCursor.close(); + heldReader.close(); + } + + stop.set(true); + for (final Thread reader : readers) { + reader.join(5_000); + } + + env.close(); + + assertThat(reads.get()).isGreaterThan(0L); + assertThat(unexpected).isEmpty(); + assertThat(env.isClosed()).isTrue(); + } + @ParameterizedTest @CsvSource({ "true, true, false", From 6ceea101a86ded78d0340cd1f7fab195251ef219 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:07:58 +0100 Subject: [PATCH 41/61] gh-279 Add test coverage --- src/test/java/org/lmdbjava/EnvTest.java | 369 +++++++++++++----------- 1 file changed, 206 insertions(+), 163 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index ef30f8ec..83826d91 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -56,7 +56,9 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** Test {@link Env}. */ +/** + * Test {@link Env}. + */ public final class EnvTest { private TempDir tempDir; @@ -75,12 +77,12 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } @@ -103,6 +105,7 @@ void cannotChangeBuilderAfterOpen() { assertThatThrownBy(() -> builder.setEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR))) .isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(() -> builder.setMaxReaders(1)).isInstanceOf(AlreadyOpenException.class); + //noinspection OctalInteger assertThatThrownBy(() -> builder.setFilePermissions(0666)) .isInstanceOf(AlreadyOpenException.class); assertThatThrownBy(() -> builder.setMaxDbs(1)).isInstanceOf(AlreadyOpenException.class); @@ -135,33 +138,33 @@ void cannotInfoOnceClosed() { @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @@ -272,7 +275,7 @@ void copyFileBased() { assertThat(Files.exists(dest)).isFalse(); final Path src = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { env.copy(dest, MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); @@ -285,7 +288,7 @@ void copyFileRejectsExistingDestination() throws IOException { assertThat(Files.exists(dest)).isTrue(); final Path src = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } @@ -306,13 +309,13 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -322,7 +325,7 @@ void createAsFile() { void detectTransactionThreadViolation() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { + Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { try (Txn ignored = env.txnRead()) { // When NOT using MDB_NOTLS flag, you cannot open a second read txn on the same thread assertThatThrownBy(env::txnRead).isInstanceOf(BadReaderLockException.class); @@ -334,15 +337,16 @@ void detectTransactionThreadViolation() { void multipleReadTxnsOnSameThread() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(3) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(3) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .open(file)) { try (Txn ignored1 = env.txnRead()) { // MDB_NOTLS flag allows us to open multiple read txns on the same thread //noinspection EmptyTryBlock - try (Txn ignored2 = env.txnRead()) {} + try (Txn ignored2 = env.txnRead()) { + } } } } @@ -351,13 +355,13 @@ void multipleReadTxnsOnSameThread() { void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -379,25 +383,25 @@ void mapFull() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); assertThatThrownBy( - () -> { - // Fill the env until MapFullException is thrown - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - }) + () -> { + // Fill the env until MapFullException is thrown + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + }) .isInstanceOf(MapFullException.class); } } @@ -411,7 +415,7 @@ void readOnlySupported() { rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -433,12 +437,12 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(256, ByteUnit.KIBIBYTES) - .setMaxDbs(1) - .open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -490,7 +494,7 @@ void setMapSize() { void stats() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { final Stat stat = env.stat(); assertThat(stat).isNotNull(); assertThat(stat.branchPages).isEqualTo(0L); @@ -507,7 +511,7 @@ void stats() { void testDefaultOpen() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -520,7 +524,7 @@ void testDefaultOpen() { void testDefaultOpenNoName1() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -549,7 +553,7 @@ void testDefaultOpenNoName1() { void testDefaultOpenNoName2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -573,14 +577,14 @@ void testDefaultOpenNoName2() { void addEnvFlag() { final Path file = tempDir.createTempFile(); try (final Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -592,17 +596,17 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -614,14 +618,14 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -634,18 +638,18 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -657,14 +661,14 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -679,14 +683,15 @@ void setEnvFlags_null1() { () -> { //noinspection EmptyTryBlock try (final Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) {} + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class) .hasMessageContaining("No such file or directory"); @@ -700,14 +705,15 @@ void setEnvFlags_null2() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) {} + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -720,14 +726,15 @@ void setEnvFlags_null3() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) {} + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) { + } }) .isInstanceOf(LmdbNativeException.class); } @@ -862,13 +869,10 @@ void closeDuringConcurrentReads_isRejectedWhileReadersLiveAndSurvives() throws E // A transaction held on this thread guarantees the count is non-zero, so the racing close() // below deterministically fails fast rather than unmapping. The hammer threads meanwhile race // real native txn begin/renew against that close(). - final Txn heldReader = env.txnRead(); - try { + try (Txn ignoredHeldReader = env.txnRead()) { Thread.sleep(100); // let the reader threads saturate the native read path Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); assertThat(env.isClosed()).isFalse(); // must NOT have unmapped with live readers - } finally { - heldReader.close(); } // Stop the hammer threads and wait for every in-flight transaction to be released. @@ -914,7 +918,7 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th () -> { while (!stop.get()) { try (Txn txn = env.txnRead(); - Cursor cursor = db.openCursor(txn)) { + Cursor cursor = db.openCursor(txn)) { cursor.first(); reads.incrementAndGet(); } catch (final AlreadyClosedException expected) { @@ -957,18 +961,19 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th @ParameterizedTest @CsvSource({ - "true, true, false", - "true, false, false", - "false, true, false", - "false, false, false", - "false, false, true" + "TRUE, TRUE", + "TRUE, FALSE", + "FALSE, TRUE", + "FALSE, FALSE", + "NO_ARG, NO_ARG", + "NOT_CALLED, NOT_CALLED" }) - void singleThreaded(final boolean safeClose, final boolean singleThreaded, final boolean noArgs) { - testEnvUse(safeClose, singleThreaded, noArgs); + void singleThreaded(final BooleanArg safeClose, final BooleanArg singleThreaded) { + testEnvUse(safeClose, singleThreaded); } private void testEnvUse( - final boolean safeClose, final boolean singleThreaded, final boolean noArgs) { + final BooleanArg safeClose, final BooleanArg singleThreaded) { final Path file = tempDir.createTempFile(); final Builder builder = @@ -978,13 +983,34 @@ private void testEnvUse( .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR); - if (noArgs) { - builder.setSafeClose().setSingleThreaded(); - } else { - builder.setSafeClose(safeClose).setSingleThreaded(singleThreaded); + switch (safeClose) { + case TRUE: + case FALSE: + builder.setSafeClose(safeClose.getAsBoolean()); + // Handle no argument case + break; + case NO_ARG: + builder.setSafeClose(); + break; + } + + switch (singleThreaded) { + case TRUE: + case FALSE: + builder.setSingleThreaded(singleThreaded.getAsBoolean()); + // Handle no argument case + break; + case NO_ARG: + builder.setSingleThreaded(); + break; } try (Env env = builder.open(file)) { + assertThat(env.isSafeClose()) + .isEqualTo(safeClose.getAsBoolean()); + assertThat(env.isSingleThreaded()) + .isEqualTo(singleThreaded.getAsBoolean()); + final Dbi dbi = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -992,7 +1018,7 @@ private void testEnvUse( for (int i = 0; i < 10; i++) { dbi.put(txn, bb(i), bb(100 + i), MDB_APPENDDUP); - if (safeClose) { + if (safeClose.getAsBoolean()) { Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); } } @@ -1001,13 +1027,13 @@ private void testEnvUse( for (int i = 0; i < 5; i++) { try (Txn txn = env.txnRead(); - Cursor cursor = dbi.openCursor(txn)) { + Cursor cursor = dbi.openCursor(txn)) { int j = 0; while (cursor.next()) { final KeyVal keyVal = cursor.keyVal(); Assertions.assertThat(keyVal.key().getInt()).isEqualTo(j); Assertions.assertThat(keyVal.val().getInt()).isEqualTo(100 + j); - if (safeClose) { + if (safeClose.getAsBoolean()) { Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); } j++; @@ -1016,4 +1042,21 @@ private void testEnvUse( } } } + + private enum BooleanArg { + TRUE(true), + FALSE(false), + NO_ARG(true), + NOT_CALLED(false); // Both safeClose and singleThreaded default to false if not set + + private final boolean isTrue; + + BooleanArg(final boolean isTrue) { + this.isTrue = isTrue; + } + + boolean getAsBoolean() { + return isTrue; + } + } } From fae66eeaddcc5991fef421f377a5d468a0e1c21c Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:19:19 +0100 Subject: [PATCH 42/61] gh-279 Add test coverage --- .../java/org/lmdbjava/StripedRefCounter.java | 12 +++--- .../java/org/lmdbjava/RefCounterTest.java | 18 +++++++++ .../org/lmdbjava/StripedRefCounterTest.java | 37 +++++++++++++++++++ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 4e6ffe31..35a05874 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -63,10 +63,11 @@ class StripedRefCounter implements RefCounter { } StripedRefCounter(final int stripeCount) { - validateStripeCount(stripeCount); - this.stripeMask = stripeCount - 1; - this.counters = new Stripe[stripeCount]; - for (int i = 0; i < stripeCount; i++) { + final int effectiveStripeCount = lowestPowerOfTwoGreaterThanOrEqualTo(stripeCount); + validateStripeCount(effectiveStripeCount); + this.stripeMask = effectiveStripeCount - 1; + this.counters = new Stripe[effectiveStripeCount]; + for (int i = 0; i < effectiveStripeCount; i++) { counters[i] = new Stripe(this); } } @@ -321,9 +322,6 @@ private void validateStripeCount(final int stripeCount) { throw new IllegalArgumentException( "Stripe count exceeds maximum. Got: " + stripeCount + ", max: " + MAX_STRIPES); } - if ((stripeCount & (stripeCount - 1)) != 0) { - throw new IllegalArgumentException("Stripe count must be power of 2, got: " + stripeCount); - } } /** diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index c35c3a99..8a6c50df 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -562,6 +562,23 @@ void releaseAfterClose(final RefCounter refCounter) { assertThatThrownBy(releaser::release).isInstanceOf(Env.AlreadyClosedException.class); } + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void countAfterClose(final RefCounter refCounter) { + final AtomicInteger onCloseCallCount = new AtomicInteger(); + assertThat(refCounter.getCount()).isZero(); + final RefCounter.RefCounterReleaser releaser = refCounter.acquire(); + if (!(refCounter instanceof NoOpRefCounter)) { + assertThat(refCounter.getCount()).isEqualTo(1); + } + // Need to release to allow the close + releaser.release(); + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount.get()).isEqualTo(1); + + assertThat(refCounter.getCount()).isZero(); + } + @ParameterizedTest @MethodSource("allRefCounterProvider") void use(final RefCounter refCounter) { @@ -593,6 +610,7 @@ void use(final RefCounter refCounter) { refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount.get()).isEqualTo(1); + // use after close if (!(refCounter instanceof NoOpRefCounter)) { assertThatThrownBy(() -> refCounter.use(useCallCount::incrementAndGet)) .isInstanceOf(Env.AlreadyClosedException.class); diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index d4e94d46..aeeb90c6 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -16,6 +16,7 @@ package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.Test; @@ -46,4 +47,40 @@ void lowestPowerOfTwoGreaterThanOrEqualTo() { assertThat(StripedRefCounter.lowestPowerOfTwoGreaterThanOrEqualTo(536870913)) .isEqualTo(1073741824); } + + @Test + void getStripeCount() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(); + assertThat(stripedRefCounter.getStripeCount()).isGreaterThan(1); + } + + @Test + void getStripeCount2() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(16); + assertThat(stripedRefCounter.getStripeCount()).isEqualTo(16); + } + + @Test + void getStripeCount3() { + final StripedRefCounter stripedRefCounter = new StripedRefCounter(15); + assertThat(stripedRefCounter.getStripeCount()).isEqualTo(16); + } + + @Test + void getStripeCount4() { + assertThatThrownBy(() -> new StripedRefCounter(99999999)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void getStripeCount5() { + assertThatThrownBy(() -> new StripedRefCounter(0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void getStripeCount6() { + assertThatThrownBy(() -> new StripedRefCounter(-1)) + .isInstanceOf(IllegalArgumentException.class); + } } From 04043b500f5f2fe455c7b688c22dde62001270c1 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:22:46 +0100 Subject: [PATCH 43/61] gh-279 Fix GH suggestions --- src/test/java/org/lmdbjava/EnvTest.java | 323 +++++++++--------- .../org/lmdbjava/StripedRefCounterTest.java | 3 +- 2 files changed, 161 insertions(+), 165 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 83826d91..02a367d4 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -56,9 +56,7 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** - * Test {@link Env}. - */ +/** Test {@link Env}. */ public final class EnvTest { private TempDir tempDir; @@ -77,12 +75,12 @@ void afterEach() { void byteUnit() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(1, ByteUnit.MEBIBYTES) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } @@ -138,33 +136,33 @@ void cannotInfoOnceClosed() { @Test void cannotOverflowMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - final int mb = 1_024 * 1_024; - //noinspection NumericOverflow // Intentional overflow - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow + final int size = mb * 2_048; // as per issue 18 + builder.setMapSize(size); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1); + }) .isInstanceOf(IllegalArgumentException.class); } @Test void negativeMapSize2() { assertThatThrownBy( - () -> { - final Builder builder = Env.create().setSafeClose().setMaxReaders(1); - builder.setMapSize(-1, ByteUnit.MEBIBYTES); - }) + () -> { + final Builder builder = Env.create().setSafeClose().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) .isInstanceOf(IllegalArgumentException.class); } @@ -275,7 +273,7 @@ void copyFileBased() { assertThat(Files.exists(dest)).isFalse(); final Path src = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { env.copy(dest, MDB_CP_COMPACT); } assertThat(FileUtil.size(dest)).isGreaterThan(0L); @@ -288,7 +286,7 @@ void copyFileRejectsExistingDestination() throws IOException { assertThat(Files.exists(dest)).isTrue(); final Path src = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { assertThatThrownBy(() -> env.copy(dest, MDB_CP_COMPACT)) .isInstanceOf(InvalidCopyDestination.class); } @@ -309,13 +307,13 @@ void createAsDirectory() { void createAsFile() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); } @@ -325,7 +323,7 @@ void createAsFile() { void detectTransactionThreadViolation() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { + Env.create().setSafeClose().setMaxReaders(3).setEnvFlags(MDB_NOSUBDIR).open(file)) { try (Txn ignored = env.txnRead()) { // When NOT using MDB_NOTLS flag, you cannot open a second read txn on the same thread assertThatThrownBy(env::txnRead).isInstanceOf(BadReaderLockException.class); @@ -337,16 +335,15 @@ void detectTransactionThreadViolation() { void multipleReadTxnsOnSameThread() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(3) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(3) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .open(file)) { try (Txn ignored1 = env.txnRead()) { // MDB_NOTLS flag allows us to open multiple read txns on the same thread //noinspection EmptyTryBlock - try (Txn ignored2 = env.txnRead()) { - } + try (Txn ignored2 = env.txnRead()) {} } } } @@ -355,13 +352,13 @@ void multipleReadTxnsOnSameThread() { void info() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(4) - .setMapSize(123_456) - .setEnvFlags(MDB_NOSUBDIR) - .setEnvFlags(MDB_NOSUBDIR) - .open(file)) { + Env.create() + .setSafeClose() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { final EnvInfo info = env.info(); assertThat(info).isNotNull(); assertThat(info.lastPageNumber).isEqualTo(1L); @@ -383,25 +380,25 @@ void mapFull() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(8, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); assertThatThrownBy( - () -> { - // Fill the env until MapFullException is thrown - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - }) + () -> { + // Fill the env until MapFullException is thrown + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + }) .isInstanceOf(MapFullException.class); } } @@ -415,7 +412,7 @@ void readOnlySupported() { rwDb.put(bb(1), bb(42)); } try (Env roEnv = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { final Dbi roDb = roEnv .createDbi() @@ -437,12 +434,12 @@ void setMapSize() { final ByteBuffer val = allocateDirect(1_024); final Random rnd = new Random(); try (Env env = - Env.create() - .setSafeClose() - .setMaxReaders(1) - .setMapSize(256, ByteUnit.KIBIBYTES) - .setMaxDbs(1) - .open(dir)) { + Env.create() + .setSafeClose() + .setMaxReaders(1) + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxDbs(1) + .open(dir)) { final Dbi db = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -494,7 +491,7 @@ void setMapSize() { void stats() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { final Stat stat = env.stat(); assertThat(stat).isNotNull(); assertThat(stat.branchPages).isEqualTo(0L); @@ -511,7 +508,7 @@ void stats() { void testDefaultOpen() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -524,7 +521,7 @@ void testDefaultOpen() { void testDefaultOpenNoName1() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -553,7 +550,7 @@ void testDefaultOpenNoName1() { void testDefaultOpenNoName2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + Env.create().setSafeClose().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); final Dbi db = @@ -577,14 +574,14 @@ void testDefaultOpenNoName2() { void addEnvFlag() { final Path file = tempDir.createTempFile(); try (final Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -596,17 +593,17 @@ void addEnvFlag() { void addEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one - .addEnvFlag(null) // no-op - .addEnvFlags((EnvFlagSet) null) // no-op - .addEnvFlags((Collection) null) // no-op - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); @@ -618,14 +615,14 @@ void addEnvFlags() { void addEnvFlags2() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .addEnvFlags(Collections.singleton(MDB_NOSYNC)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()) .containsExactlyInAnyOrderElementsOf( @@ -638,18 +635,18 @@ void addEnvFlags2() { void setEnvFlags() { final Path file = tempDir.createTempFile(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags((EnvFlagSet) null) // No-op - .setEnvFlags((EnvFlags) null) // No-op - .setEnvFlags((EnvFlags[]) null) // No-op - .setEnvFlags((Collection) null) // No-op - .setEnvFlags(MDB_NOSYNC) // Will be overwritten - .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) - .open(file)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { env.sync(true); assertThat(Files.isRegularFile(file)).isTrue(); assertThat(env.getEnvFlagSet().getFlags()) @@ -661,14 +658,14 @@ void setEnvFlags() { void setEnvFlags2() { final Path dir = tempDir.createTempDir(); try (Env env = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) - .setEnvFlags(Collections.emptySet()) // Clears them - .open(dir)) { + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { env.sync(true); assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); assertThat(Files.isDirectory(dir)); @@ -683,15 +680,14 @@ void setEnvFlags_null1() { () -> { //noinspection EmptyTryBlock try (final Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((Collection) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class) .hasMessageContaining("No such file or directory"); @@ -705,15 +701,14 @@ void setEnvFlags_null2() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlags) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -726,15 +721,14 @@ void setEnvFlags_null3() { () -> { //noinspection EmptyTryBlock try (Env ignored = - Env.create() - .setSafeClose() - .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxDbs(1) - .setMaxReaders(1) - .addEnvFlag(MDB_NOSUBDIR) - .setEnvFlags((EnvFlagSet) null) // Clears the flags - .open(file)) { - } + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) {} }) .isInstanceOf(LmdbNativeException.class); } @@ -918,7 +912,7 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th () -> { while (!stop.get()) { try (Txn txn = env.txnRead(); - Cursor cursor = db.openCursor(txn)) { + Cursor cursor = db.openCursor(txn)) { cursor.first(); reads.incrementAndGet(); } catch (final AlreadyClosedException expected) { @@ -961,19 +955,18 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th @ParameterizedTest @CsvSource({ - "TRUE, TRUE", - "TRUE, FALSE", - "FALSE, TRUE", - "FALSE, FALSE", - "NO_ARG, NO_ARG", - "NOT_CALLED, NOT_CALLED" + "TRUE, TRUE", + "TRUE, FALSE", + "FALSE, TRUE", + "FALSE, FALSE", + "NO_ARG, NO_ARG", + "NOT_CALLED, NOT_CALLED" }) void singleThreaded(final BooleanArg safeClose, final BooleanArg singleThreaded) { testEnvUse(safeClose, singleThreaded); } - private void testEnvUse( - final BooleanArg safeClose, final BooleanArg singleThreaded) { + private void testEnvUse(final BooleanArg safeClose, final BooleanArg singleThreaded) { final Path file = tempDir.createTempFile(); final Builder builder = @@ -992,6 +985,9 @@ private void testEnvUse( case NO_ARG: builder.setSafeClose(); break; + case NOT_CALLED: + // Don't call anything + break; } switch (singleThreaded) { @@ -1003,13 +999,14 @@ private void testEnvUse( case NO_ARG: builder.setSingleThreaded(); break; + case NOT_CALLED: + // Don't call anything + break; } try (Env env = builder.open(file)) { - assertThat(env.isSafeClose()) - .isEqualTo(safeClose.getAsBoolean()); - assertThat(env.isSingleThreaded()) - .isEqualTo(singleThreaded.getAsBoolean()); + assertThat(env.isSafeClose()).isEqualTo(safeClose.getAsBoolean()); + assertThat(env.isSingleThreaded()).isEqualTo(singleThreaded.getAsBoolean()); final Dbi dbi = env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); @@ -1027,7 +1024,7 @@ private void testEnvUse( for (int i = 0; i < 5; i++) { try (Txn txn = env.txnRead(); - Cursor cursor = dbi.openCursor(txn)) { + Cursor cursor = dbi.openCursor(txn)) { int j = 0; while (cursor.next()) { final KeyVal keyVal = cursor.keyVal(); diff --git a/src/test/java/org/lmdbjava/StripedRefCounterTest.java b/src/test/java/org/lmdbjava/StripedRefCounterTest.java index aeeb90c6..3cc9752a 100644 --- a/src/test/java/org/lmdbjava/StripedRefCounterTest.java +++ b/src/test/java/org/lmdbjava/StripedRefCounterTest.java @@ -74,8 +74,7 @@ void getStripeCount4() { @Test void getStripeCount5() { - assertThatThrownBy(() -> new StripedRefCounter(0)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new StripedRefCounter(0)).isInstanceOf(IllegalArgumentException.class); } @Test From 58f77e1f09b79340da7a7551905261320b19c358 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:28:55 +0100 Subject: [PATCH 44/61] gh-279 Add test coverage --- src/test/java/org/lmdbjava/RefCounterTest.java | 2 ++ src/test/java/org/lmdbjava/TxnTest.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 8a6c50df..28bec1c9 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -537,6 +537,8 @@ void immediateClose(final RefCounter refCounter) { final AtomicInteger onCloseCallCount = new AtomicInteger(); refCounter.close(onCloseCallCount::incrementAndGet); assertThat(refCounter.getCount()).isZero(); + assertThat(refCounter.isClosed()).isTrue(); + assertThatThrownBy(refCounter::checkNotClosed).isInstanceOf(Env.AlreadyClosedException.class); assertThat(onCloseCallCount.get()).isEqualTo(1); } diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 9170c255..aedc718c 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -142,6 +142,8 @@ void readOnlyTxnAllowedInReadOnlyEnv() { .open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); + assertThat(readTxn.isReadOnly()).isTrue(); + assertThat(readTxn.isWritable()).isFalse(); } } } @@ -387,6 +389,7 @@ void txReadWrite() { assertThat(txn.getParent()).isNull(); assertThat(txn.getState()).isEqualTo(READY); assertThat(txn.isReadOnly()).isFalse(); + assertThat(txn.isWritable()).isTrue(); txn.checkReady(); txn.checkWritesAllowed(); txn.commit(); From 07297285ea6d9392b0cc4179a8b522a49c806818 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:13:22 +0100 Subject: [PATCH 45/61] gh-279 Fix failing test --- src/main/java/org/lmdbjava/Env.java | 5 +++++ src/main/java/org/lmdbjava/NoOpRefCounter.java | 3 --- src/main/java/org/lmdbjava/TargetName.java | 2 +- src/test/java/org/lmdbjava/EnvTest.java | 11 +++++++++++ src/test/java/org/lmdbjava/RefCounterTest.java | 8 ++++++-- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 08b88785..b7c1dd3c 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -596,6 +596,7 @@ public void sync(final boolean force) { */ @Deprecated public Txn txn(final Txn parent, final TxnFlags... flags) { + checkNotClosed(); return new Txn<>(this, parent, proxy, TxnFlagSet.of(flags)); } @@ -606,6 +607,7 @@ public Txn txn(final Txn parent, final TxnFlags... flags) { * @return a transaction (never null) */ public Txn txn(final Txn parent) { + checkNotClosed(); return new Txn<>(this, parent, proxy, TxnFlagSet.EMPTY); } @@ -619,6 +621,7 @@ public Txn txn(final Txn parent) { * @return a transaction (never null) */ public Txn txn(final Txn parent, final TxnFlagSet flags) { + checkNotClosed(); return new Txn<>(this, parent, proxy, flags); } @@ -628,6 +631,7 @@ public Txn txn(final Txn parent, final TxnFlagSet flags) { * @return a read-only transaction */ public Txn txnRead() { + checkNotClosed(); return new Txn<>(this, null, proxy, TxnFlags.MDB_RDONLY_TXN); } @@ -637,6 +641,7 @@ public Txn txnRead() { * @return a read-write transaction */ public Txn txnWrite() { + checkNotClosed(); return new Txn<>(this, null, proxy, TxnFlagSet.EMPTY); } diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index 794f519f..c8be7dd1 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -33,9 +33,6 @@ public class NoOpRefCounter implements RefCounter { @Override public RefCounterReleaser acquire() { - if (isClosed.get()) { - throw new Env.AlreadyClosedException(); - } return NO_OP_RELEASER; } diff --git a/src/main/java/org/lmdbjava/TargetName.java b/src/main/java/org/lmdbjava/TargetName.java index 49a65dea..7987c0c4 100644 --- a/src/main/java/org/lmdbjava/TargetName.java +++ b/src/main/java/org/lmdbjava/TargetName.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 02a367d4..d164dff9 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -202,6 +202,17 @@ void cannotOpenWriteTxnOnceClosed() { assertThatThrownBy(env::txnWrite).isInstanceOf(AlreadyClosedException.class); } + @Test + void cannotOpenTxnOnceClosed() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + assertThatThrownBy(() -> env.txn(null)).isInstanceOf(AlreadyClosedException.class); + assertThatThrownBy(() -> env.txn(null, TxnFlags.MDB_RDONLY_TXN)) + .isInstanceOf(AlreadyClosedException.class); + } + @Test void copyDirectoryBased() { final Path dest = tempDir.createTempDir(); diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index 28bec1c9..fe9b25f2 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -548,7 +548,10 @@ void acquireAfterClose(final RefCounter refCounter) { final AtomicInteger onCloseCallCount = new AtomicInteger(); refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount.get()).isEqualTo(1); - assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); + + if (!(refCounter instanceof NoOpRefCounter)) { + assertThatThrownBy(refCounter::acquire).isInstanceOf(Env.AlreadyClosedException.class); + } } @ParameterizedTest @@ -561,7 +564,8 @@ void releaseAfterClose(final RefCounter refCounter) { refCounter.close(onCloseCallCount::incrementAndGet); assertThat(onCloseCallCount.get()).isEqualTo(1); - assertThatThrownBy(releaser::release).isInstanceOf(Env.AlreadyClosedException.class); + // This is a no-op as already released + releaser.release(); } @ParameterizedTest From b083c29c5184c53913a4e5207c0feebe02f61b58 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:40:55 +0100 Subject: [PATCH 46/61] gh-279 Fix catch in Cursor ctor --- src/main/java/org/lmdbjava/Cursor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 09b36f7b..7d89a087 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -61,8 +61,10 @@ public final class Cursor implements AutoCloseable { try { this.kv = txn.newKeyVal(); } catch (final Exception e) { + closed.set(true); this.refCounterReleaser.release(); - closed.set(false); + // Clean up the native cursor + LIB.mdb_cursor_close(ptrCursor); throw e; } } From 859c0db09356adaaf97b94baea67342db54cfe93 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 09:25:21 +0100 Subject: [PATCH 47/61] gh-279 Tidy asserts in TxnTest --- src/test/java/org/lmdbjava/TxnTest.java | 63 ++++++++++++++++--------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index aedc718c..0750b3e8 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -142,8 +142,7 @@ void readOnlyTxnAllowedInReadOnlyEnv() { .open(file)) { try (Txn readTxn = roEnv.txnRead()) { assertThat(readTxn).isNotNull(); - assertThat(readTxn.isReadOnly()).isTrue(); - assertThat(readTxn.isWritable()).isFalse(); + assertReadOnly(readTxn); } } } @@ -227,9 +226,9 @@ void testGetId() { @Test void txCanCommitThenCloseWithoutError() { try (Txn txn = env.txnRead()) { - assertThat(txn.getState()).isEqualTo(READY); + assertState(txn, READY); txn.commit(); - assertThat(txn.getState()).isEqualTo(DONE); + assertState(txn, DONE); } } @@ -237,10 +236,9 @@ void txCanCommitThenCloseWithoutError() { void txCannotAbortIfAlreadyCommitted() { try (Txn txn = env.txnRead()) { - assertThat(txn.getState()).isEqualTo(READY); + assertState(txn, READY); txn.commit(); - assertThat(txn.getState()).isEqualTo(DONE); - + assertState(txn, DONE); assertThatThrownBy(txn::abort).isInstanceOf(NotReadyException.class); } } @@ -368,18 +366,16 @@ void txParentRWChildROIncompatible() { void txReadOnly() { try (Txn txn = env.txnRead()) { assertThat(txn.getParent()).isNull(); - assertThat(txn.getState()).isEqualTo(READY); - assertThat(txn.isReadOnly()).isTrue(); - txn.checkReady(); - txn.checkReadOnly(); + assertState(txn, READY); + assertReadOnly(txn); txn.reset(); - assertThat(txn.getState()).isEqualTo(RESET); + assertState(txn, RESET); txn.renew(); - assertThat(txn.getState()).isEqualTo(READY); + assertState(txn, READY); txn.commit(); - assertThat(txn.getState()).isEqualTo(DONE); + assertState(txn, DONE); txn.close(); - assertThat(txn.getState()).isEqualTo(RELEASED); + assertState(txn, RELEASED); } } @@ -387,15 +383,12 @@ void txReadOnly() { void txReadWrite() { final Txn txn = env.txnWrite(); assertThat(txn.getParent()).isNull(); - assertThat(txn.getState()).isEqualTo(READY); - assertThat(txn.isReadOnly()).isFalse(); - assertThat(txn.isWritable()).isTrue(); - txn.checkReady(); - txn.checkWritesAllowed(); + assertState(txn, READY); + assertWritable(txn); txn.commit(); - assertThat(txn.getState()).isEqualTo(DONE); + assertState(txn, DONE); txn.close(); - assertThat(txn.getState()).isEqualTo(RELEASED); + assertState(txn, RELEASED); } @Test @@ -451,4 +444,30 @@ void zeroByteKeysRejected() { }) .isInstanceOf(BadValueSizeException.class); } + + private void assertState(final Txn txn, final Txn.State expectedState) { + assertThat(txn.getState()).isEqualTo(expectedState); + if (expectedState == READY) { + assertThat(txn.isReady()).isTrue(); + txn.checkReady(); + } else { + assertThat(txn.isReady()).isFalse(); + assertThatThrownBy(txn::checkReady).isInstanceOf(NotReadyException.class); + } + } + + private void assertReadOnly(final Txn txn) { + assertThat(txn.isReadOnly()).isTrue(); + assertThat(txn.isWritable()).isFalse(); + txn.checkReadOnly(); + assertThatThrownBy(txn::checkWritesAllowed).isInstanceOf(ReadWriteRequiredException.class); + } + + private void assertWritable(final Txn txn) { + assertThat(txn.isReadOnly()).isFalse(); + assertThat(txn.isWritable()).isTrue(); + assertThatThrownBy(txn::checkReadOnly).isInstanceOf(ReadOnlyRequiredException.class); + // Should not throw in a writable state + txn.checkWritesAllowed(); + } } From 3c2ad81334f5c58b97054cae77513516a4daceb7 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:25:00 +0100 Subject: [PATCH 48/61] gh-279 Add Env.tryClose, add more tests --- src/main/java/org/lmdbjava/Env.java | 56 +++++++- .../java/org/lmdbjava/NoOpRefCounter.java | 11 ++ src/main/java/org/lmdbjava/RefCounter.java | 10 ++ .../java/org/lmdbjava/SimpleRefCounter.java | 13 ++ .../lmdbjava/SingleThreadedRefCounter.java | 14 ++ .../java/org/lmdbjava/StripedRefCounter.java | 35 ++++- .../org/lmdbjava/SynchronisedRefCounter.java | 15 ++ src/test/java/org/lmdbjava/EnvTest.java | 128 +++++++++++++++++- .../java/org/lmdbjava/RefCounterTest.java | 51 ++++++- src/test/java/org/lmdbjava/TxnTest.java | 24 +++- 10 files changed, 344 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index b7c1dd3c..bbadc77a 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -157,7 +157,7 @@ public static Env open(final File path, final int size, final EnvFla /** * Close the handle. * - *

        Will silently return if already closed or never opened. + *

        Will silently return if already closed. * *

        Before and during this call, the caller MUST ensure that: * @@ -184,12 +184,60 @@ public static Env open(final File path, final int size, final EnvFla *

        If safeClose has been enabled on the {@link Env}, then this method will throw a {@link * EnvInUseException} if transactions or cursors are still active. * - * @throws EnvInUseException If safeClose has been set and {@link Txn} or {@link Cursor} is still - * open on this {@link Env} + *

        If safeClose has not been enabled then this method will perform the close regardless of + * whether it is in use or not with the implications detailed above. + * + * @throws EnvInUseException If safeClose has been set and a {@link Txn} or {@link Cursor} is + * still open on this {@link Env} */ @Override public void close() { - refCounter.close(() -> LIB.mdb_env_close(ptr)); + refCounter.close(this::doClose); + } + + /** + * Try to close the handle. + * + *

        Will silently return if already closed. + * + *

        Before and during this call, the caller MUST ensure that: + * + *

          + *
        • every {@link Txn} and {@link Cursor} obtained from this environment has already been + * closed; and + *
        • no other thread is executing any operation on this environment or on a handle + * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as + * {@code Dbi.get}. + *
        + * + *

        Violating this contract is undefined behaviour that can crash the whole JVM + * ({@code SIGSEGV} on Linux/macOS, {@code EXCEPTION_ACCESS_VIOLATION 0xC0000005} on Windows); it + * does not raise a Java exception. The underlying {@code mdb_env_close} unmaps the + * memory map, so a transaction still being started or used on another thread then dereferences + * freed memory — typically observed as a native crash in {@code mdb_txn_renew0} / {@code + * mdb_txn_begin}. + * + *

        If you must close an environment while reader threads may still be active, serialise the + * close against those readers in application code: e.g. a read/write lock where each reader holds + * the read lock for the entire duration of its transaction and {@code close()} holds the write + * lock, so the map is never unmapped while a read is in flight. + * + *

        If safeClose has been enabled on the {@link Env}, then this method will return false if + * transactions or cursors are still active. + * + *

        If safeClose has not been enabled then this method will perform the close regardless of + * whether it is in use or not with the implications detailed above, i.e. it has the same + * behaviour as {@link #close()} with safeClose disabled. + * + * @return {@code true} if the environment was closed or {@code false} if it was already closed or + * safeClose prevented its closure due to being in use. + */ + public boolean tryClose() { + return refCounter.tryClose(this::doClose); + } + + private void doClose() { + LIB.mdb_env_close(ptr); } /** diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index c8be7dd1..5f1fcdda 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -49,6 +49,17 @@ public void close(final Runnable onClose) { } } + @Override + public boolean tryClose(Runnable onClose) { + if (isClosed.compareAndSet(false, true)) { + // Close with no checks + onClose.run(); + return true; + } else { + return false; + } + } + @Override public boolean isClosed() { return isClosed.get(); diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index b1f8cf94..0887cc2a 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -53,6 +53,16 @@ default void use(final Runnable runnable) { */ void close(final Runnable onClose); + /** + * If the reference count is zero, onClose will be called and true returned. This {@link + * RefCounter} will be marked as closed so all future calls to acquire will throw a {@link + * org.lmdbjava.Env.AlreadyClosedException}. If the count is non-zero it is a no-op and false is + * returned. If already closed, this is a no-op and false is returned. + * + * @return True if onClose was called. + */ + boolean tryClose(final Runnable onClose); + /** * @return True if {@link RefCounter} has been closed. */ diff --git a/src/main/java/org/lmdbjava/SimpleRefCounter.java b/src/main/java/org/lmdbjava/SimpleRefCounter.java index 21edc484..98c9be0f 100644 --- a/src/main/java/org/lmdbjava/SimpleRefCounter.java +++ b/src/main/java/org/lmdbjava/SimpleRefCounter.java @@ -62,6 +62,19 @@ public void close(final Runnable onClose) { } } + @Override + public boolean tryClose(Runnable onClose) { + Objects.requireNonNull(onClose); + if (counter.get() != CLOSED_VALUE) { + // Set to CLOSED_VALUE to indicate closure, if the count is 0 + if (counter.compareAndSet(0, CLOSED_VALUE)) { + onClose.run(); + return true; + } + } + return false; + } + private void release() { final int newVal = counter.updateAndGet(currVal -> currVal == CLOSED_VALUE ? currVal : currVal - 1); diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 65a59075..572e14ed 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -65,6 +65,20 @@ public void close(final Runnable onClose) { } } + @Override + public boolean tryClose(Runnable onClose) { + Objects.requireNonNull(onClose); + if (!isClosed) { + final long count = getCount(); + if (count == 0) { + isClosed = true; + onClose.run(); + return true; + } + } + return false; + } + @Override public boolean isClosed() { return isClosed; diff --git a/src/main/java/org/lmdbjava/StripedRefCounter.java b/src/main/java/org/lmdbjava/StripedRefCounter.java index 35a05874..a052f9e5 100644 --- a/src/main/java/org/lmdbjava/StripedRefCounter.java +++ b/src/main/java/org/lmdbjava/StripedRefCounter.java @@ -141,16 +141,34 @@ private void release(final AtomicInteger counter) { @Override public void close(final Runnable onClose) { + final Long count = doClose(onClose); + if (count != null && count > 0) { + throw new Env.EnvInUseException(count); + } + } + + @Override + public boolean tryClose(Runnable onClose) { + final Long count = doClose(onClose); + return count != null && count == 0; + } + + /** + * @return A non-zero count to indicate the resource is in use. A zero count to indicate the + * onClose was called successfully. A null count to indicate the resource was already in a + * closed state. + */ + private Long doClose(final Runnable onClose) { Objects.requireNonNull(onClose); // close is idempotent so silently drop out if (isClosed.get()) { - return; + return null; } synchronized (this) { if (isClosed.get()) { - return; + return null; } // Once we have marked all counters as count-in-progress, any threads trying to mutate the @@ -179,9 +197,8 @@ public void close(final Runnable onClose) { for (final Stripe stripe : counters) { stripe.counter.set(MAGIC_CLOSED_VALUE); } - } else { - throw new Env.EnvInUseException(totalCount); } + return totalCount; } finally { if (!isClosed.get()) { // Return all counters to their original positive values so @@ -353,6 +370,16 @@ private int getStripeIdx() { return (int) ((threadId ^ (threadId >>> 31)) & stripeMask); } + private enum CloseOutcome { + /** Successfully closed. */ + CLOSED, + /** The resource is in use. */ + IN_USE, + /** Already in a closed state. */ + ALREADY_CLOSED, + ; + } + private enum Delta { PLUS_ONE(1), MINUS_ONE(-1), diff --git a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java index 420d281c..85158606 100644 --- a/src/main/java/org/lmdbjava/SynchronisedRefCounter.java +++ b/src/main/java/org/lmdbjava/SynchronisedRefCounter.java @@ -65,6 +65,21 @@ public void close(final Runnable onClose) { } } + @Override + public boolean tryClose(Runnable onClose) { + Objects.requireNonNull(onClose); + synchronized (this) { + if (!isClosed) { + if (counter == 0) { + isClosed = true; + onClose.run(); + return true; + } + } + } + return false; + } + private void release() { synchronized (this) { if (isClosed) { diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index d164dff9..11914c73 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -34,6 +34,7 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,6 +44,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -754,7 +756,6 @@ void closeWithOpenReadTxn() { .setMaxDbs(1) .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR) - .setSafeClose() .open(file); // Open but don't close @@ -767,7 +768,7 @@ void closeWithOpenReadTxn() { } @Test - void closeWithOpenWriteTxn() { + void tryCloseWithOpenReadTxn() { final Path file = tempDir.createTempFile(); final Env env = Env.create() @@ -776,7 +777,62 @@ void closeWithOpenWriteTxn() { .setMaxDbs(1) .setMaxReaders(1) .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + // Open but don't close + final Txn readTxn = env.txnWrite(); + + assertThat(env.tryClose()).isFalse(); + readTxn.close(); + assertThat(env.tryClose()).isTrue(); + // already closed + assertThat(env.tryClose()).isFalse(); + } + + @Test + void immediateClose() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create() .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + env.close(); + // no-op + env.close(); + } + + @Test + void immediateTryClose() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + + assertThat(env.tryClose()).isTrue(); + // already closed + assertThat(env.tryClose()).isFalse(); + } + + @Test + void closeWithOpenWriteTxn() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create() + .setSafeClose() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) .open(file); // Open but don't close @@ -877,6 +933,7 @@ void closeDuringConcurrentReads_isRejectedWhileReadersLiveAndSurvives() throws E try (Txn ignoredHeldReader = env.txnRead()) { Thread.sleep(100); // let the reader threads saturate the native read path Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + assertThat(env.tryClose()).isFalse(); assertThat(env.isClosed()).isFalse(); // must NOT have unmapped with live readers } @@ -946,6 +1003,7 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th try { Thread.sleep(100); Assertions.assertThatThrownBy(env::close).isInstanceOf(Env.EnvInUseException.class); + assertThat(env.tryClose()).isFalse(); assertThat(env.isClosed()).isFalse(); } finally { heldCursor.close(); @@ -964,6 +1022,72 @@ void closeDuringConcurrentCursorReads_isRejectedWhileCursorsLiveAndSurvives() th assertThat(env.isClosed()).isTrue(); } + @Test + void testEventualTryClose() throws InterruptedException { + final Path dir = tempDir.createTempDir(); + final Env env = + Env.create().setSafeClose().setMaxReaders(64).setMaxDbs(1).open(dir); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + for (int i = 0; i < 32; i++) { + db.put(bb(i), bb(i)); + } + + final int readerCount = 16; + final AtomicLong reads = new AtomicLong(); + final LongAdder completedCount = new LongAdder(); + final List unexpected = new CopyOnWriteArrayList<>(); + final List readers = new ArrayList<>(readerCount); + + final Instant endTime = Instant.now().plusMillis(500); + + // Have 16 threads all hammer the env with reads until timeout + for (int i = 0; i < readerCount; i++) { + final Thread reader = + new Thread( + () -> { + while (Instant.now().isBefore(endTime)) { + try (Txn txn = env.txnRead(); + Cursor cursor = db.openCursor(txn)) { + cursor.first(); + reads.incrementAndGet(); + } catch (final AlreadyClosedException expected) { + return; // benign: env is closing/closed + } catch (final Throwable t) { + unexpected.add(t); + return; + } + } + completedCount.increment(); + }, + "cursor-reader-" + i); + reader.setDaemon(true); + reader.start(); + readers.add(reader); + } + + // Keep trying to close until we are able + boolean didClose = false; + while (!didClose) { + Thread.sleep(10); + didClose = env.tryClose(); + if (didClose) { + assertThat(completedCount).hasValue(readerCount); + } + } + + // readers should all have completed by now anyway + for (final Thread reader : readers) { + reader.join(5_000); + } + + assertThat(env.tryClose()).isFalse(); + assertThat(reads.get()).isGreaterThan(0L); + assertThat(unexpected).isEmpty(); + assertThat(completedCount).hasValue(readerCount); + assertThat(env.isClosed()).isTrue(); + } + @ParameterizedTest @CsvSource({ "TRUE, TRUE", diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index fe9b25f2..bb01e939 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -150,7 +150,7 @@ public void perfTest() { @ParameterizedTest @MethodSource("allRefCounterProvider") - void testRefCounters(final RefCounter refCounter) { + void testRefCounters_close(final RefCounter refCounter) { // Acquire twice final RefCounter.RefCounterReleaser releaser1 = refCounter.acquire(); assertRefCount(refCounter, 1); @@ -161,6 +161,7 @@ void testRefCounters(final RefCounter refCounter) { if (!(refCounter instanceof NoOpRefCounter)) { // Close() not called as ref count is two. + Assertions.assertThatThrownBy( () -> { refCounter.close(onCloseCallCount::incrementAndGet); @@ -206,6 +207,54 @@ void testRefCounters(final RefCounter refCounter) { assertThat(onCloseCallCount).hasValue(1); } + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void testRefCounters_tryClose(final RefCounter refCounter) { + // Acquire twice + final RefCounter.RefCounterReleaser releaser1 = refCounter.acquire(); + assertRefCount(refCounter, 1); + final RefCounter.RefCounterReleaser releaser2 = refCounter.acquire(); + assertRefCount(refCounter, 2); + + final AtomicInteger onCloseCallCount = new AtomicInteger(); + + if (!(refCounter instanceof NoOpRefCounter)) { + // Close() not called as ref count is two. + assertThat(refCounter.tryClose(onCloseCallCount::incrementAndGet)).isFalse(); + } + assertThat(onCloseCallCount).hasValue(0); + + // Release 1st releaser + releaser1.release(); + assertRefCount(refCounter, 1); + + if (!(refCounter instanceof NoOpRefCounter)) { + // Close() not called as ref count is one. + assertThat(refCounter.tryClose(onCloseCallCount::incrementAndGet)).isFalse(); + } + assertThat(onCloseCallCount).hasValue(0); + + // Release 2nd releaser + releaser2.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // no-op if already released + releaser1.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // no-op if already released + releaser2.release(); + assertThat(refCounter.getCount()).isEqualTo(0); + + // onClose is called now + assertThat(refCounter.tryClose(onCloseCallCount::incrementAndGet)).isTrue(); + assertThat(onCloseCallCount).hasValue(1); + + // no-op as onClose already called + assertThat(refCounter.tryClose(onCloseCallCount::incrementAndGet)).isFalse(); + assertThat(onCloseCallCount).hasValue(1); + } + @ParameterizedTest @MethodSource("multiThreadedRefCounterProvider") void multipleThreads(final RefCounter refCounter) { diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 0750b3e8..1dec78ec 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -181,6 +181,16 @@ void testCheckNotCommitted() { .isInstanceOf(NotReadyException.class); } + @Test + void testDoubleCommit() { + try (Txn txn = env.txnRead()) { + txn.commit(); + assertState(txn, DONE); + assertThatThrownBy(txn::commit).isInstanceOf(NotReadyException.class); + assertState(txn, DONE); + } + } + @Test void testCheckReadOnly() { assertThatThrownBy( @@ -223,6 +233,14 @@ void testGetId() { assertThat(txId1.get()).isNotEqualTo(txId2.get()); } + @Test + void txIdDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + txnRead.close(); + env.close(); + assertThatThrownBy(txnRead::getId).isInstanceOf(AlreadyClosedException.class); + } + @Test void txCanCommitThenCloseWithoutError() { try (Txn txn = env.txnRead()) { @@ -265,18 +283,20 @@ void txRenewDeniedIfEnvClosed() { assertThatThrownBy(txnRead::renew).isInstanceOf(AlreadyClosedException.class); } - @Disabled // We shouldn't be trying to close the env with open txns @Test void txCloseDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); + // We can't test closing the env with the txn open as the env will prevent it + txnRead.close(); env.close(); assertThatThrownBy(txnRead::close).isInstanceOf(AlreadyClosedException.class); } - @Disabled // We shouldn't be trying to close the env with open txns @Test void txCommitDeniedIfEnvClosed() { final Txn txnRead = env.txnRead(); + // We can't test closing the env with the txn open as the env will prevent it + txnRead.close(); env.close(); assertThatThrownBy(txnRead::commit).isInstanceOf(AlreadyClosedException.class); } From a9228db707977093e24cce4039391b716ac683c6 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:13:40 +0100 Subject: [PATCH 49/61] gh-279 Add test for Cursor ctor exception --- src/test/java/org/lmdbjava/CursorTest.java | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index d37234c8..1cdcce04 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -26,10 +26,12 @@ import static org.lmdbjava.DbiFlags.MDB_DUPSORT; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.Library.LIB; import static org.lmdbjava.PutFlags.MDB_APPENDDUP; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; +import static org.lmdbjava.ResultCodeMapper.checkRc; import static org.lmdbjava.SeekOp.MDB_FIRST; import static org.lmdbjava.SeekOp.MDB_GET_BOTH; import static org.lmdbjava.SeekOp.MDB_LAST; @@ -40,6 +42,7 @@ import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.function.Consumer; +import jnr.ffi.byref.PointerByReference; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -47,6 +50,7 @@ import org.junit.jupiter.api.Test; import org.lmdbjava.Cursor.ClosedException; import org.lmdbjava.Txn.ReadOnlyRequiredException; +import org.mockito.Mockito; /** Test {@link Cursor}. */ public final class CursorTest { @@ -599,6 +603,25 @@ void testCursorByteBufferDuplicate() { } } + @Test + void testCursorConstructorFailure() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Txn txn = env.txnWrite()) { + + // These two lines do what Dbi.openCursor does before calling the Cursor constructor + final PointerByReference cursorPtr = new PointerByReference(); + checkRc(LIB.mdb_cursor_open(txn.pointer(), db.pointer(), cursorPtr)); + + //noinspection unchecked,resource + final Txn mockTxn = (Txn) Mockito.mock(Txn.class); + Mockito.when(mockTxn.newKeyVal()).thenThrow(new RuntimeException("newKeyVal error")); + assertThatThrownBy(() -> new Cursor<>(cursorPtr.getValue(), mockTxn, env)) + .isInstanceOf(RuntimeException.class) + .hasMessage("newKeyVal error"); + } + } + private void doEnvClosedTest( final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { From 2ce1c760d8c582a49e8ca6aabd7d473a5d7955b2 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:23:22 +0100 Subject: [PATCH 50/61] gh-279 Remove dead code, add test for code coverage --- src/main/java/org/lmdbjava/Env.java | 5 ----- src/test/java/org/lmdbjava/EnvTest.java | 9 +++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index bbadc77a..65e57bcc 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -777,11 +777,6 @@ public static final class EnvInUseException extends LmdbException { private static final long serialVersionUID = 1L; - /** Creates a new instance. */ - public EnvInUseException() { - super("Environment has open transactions/cursors so cannot be closed."); - } - /** * Creates a new instance. * diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 11914c73..d8de3181 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -1101,6 +1101,15 @@ void singleThreaded(final BooleanArg safeClose, final BooleanArg singleThreaded) testEnvUse(safeClose, singleThreaded); } + @Test + void testToString() { + final Path dir = tempDir.createTempDir(); + try (Env env = + Env.create().setSafeClose().setMaxReaders(64).setMaxDbs(1).open(dir)) { + assertThat(env.toString()).doesNotStartWith("@"); + } + } + private void testEnvUse(final BooleanArg safeClose, final BooleanArg singleThreaded) { final Path file = tempDir.createTempFile(); From 8aa99a1ab895dfc706d9a6404a386aa18a95059c Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:37:48 +0100 Subject: [PATCH 51/61] gh-279 Improve Env.copy tests --- src/test/java/org/lmdbjava/EnvTest.java | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index d8de3181..edaa30a6 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -285,10 +286,58 @@ void copyFileBased() { final Path dest = tempDir.createTempFile(); assertThat(Files.exists(dest)).isFalse(); final Path src = tempDir.createTempFile(); + + // Create the source env and put an entry try (Env env = Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + final Dbi rwDb = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + rwDb.put(bb(1), bb(42)); + + env.copy(dest, MDB_CP_COMPACT); + } + + // Check the destination env and get the entry + try (Env env = + Env.create().setSafeClose().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(dest)) { + final Dbi dbi = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Txn txn = env.txnRead()) { + final ByteBuffer byteBuffer = dbi.get(txn, bb(1)); + assertThat(byteBuffer).isNotNull(); + assertThat(byteBuffer.getInt()).isEqualTo(42); + } + } + assertThat(FileUtil.size(dest)).isGreaterThan(0L); + } + + @Test + void copyDirBased() { + final Path dest = tempDir.createTempDir(); + assertThat(isEmptyDir(dest)).isTrue(); + final Path src = tempDir.createTempDir(); + assertThat(isEmptyDir(src)).isTrue(); + // Create the source env and put an entry + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(src)) { + final Dbi rwDb = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + rwDb.put(bb(1), bb(42)); + env.copy(dest, MDB_CP_COMPACT); } + + // Check the destination env and get the entry + try (Env env = Env.create().setSafeClose().setMaxReaders(1).open(dest)) { + final Dbi dbi = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Txn txn = env.txnRead()) { + final ByteBuffer byteBuffer = dbi.get(txn, bb(1)); + assertThat(byteBuffer).isNotNull(); + assertThat(byteBuffer.getInt()).isEqualTo(42); + } + } + assertThat(isEmptyDir(dest)).isFalse(); + assertThat(isEmptyDir(src)).isFalse(); assertThat(FileUtil.size(dest)).isGreaterThan(0L); } @@ -1110,6 +1159,14 @@ void testToString() { } } + private boolean isEmptyDir(final Path dir) { + try (Stream pathStream = Files.list(dir)) { + return !pathStream.findAny().isPresent(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private void testEnvUse(final BooleanArg safeClose, final BooleanArg singleThreaded) { final Path file = tempDir.createTempFile(); From ecf7251ab83af83f15ec7fdddacb707f28391bae Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:53:12 +0100 Subject: [PATCH 52/61] Add test for KeyVal.close --- src/test/java/org/lmdbjava/KeyValTest.java | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/test/java/org/lmdbjava/KeyValTest.java diff --git a/src/test/java/org/lmdbjava/KeyValTest.java b/src/test/java/org/lmdbjava/KeyValTest.java new file mode 100644 index 00000000..af61c12a --- /dev/null +++ b/src/test/java/org/lmdbjava/KeyValTest.java @@ -0,0 +1,27 @@ +package org.lmdbjava; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.ByteBuffer; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class KeyValTest { + + @Test + void testClose() { + //noinspection unchecked + final BufferProxy mockBufferProxy = + (BufferProxy) Mockito.mock(BufferProxy.class); + final KeyVal keyVal = new KeyVal<>(mockBufferProxy); + + keyVal.close(); + + Mockito.verify(mockBufferProxy, Mockito.times(2)).deallocate(Mockito.any()); + + // Already closed, a no-op + keyVal.close(); + + Mockito.verify(mockBufferProxy, Mockito.times(2)).deallocate(Mockito.any()); + } +} From 7565c94ff915c7729aae94710c212a73e79aa93a Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:22:43 +0100 Subject: [PATCH 53/61] gh-279 Add RefCounter.run test --- src/main/java/org/lmdbjava/NoOpRefCounter.java | 4 +++- src/main/java/org/lmdbjava/RefCounter.java | 4 +++- .../org/lmdbjava/SingleThreadedRefCounter.java | 12 +++++++----- .../java/org/lmdbjava/AbstractFlagSetTest.java | 2 ++ src/test/java/org/lmdbjava/RefCounterTest.java | 14 ++++++++++++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/lmdbjava/NoOpRefCounter.java b/src/main/java/org/lmdbjava/NoOpRefCounter.java index 5f1fcdda..e82521c3 100644 --- a/src/main/java/org/lmdbjava/NoOpRefCounter.java +++ b/src/main/java/org/lmdbjava/NoOpRefCounter.java @@ -38,7 +38,9 @@ public RefCounterReleaser acquire() { @Override public void use(final Runnable runnable) { - runnable.run(); + if (runnable != null) { + runnable.run(); + } } @Override diff --git a/src/main/java/org/lmdbjava/RefCounter.java b/src/main/java/org/lmdbjava/RefCounter.java index 0887cc2a..1118451f 100644 --- a/src/main/java/org/lmdbjava/RefCounter.java +++ b/src/main/java/org/lmdbjava/RefCounter.java @@ -30,7 +30,9 @@ interface RefCounter { /** * Calls {@link RefCounter#acquire()}, runs runnable, then calls {@link - * RefCounterReleaser#release()} + * RefCounterReleaser#release()}. + * + *

        If runnable is null, this is a no-op. */ default void use(final Runnable runnable) { if (runnable != null) { diff --git a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java index 572e14ed..4e293f3f 100644 --- a/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java +++ b/src/main/java/org/lmdbjava/SingleThreadedRefCounter.java @@ -43,11 +43,13 @@ private void release() { @Override public void use(Runnable runnable) { - final RefCounterReleaser releaser = acquire(); - try { - runnable.run(); - } finally { - releaser.release(); + if (runnable != null) { + final RefCounterReleaser releaser = acquire(); + try { + runnable.run(); + } finally { + releaser.release(); + } } } diff --git a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java index a886d9e1..18a52cdf 100644 --- a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java +++ b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java @@ -67,6 +67,8 @@ void testSingleFlagSet() { final List allFlags = getAllFlags(); for (T flag : allFlags) { final F flagSet = getBuilder().addFlag(flag).build(); + assertThat(FlagSet.equals(flagSet, flag)).isTrue(); + assertThat(FlagSet.equals(flagSet, flag.getFlags())).isTrue(); assertThat(flagSet.getMask()).isEqualTo(flag.getMask()); assertThat(flagSet.getMask()).isEqualTo(MaskedFlag.mask(flag)); assertThat(flagSet.getFlags()).containsExactly(flag); diff --git a/src/test/java/org/lmdbjava/RefCounterTest.java b/src/test/java/org/lmdbjava/RefCounterTest.java index bb01e939..12ddceca 100644 --- a/src/test/java/org/lmdbjava/RefCounterTest.java +++ b/src/test/java/org/lmdbjava/RefCounterTest.java @@ -634,6 +634,20 @@ void countAfterClose(final RefCounter refCounter) { assertThat(refCounter.getCount()).isZero(); } + @ParameterizedTest + @MethodSource("allRefCounterProvider") + void use_null(final RefCounter refCounter) { + // A no-op + refCounter.use(null); + + final AtomicInteger onCloseCallCount = new AtomicInteger(); + refCounter.close(onCloseCallCount::incrementAndGet); + assertThat(onCloseCallCount.get()).isEqualTo(1); + + // A no-op + refCounter.use(null); + } + @ParameterizedTest @MethodSource("allRefCounterProvider") void use(final RefCounter refCounter) { From b486b76f0d2bdd6847cdac5672d1d5f13910280e Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:43:00 +0100 Subject: [PATCH 54/61] gh-279 Fix AbstractFlagSetTest --- src/main/java/org/lmdbjava/AbstractFlagSet.java | 14 +++++++++++++- .../java/org/lmdbjava/AbstractFlagSetTest.java | 10 ++++++++-- src/test/java/org/lmdbjava/KeyValTest.java | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/lmdbjava/AbstractFlagSet.java b/src/main/java/org/lmdbjava/AbstractFlagSet.java index 2e917515..9eac7ac6 100644 --- a/src/main/java/org/lmdbjava/AbstractFlagSet.java +++ b/src/main/java/org/lmdbjava/AbstractFlagSet.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,6 +85,18 @@ public String toString() { return FlagSet.asString(this); } + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) return false; + AbstractFlagSet that = (AbstractFlagSet) object; + return mask == that.mask && Objects.equals(flags, that.flags); + } + + @Override + public int hashCode() { + return Objects.hash(flags, mask); + } + static class AbstractEmptyFlagSet implements FlagSet { @Override diff --git a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java index 18a52cdf..743f7079 100644 --- a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java +++ b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,8 +67,14 @@ void testSingleFlagSet() { final List allFlags = getAllFlags(); for (T flag : allFlags) { final F flagSet = getBuilder().addFlag(flag).build(); + + // Compare as a Set + assertThat(flagSet.getFlags()).isEqualTo(flag.getFlags()); + // Compare as a FlagSet + assertThat(flagSet).isEqualTo(flag); + assertThat(FlagSet.equals(flagSet, flag)).isTrue(); - assertThat(FlagSet.equals(flagSet, flag.getFlags())).isTrue(); + assertThat(flagSet.getMask()).isEqualTo(flag.getMask()); assertThat(flagSet.getMask()).isEqualTo(MaskedFlag.mask(flag)); assertThat(flagSet.getFlags()).containsExactly(flag); diff --git a/src/test/java/org/lmdbjava/KeyValTest.java b/src/test/java/org/lmdbjava/KeyValTest.java index af61c12a..6dd3b3f9 100644 --- a/src/test/java/org/lmdbjava/KeyValTest.java +++ b/src/test/java/org/lmdbjava/KeyValTest.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2016-2026 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.lmdbjava; import static org.junit.jupiter.api.Assertions.*; From c3a05d7fa2758490a24cad582020ea2ca441d0e5 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:11:46 +0100 Subject: [PATCH 55/61] gh-279 Changes to AbstractFlagSetTest for codecov --- .../java/org/lmdbjava/AbstractFlagSetTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java index 743f7079..22aae9fb 100644 --- a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java +++ b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java @@ -70,10 +70,11 @@ void testSingleFlagSet() { // Compare as a Set assertThat(flagSet.getFlags()).isEqualTo(flag.getFlags()); - // Compare as a FlagSet - assertThat(flagSet).isEqualTo(flag); - + // Compare as a FlagSet because a single flag enum implements FlagSet + assertThat(flagSet.equals(flag)).isTrue(); + assertThat(flag.equals(flagSet)).isTrue(); assertThat(FlagSet.equals(flagSet, flag)).isTrue(); + assertThat(flagSet.hashCode()).isEqualTo(flag.hashCode()); assertThat(flagSet.getMask()).isEqualTo(flag.getMask()); assertThat(flagSet.getMask()).isEqualTo(MaskedFlag.mask(flag)); @@ -96,6 +97,14 @@ void testSingleFlagSet() { assertThat(flagSet.getMask()).isNotEqualTo(MaskedFlag.mask(getFirst())); assertThat(flagSet.getMaskWith(getFirst())).isEqualTo(MaskedFlag.mask(flag, getFirst())); } + // Here to help codecov pick up the toString() method + if (flagSet instanceof AbstractFlagSet) { + //noinspection unchecked + final AbstractFlagSet abstractFlagSet = (AbstractFlagSet) flagSet; + assertThat(abstractFlagSet.toString()) + .isNotNull() + .doesNotStartWith("@"); + } assertThat(flagSet.toString()).isNotNull(); assertThat(flag.name()).isNotNull(); assertThat(flag.isSet(flag)).isTrue(); From 9c7f97cdab749907529f95812caf2f880ca04785 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:12:09 +0100 Subject: [PATCH 56/61] gh-279 Format --- src/test/java/org/lmdbjava/AbstractFlagSetTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java index 22aae9fb..217217e7 100644 --- a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java +++ b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java @@ -101,9 +101,7 @@ void testSingleFlagSet() { if (flagSet instanceof AbstractFlagSet) { //noinspection unchecked final AbstractFlagSet abstractFlagSet = (AbstractFlagSet) flagSet; - assertThat(abstractFlagSet.toString()) - .isNotNull() - .doesNotStartWith("@"); + assertThat(abstractFlagSet.toString()).isNotNull().doesNotStartWith("@"); } assertThat(flagSet.toString()).isNotNull(); assertThat(flag.name()).isNotNull(); From bfdefef6032a5f778484de7ca7f5aeb918a5083a Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:43:14 +0100 Subject: [PATCH 57/61] gh-279 Add tests to TargetNameTest --- .../java/org/lmdbjava/TargetNameTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/org/lmdbjava/TargetNameTest.java b/src/test/java/org/lmdbjava/TargetNameTest.java index 6c0a8f44..52762af1 100644 --- a/src/test/java/org/lmdbjava/TargetNameTest.java +++ b/src/test/java/org/lmdbjava/TargetNameTest.java @@ -21,6 +21,7 @@ import static org.lmdbjava.TargetName.resolveFilename; import static org.lmdbjava.TestUtils.invokePrivateConstructor; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; /** Test {@link TargetName}. */ @@ -66,6 +67,36 @@ void externalTakesPriority() { assertThat(isExternal("/lm.so")).isTrue(); } + @Test + void resolveExtension_null() { + assertThat(TargetName.resolveExtension(null)).isEqualTo("so"); + } + + @Test + void resolveExtension_unknown() { + assertThat(TargetName.resolveExtension("foo")).isEqualTo("so"); + } + + @Test + void badArch() { + Assertions.assertThatThrownBy( + () -> { + TargetName.resolveFilename(NONE, NONE, "badArch", "Linux"); + }) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("os.arch"); + } + + @Test + void badOs() { + Assertions.assertThatThrownBy( + () -> { + TargetName.resolveFilename(NONE, NONE, "arch", "badOs"); + }) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("os.name"); + } + private void embed(final String lib, final String arch, final String os) { assertThat(resolveFilename(NONE, NONE, arch, os)).isEqualTo("org/lmdbjava/native/" + lib); assertThat(isExternal(NONE)).isFalse(); From eefc87f459cbeb010c9e4d1ad8cb6d8a4780d425 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:21:50 +0100 Subject: [PATCH 58/61] gh-279 Change safeClose to only track RW cursors Add more cursor tests, improve javadoc --- src/main/java/org/lmdbjava/Cursor.java | 56 ++++-- src/test/java/org/lmdbjava/CursorTest.java | 201 ++++++++++++++++++--- src/test/java/org/lmdbjava/TestUtils.java | 23 +++ 3 files changed, 244 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index 7d89a087..6bb356f8 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -35,18 +35,33 @@ import jnr.ffi.byref.NativeLongByReference; /** - * A cursor handle. + * A cursor handle for iterating through key/value pairs in an LMDB database. + * + *

        A cursor belongs to a {@link Txn}. + * + *

        If {@link Txn} is a read-write transaction, LMDB will automatically close the cursor handle + * when the {@link Txn} is committed or aborted, meaning that Cursor#close() does not need to be + * called, however, if it is called, it must be called before the {@link Txn} is committed/aborted. + * + *

        NOTE: If {@link Env.Builder#setSafeClose()} is set, the {@link Env} requires that all cursors + * are closed before the {@link Env} is closed, therefore it is good practice to explicitly call + * {@link Cursor#close()} or use a try-with-resources block on all types of cursor. + * + *

        If {@link Txn} is a read-only transaction, {@link Cursor#close()} must be called to free up + * the cursor handle. This can be called at any time. Read-only transactions can 'moved' to a + * different transaction using the {@link Cursor#renew(Txn)} method. This can also be done at any + * time. * * @param buffer type */ public final class Cursor implements AutoCloseable { - private AtomicBoolean closed; + private final AtomicBoolean closed; private final KeyVal kv; private final Pointer ptrCursor; - private Txn txn; private final Env env; private final RefCounter.RefCounterReleaser refCounterReleaser; + private volatile Txn txn; Cursor(final Pointer ptr, final Txn txn, final Env env) { requireNonNull(ptr); @@ -54,17 +69,21 @@ public final class Cursor implements AutoCloseable { requireNonNull(env); this.ptrCursor = ptr; this.txn = txn; - // The env needs to track open cursors to prevent env closure before the cursors are closed - this.refCounterReleaser = env.acquire(); + // The env needs to track open RW cursors to prevent env closure before the cursors are closed. + // We don't care about RO cursors as LMDB will automatically free them. + refCounterReleaser = txn.isWritable() ? env.acquire() : null; this.env = env; this.closed = new AtomicBoolean(false); try { this.kv = txn.newKeyVal(); } catch (final Exception e) { closed.set(true); - this.refCounterReleaser.release(); + releaseRefCount(); + // Clean up the native cursor - LIB.mdb_cursor_close(ptrCursor); + if (txn.isReadOnly() || txn.isReady()) { + LIB.mdb_cursor_close(ptrCursor); + } throw e; } } @@ -73,7 +92,7 @@ public final class Cursor implements AutoCloseable { * Close a cursor handle. * *

        The cursor handle will be freed and must not be used again after this call. Its transaction - * must still be live (i.e. not committed) if it is a write-transaction. + * must still be live (i.e. not committed or aborted) if it is a write-transaction. */ @Override public void close() { @@ -93,17 +112,17 @@ public void close() { } } LIB.mdb_cursor_close(ptrCursor); - refCounterReleaser.release(); + releaseRefCount(); } } /** - * Return count of duplicates for current key. + * Return count of duplicates for the current key. * *

        This call is only valid on databases that support sorted duplicate data items {@link * DbiFlags#MDB_DUPSORT}. * - * @return count of duplicates for current key + * @return count of duplicates for the current key */ public long count() { if (SHOULD_CHECK) { @@ -388,7 +407,6 @@ public void putMultiple(final T key, final T val, final int elements) { */ public void putMultiple(final T key, final T val, final int elements, final PutFlagSet flags) { if (SHOULD_CHECK) { - requireNonNull(txn); requireNonNull(key); requireNonNull(val); env.checkNotClosed(); @@ -417,19 +435,20 @@ public void putMultiple(final T key, final T val, final int elements, final PutF * may be associated with a new read-only transaction, and referencing the same database handle as * it was created with. This may be done whether the previous transaction is live or dead. * - * @param newTxn transaction handle + * @param newTxn The new transaction handle to associate with this cursor. It must be a read-only + * transaction and in a ready state, i.e. not committed/aborted/closed. */ public void renew(final Txn newTxn) { if (SHOULD_CHECK) { requireNonNull(newTxn); env.checkNotClosed(); checkNotClosed(); - this.txn.checkReadOnly(); // existing + txn.checkReadOnly(); // existing newTxn.checkReadOnly(); newTxn.checkReady(); } checkRc(LIB.mdb_cursor_renew(newTxn.pointer(), ptrCursor)); - this.txn = newTxn; + txn = newTxn; } /** @@ -543,6 +562,13 @@ private void checkNotClosed() { } } + private void releaseRefCount() { + // May be null if the cursor was created with a read-only transaction + if (refCounterReleaser != null) { + refCounterReleaser.release(); + } + } + /** Cursor has already been closed. */ public static final class ClosedException extends LmdbException { diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 1cdcce04..2be3db66 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -26,6 +26,7 @@ import static org.lmdbjava.DbiFlags.MDB_DUPSORT; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.EnvFlags.MDB_NOTLS; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.PutFlags.MDB_APPENDDUP; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; @@ -38,9 +39,13 @@ import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.getEntryCount; +import static org.lmdbjava.TestUtils.getInt; import java.nio.ByteBuffer; import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiConsumer; import java.util.function.Consumer; import jnr.ffi.byref.PointerByReference; import org.assertj.core.api.Assertions; @@ -57,20 +62,25 @@ public final class CursorTest { private Env env; private TempDir tempDir; + private Path envFile; @BeforeEach void beforeEach() { tempDir = new TempDir(); - Path file = tempDir.createTempFile(); + envFile = tempDir.createTempFile(); + openEnv(); + } + + private void openEnv() { env = create(PROXY_OPTIMAL) .setSafeClose() .setMapSize(1, ByteUnit.MEBIBYTES) - .setMaxReaders(1) + .setMaxReaders(2) .setMaxDbs(1) - .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) .setSafeClose() - .open(file); + .open(envFile); } @AfterEach @@ -487,23 +497,16 @@ void renewTxRo() { @Test void renewTxRw() { - assertThatThrownBy( - () -> { - final Dbi db = - env.createDbi() - .setDbName(DB_1) - .withDefaultComparator() - .setDbiFlags(MDB_CREATE) - .open(); - try (Txn txn = env.txnWrite()) { - assertThat(txn.isReadOnly()).isFalse(); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); - try (Cursor c = db.openCursor(txn)) { - c.renew(txn); - } - } - }) - .isInstanceOf(ReadOnlyRequiredException.class); + try (Txn txn = env.txnWrite()) { + assertThat(txn.isReadOnly()).isFalse(); + + try (Cursor c = db.openCursor(txn)) { + assertThatThrownBy(() -> c.renew(txn)).isInstanceOf(ReadOnlyRequiredException.class); + } + } } @Test @@ -622,6 +625,137 @@ void testCursorConstructorFailure() { } } + @Test + void testMultipleROCursorsOneTxn() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + db.put(bb(1), bb(10)); + db.put(bb(2), bb(20)); + db.put(bb(2), bb(30)); + db.put(bb(4), bb(40)); + + try (Txn readTxn1 = env.txnRead()) { + + try (Cursor cursor1 = db.openCursor(readTxn1); + Cursor cursor2 = db.openCursor(readTxn1)) { + + // Two independent cursors at different positions + cursor1.seek(MDB_FIRST); + cursor2.seek(MDB_LAST); + + assertThat(cursor1.key()).isEqualTo(bb(1)); + assertThat(cursor2.key()).isEqualTo(bb(4)); + + cursor1.seek(MDB_LAST); + cursor2.seek(MDB_FIRST); + + assertThat(cursor1.key()).isEqualTo(bb(4)); + assertThat(cursor2.key()).isEqualTo(bb(1)); + } + } + } + + @Test + void testMultipleRWCursorsOneTxn() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + assertThat(getEntryCount(db, env)).isEqualTo(0); + + db.put(bb(1), bb(10)); + db.put(bb(2), bb(20)); + db.put(bb(3), bb(30)); + db.put(bb(4), bb(40)); + + assertThat(getEntryCount(db, env)).isEqualTo(4); + + try (final Txn writeTxn = env.txnWrite()) { + + assertThat(getEntryCount(db, writeTxn)).isEqualTo(4); + + try (final Cursor cursor1 = db.openCursor(writeTxn); + final Cursor cursor2 = db.openCursor(writeTxn)) { + + // Two independent cursors at different positions + cursor1.seek(MDB_FIRST); + cursor2.seek(MDB_LAST); + + assertThat(getInt(cursor1.key())).isEqualTo(1); + assertThat(getInt(cursor2.key())).isEqualTo(4); + + cursor1.delete(); + cursor1.seek(MDB_FIRST); + assertThat(getInt(cursor1.key())).isEqualTo(2); + assertThat(getInt(cursor2.key())).isEqualTo(4); + + cursor2.delete(); + cursor2.seek(MDB_LAST); + assertThat(getInt(cursor2.key())).isEqualTo(3); + + assertThat(getEntryCount(db, writeTxn)).isEqualTo(2); + + // This uses a separate read txn so can't sse the deletes + assertThat(getEntryCount(db, env)).isEqualTo(4); + } + } + } + + @Test + void testNonReadyTxnRejectsLast() { + doNonReadyTxnTest(Cursor::last); + } + + @Test + void testNonReadyTxnRejectsNext() { + doNonReadyTxnTest(Cursor::next); + } + + @Test + void testNonReadyTxnRejectsPrev() { + doNonReadyTxnTest(Cursor::prev); + } + + @Test + void testNonReadyTxnRejectsPut() { + doNonReadyTxnTest(c -> c.put(bb(5), bb(6))); + } + + @Test + void testNonReadyTxnRejectsPutMultiple() { + doNonReadyTxnTest(c -> c.putMultiple(bb(5), bb(6), 1, MDB_MULTIPLE)); + } + + @Test + void testNonReadyTxnRejectsSeek() { + doNonReadyTxnTest(c -> c.seek(MDB_FIRST)); + } + + private void doNonReadyTxnTest(final Consumer> work) { + doCursorTest( + true, + (txn, c) -> { + txn.abort(); + assertThatThrownBy(() -> work.accept(c)).isInstanceOf(Txn.NotReadyException.class); + c.close(); + }); + openEnv(); + doCursorTest( + true, + (txn, c) -> { + txn.close(); + assertThatThrownBy(() -> work.accept(c)).isInstanceOf(Txn.NotReadyException.class); + c.close(); + }); + openEnv(); + doCursorTest( + true, + (txn, c) -> { + txn.abort(); + assertThatThrownBy(() -> work.accept(c)).isInstanceOf(Txn.NotReadyException.class); + c.close(); + }); + } + private void doEnvClosedTest( final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { @@ -630,7 +764,7 @@ private void doEnvClosedTest( db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); - db.put(bb(2), bb(30)); + db.put(bb(3), bb(30)); db.put(bb(4), bb(40)); try (Txn txn = env.txnWrite()) { @@ -648,4 +782,29 @@ private void doEnvClosedTest( } } } + + private void doCursorTest( + final boolean readOnly, final BiConsumer, Cursor> work) { + Objects.requireNonNull(work); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + db.put(bb(1), bb(10)); + db.put(bb(2), bb(20)); + db.put(bb(3), bb(30)); + db.put(bb(4), bb(40)); + + final TxnFlagSet txnFlagSet = readOnly ? TxnFlags.MDB_RDONLY_TXN : TxnFlagSet.EMPTY; + + try (Txn txn = env.txn(null, txnFlagSet)) { + Cursor c = db.openCursor(txn); + try { + work.accept(txn, c); + } finally { + if (txn.isReadOnly() || txn.isReady()) { + c.close(); + } + } + } + } } diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index da7908f0..0900a64b 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -85,6 +85,12 @@ static ByteBuffer bbNative(final long value) { return bb; } + static int getInt(final ByteBuffer bb) { + final int val = bb.getInt(); + bb.rewind(); + return val; + } + static int getNativeInt(final ByteBuffer bb) { final int val = bb.order(ByteOrder.nativeOrder()).getInt(); bb.rewind(); @@ -223,4 +229,21 @@ public static void sleep(final int millis) { throw new RuntimeException(e); } } + + public static int getEntryCount(final Dbi dbi, final Env env) { + try (final Txn readTxn = env.txnRead()) { + return getEntryCount(dbi, readTxn); + } + } + + public static int getEntryCount(final Dbi dbi, final Txn txn) { + int count = 0; + try (CursorIterable cursorIterable = dbi.iterate(txn)) { + + for (final CursorIterable.KeyVal kv : cursorIterable) { + count++; + } + } + return count; + } } From 017ef34ed308c48463e8b4d7d9cb2696bd2f6e81 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:23:18 +0100 Subject: [PATCH 59/61] gh-279 Add more tests, improve javadoc --- src/main/java/org/lmdbjava/Dbi.java | 5 +- src/main/java/org/lmdbjava/Env.java | 97 ++++++++++--- src/main/java/org/lmdbjava/Txn.java | 40 +++++- src/test/java/org/lmdbjava/EnvTest.java | 31 ++++- .../java/org/lmdbjava/TargetNameTest.java | 3 +- src/test/java/org/lmdbjava/TxnTest.java | 129 ++++++++++++++++++ 6 files changed, 275 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index d2afdf8f..9532af42 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -408,6 +408,9 @@ public Cursor openCursor(final Txn txn) { /** * Starts a new read-write transaction and puts the key/data pair. * + *

        NOTE: If this is called while this thread already has an open write transaction, it will + * block indefinitely. + * * @param key key to store in the database (not null) * @param val value to store in the database (not null) * @see #put(Txn, Object, Object, PutFlagSet) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 65e57bcc..edae4a97 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -45,7 +45,26 @@ import org.lmdbjava.Library.MDB_stat; /** - * LMDB environment. + * An LMDB environment that includes one or more databases ({@link Dbi}s). The {@link Env} manages + * the transactions and databases. An {@link Env} can only have one concurrent write transaction but + * supports multiple concurrent read transactions. + * + *

        WARNING: LMDBJava's and LMDB's performance comes from their low-level memory + * access, but this requires that you strictly adhere to the various contracts set out when using + * environments, databases, transactions, and cursors. Incorrect use of LMDBJava can lead to + * segmentation faults that can crash your application. + * + *

        By default, LMDBJava performs some checks, for example, checking that the {@link Env} is not + * closed when opening a transaction. It is possible, however, for race conditions to occur if you + * close the {@link Env} after one of these checks has been performed and before the transaction is + * opened. Note, these checks can be disabled by setting the {@link #DISABLE_CHECKS_PROP} system + * property to {@code true}. This may be beneficial in performance-critical applications. + * + *

        {@link Builder#setSafeClose()} can also be used to add additional checks that ensure the + * {@link Env} is not closed while transactions/cursors are in use. + * + *

        It is the responsibility of the user to ensure that the {@link Env} is not closed while + * transactions or cursors are in use. * * @param buffer type */ @@ -79,6 +98,7 @@ public final class Env implements AutoCloseable { /** True if this Env has been created on the basis of only ever being used by a single thread. */ private final boolean isSingleThreaded; + /** If true, close will be prevented if there are open txns/cursors. */ private final boolean safeClose; private Env( @@ -450,10 +470,11 @@ boolean isSafeClose() { } /** - * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. + * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. This + * method is used for both opening an existing database or creating a new one. * - *

        The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if you need to create a - * new database before opening it. + *

        The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if the database does not + * already exist, and you need to create it before opening it. * * @return A new builder instance for creating/opening a {@link Dbi}. */ @@ -641,6 +662,9 @@ public void sync(final boolean force) { * @return a transaction (never null) * @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)} *

        Obtain a transaction with the requested parent and flags. + *

        Must not race a concurrent {@link #close()} on another thread: the closed-check and the + * native transaction start are not atomic, so a close occurring between them can crash the + * JVM (see {@link #close()}). */ @Deprecated public Txn txn(final Txn parent, final TxnFlags... flags) { @@ -649,9 +673,18 @@ public Txn txn(final Txn parent, final TxnFlags... flags) { } /** - * Obtain a transaction with the requested parent and flags. + * Obtain a read-write transaction with the requested parent and flags. * - * @param parent parent transaction (may be null if no parent) + *

        Must not race a concurrent {@link #close()} on another thread: the closed-check and the + * native transaction start are not atomic, so a close occurring between them can crash the JVM + * (see {@link #close()}). + * + *

        When using a parent transaction, any committed changes will only be visible to the parent + * transaction and will only be fully committed to the {@link Dbi} if the root transaction is + * committed. Aborting this transaction will not roll back changes already made by the parent + * transaction. + * + * @param parent parent transaction (maybe null if no parent) * @return a transaction (never null) */ public Txn txn(final Txn parent) { @@ -662,11 +695,25 @@ public Txn txn(final Txn parent) { /** * Obtain a transaction with the requested parent and flags. * - * @param parent parent transaction (may be null if no parent) + *

        If you want a read-write transaction, you can instead call {@link #txn(Txn)}. To obtain a + * read-only transaction, ensure {@link TxnFlags#MDB_RDONLY_TXN} is present in the {@link + * TxnFlagSet}. + * + *

        Must not race a concurrent {@link #close()} on another thread: the closed-check and the + * native transaction start are not atomic, so a close occurring between them can crash the JVM + * (see {@link #close()}). + * + *

        When using a parent transaction, any committed changes will only be visible to the parent + * transaction and will only be fully committed to the {@link Dbi} if the root transaction is + * committed. Aborting this transaction will not roll back changes already made by the parent + * transaction. + * + * @param parent parent transaction (maybe null if no parent) * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags - * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} + * is used frequently, it is recommended to hold a static instance of the {@link TxnFlagSet} * for re-use. * @return a transaction (never null) + * @throws Env.AlreadyClosedException if this environment has already been closed. */ public Txn txn(final Txn parent, final TxnFlagSet flags) { checkNotClosed(); @@ -676,7 +723,12 @@ public Txn txn(final Txn parent, final TxnFlagSet flags) { /** * Obtain a read-only transaction. * + *

        Must not race a concurrent {@link #close()} on another thread: the closed-check and the + * native transaction start are not atomic, so a close occurring between them can crash the JVM + * (see {@link #close()}). + * * @return a read-only transaction + * @throws Env.AlreadyClosedException if this environment has already been closed. */ public Txn txnRead() { checkNotClosed(); @@ -686,7 +738,12 @@ public Txn txnRead() { /** * Obtain a read-write transaction. * + *

        Must not race a concurrent {@link #close()} on another thread: the closed-check and the + * native transaction start are not atomic, so a close occurring between them can crash the JVM + * (see {@link #close()}). + * * @return a read-write transaction + * @throws Env.AlreadyClosedException if this environment has already been closed */ public Txn txnWrite() { checkNotClosed(); @@ -1108,12 +1165,12 @@ public Builder setSingleThreaded(final boolean singleThreaded) { /** * Enables the opt-in "safe close" for the resulting {@link Env}. * - *

        When enabled, the environment tracks its live transactions and cursors so that closure of - * the {@link Env} is prevented if transactions or cursors are active. This adds a small amount - * of bookkeeping on transaction start/close; it is disabled by default so - * applications that already manage their own threading (the common low-latency case) pay - * nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if - * transactions or cursors are active. + *

        When enabled, the environment tracks its live transactions and read-write cursors so that + * closure of the {@link Env} is prevented if transactions or cursors are active. This adds a + * small amount of bookkeeping on transaction start/close; it is disabled by + * default so applications that already manage their own threading (the common + * low-latency case) pay nothing. When enabled, {@link Env#close()} will throw a {@link + * EnvInUseException} if transactions or cursors are active. * * @return the builder */ @@ -1125,12 +1182,12 @@ public Builder setSafeClose() { /** * Enables the opt-in "safe close" for the resulting {@link Env}. * - *

        When enabled, the environment tracks its live transactions and cursors so that closure of - * the {@link Env} is prevented if transactions or cursors are active. This adds a small amount - * of bookkeeping on transaction start/close; it is disabled by default so - * applications that already manage their own threading (the common low-latency case) pay - * nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if - * transactions or cursors are active. + *

        When enabled, the environment tracks its live transactions and read-write cursors so that + * closure of the {@link Env} is prevented if transactions or cursors are active. This adds a + * small amount of bookkeeping on transaction start/close; it is disabled by + * default so applications that already manage their own threading (the common + * low-latency case) pay nothing. When enabled, {@link Env#close()} will throw a {@link + * EnvInUseException} if transactions or cursors are active. * * @param safeClose true to enable cursor/transaction tracking. * @return the builder diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 797e47ce..df6259b2 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -31,7 +31,21 @@ import jnr.ffi.Pointer; /** - * LMDB transaction. + * An LMDB ACID transaction. + * + *

        A transaction belongs to an {@link Env} and must be closed before the {@link Env} is closed. + * Only one concurrent write transaction is supported. Attempts to open another write transaction + * will block until the open write transaction is closed. + * + *

        {@link Txn#commit()} must be called to commit any changes made within the transaction. + * + *

        Uncommitted changes can be rolled back by either calling {@link Txn#close()} or calling {@link + * Txn#abort()}. + * + *

        Closing a transaction without first calling {@link Txn#commit()} will perform an implicit + * rollback of any uncommitted changes made within the transaction. + * + *

        Transactions can be nested * * @param buffer type */ @@ -43,8 +57,8 @@ public final class Txn implements AutoCloseable { private final Pointer ptr; private final boolean readOnly; private final Env env; - private State state; - private RefCounter.RefCounterReleaser refCounterReleaser; + private final RefCounter.RefCounterReleaser refCounterReleaser; + private volatile State state; Txn(final Env env, final Txn parent, final BufferProxy proxy, final TxnFlagSet flags) { @@ -77,7 +91,13 @@ public final class Txn implements AutoCloseable { } } - /** Aborts this transaction. */ + /** + * Aborts this transaction. + * + *

        If this is a read-write transaction, and you have any open {@link Cursor}s against this + * transaction, they MUST be closed first, else you will not be able to close the + * cursor after this transaction has been committed. + */ public void abort() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -86,8 +106,8 @@ public void abort() { state = DONE; LIB.mdb_txn_abort(ptr); - // TODO It is not clear whether this method should call refCounterReleaser.release() like close - // does + // No call to refCounterReleaser.release() here because the keyVal is still open + // and the txn can still be reset. } /** @@ -99,6 +119,10 @@ public void abort() { * *

        Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for * each read-only buffer (ie the key and value). + * + *

        If this is a read-write transaction, and you have any open {@link Cursor}s against this + * transaction, they MUST be closed first, else you will not be able to close the + * cursor after this transaction has been closed. */ @Override public void close() { @@ -122,6 +146,10 @@ public void close() { * *

        If you have an open cursor using this transaction, you must close the cursor before * committing. + * + *

        If this is a read-write transaction, and you have any open {@link Cursor}s against this + * transaction, they MUST be closed first, else you will not be able to close the + * cursor after this transaction has been committed. */ public void commit() { if (SHOULD_CHECK) { diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index edaa30a6..ed8b7b11 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -894,7 +894,7 @@ void closeWithOpenWriteTxn() { } @Test - void closeWithOpenCursor() { + void closeWithOpenRWCursor() { final Path file = tempDir.createTempFile(); final Env env = Env.create() @@ -922,6 +922,35 @@ void closeWithOpenCursor() { // can't close the env as we are unable to close the cursor } + @Test + void closeWithOpenROCursor() { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .setSafeClose(true) + .setSingleThreaded(true) + .open(file); + + final Dbi dbi = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + // Open but don't close + final Txn readTxn = env.txnRead(); + final Cursor cursor = dbi.openCursor(readTxn); + + // Close the txn but not the cursor. LMDB will implicitly close the cursor as it is RO. + readTxn.close(); + + // Close env with no exception as it does not track RO cursors. + env.close(); + + Assertions.assertThatThrownBy(cursor::close).isInstanceOf(Env.AlreadyClosedException.class); + } + /** * Regression for the intermittent close-during-read SIGSEGV (lmdbjava#253 / lmdbjava#279). With * safe close enabled, {@link Env#close()} must never unmap the memory map while another thread is diff --git a/src/test/java/org/lmdbjava/TargetNameTest.java b/src/test/java/org/lmdbjava/TargetNameTest.java index 52762af1..a3d2d7c9 100644 --- a/src/test/java/org/lmdbjava/TargetNameTest.java +++ b/src/test/java/org/lmdbjava/TargetNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2025 The LmdbJava Open Source Project + * Copyright © 2016-2026 The LmdbJava Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lmdbjava; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 1dec78ec..d0f6f18c 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -250,6 +250,43 @@ void txCanCommitThenCloseWithoutError() { } } + @Test + void txAbortThenClose() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txn = env.txnWrite()) { + assertState(txn, READY); + db.put(txn, bb(1), bb(2)); + assertThat(db.get(txn, bb(1))).isEqualTo(bb(2)); + + // Change rolled back + txn.abort(); + } + + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, bb(1))).isNull(); + } + } + + @Test + void txCloseWithoutAbort() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txn = env.txnWrite()) { + assertState(txn, READY); + db.put(txn, bb(1), bb(2)); + assertThat(db.get(txn, bb(1))).isEqualTo(bb(2)); + + // Change rolled back by implicit abort on close + } + + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, bb(1))).isNull(); + } + } + @Test void txCannotAbortIfAlreadyCommitted() { @@ -346,6 +383,98 @@ public void txParent3() { } } + @Test + public void txParent4() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txRoot = env.txnWrite()) { + assertThat(txRoot.getParent()).isNull(); + + // Put using the parent txn + db.put(txRoot, bb(1), bb(10)); + assertThat(db.get(txRoot, bb(1))).isEqualTo(bb(10)); + + try (Txn txChild = env.txn(txRoot)) { + assertThat(txChild.getParent()).isEqualTo(txRoot); + + assertThat(db.get(txChild, bb(1))).isEqualTo(bb(10)); + + // Put using the child txn + db.put(txChild, bb(2), bb(20)); + assertThat(db.get(txChild, bb(2))).isEqualTo(bb(20)); + + // Rollback the child txn's change + txChild.abort(); + } + + // Root's change still there + assertThat(db.get(txRoot, bb(1))).isEqualTo(bb(10)); + // Child's change was rolled back + assertThat(db.get(txRoot, bb(2))).isNull(); + + // Put using the parent txn again + db.put(txRoot, bb(3), bb(30)); + assertThat(db.get(txRoot, bb(3))).isEqualTo(bb(30)); + + // Commit the parent txn's change without the child's changes + txRoot.commit(); + } + + // Open a new txn to assert the entry + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, bb(1))).isEqualTo(bb(10)); + assertThat(db.get(txn, bb(2))).isNull(); + assertThat(db.get(txn, bb(3))).isEqualTo(bb(30)); + } + } + + @Test + public void txParent5() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txRoot = env.txnWrite()) { + assertThat(txRoot.getParent()).isNull(); + + // Put using the parent txn + db.put(txRoot, bb(1), bb(10)); + assertThat(db.get(txRoot, bb(1))).isEqualTo(bb(10)); + + try (Txn txChild = env.txn(txRoot)) { + assertThat(txChild.getParent()).isEqualTo(txRoot); + + assertThat(db.get(txChild, bb(1))).isEqualTo(bb(10)); + + // Put using the child txn + db.put(txChild, bb(2), bb(20)); + assertThat(db.get(txChild, bb(2))).isEqualTo(bb(20)); + + // Commit the child txn's change + txChild.commit(); + } + + // Root's change still there + assertThat(db.get(txRoot, bb(1))).isEqualTo(bb(10)); + // Child's change was rolled back + assertThat(db.get(txRoot, bb(2))).isEqualTo(bb(20)); + + // Put using the parent txn again + db.put(txRoot, bb(3), bb(30)); + assertThat(db.get(txRoot, bb(3))).isEqualTo(bb(30)); + + // Roll back everything, including the changes committed in the child txn + txRoot.abort(); + } + + // Open a new txn to assert the entry + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, bb(1))).isNull(); + assertThat(db.get(txn, bb(2))).isNull(); + assertThat(db.get(txn, bb(3))).isNull(); + } + } + @Disabled // We shouldn't be trying to close the env with open txns @Test void txParentDeniedIfEnvClosed() { From ab0fc6a4f4aff1bd72f153ff21b6606dd66360d3 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:31:14 +0100 Subject: [PATCH 60/61] gh-279 Fix codeQL suggestion --- src/test/java/org/lmdbjava/TestUtils.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index 0900a64b..c89c3b9a 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -29,6 +29,7 @@ import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.StreamSupport; import org.agrona.MutableDirectBuffer; import org.agrona.concurrent.UnsafeBuffer; @@ -237,13 +238,8 @@ public static int getEntryCount(final Dbi dbi, final Env } public static int getEntryCount(final Dbi dbi, final Txn txn) { - int count = 0; try (CursorIterable cursorIterable = dbi.iterate(txn)) { - - for (final CursorIterable.KeyVal kv : cursorIterable) { - count++; - } + return (int) StreamSupport.stream(cursorIterable.spliterator(), false).count(); } - return count; } } From e8fdd2d458bf75518f2429d5c534a36f4b586d54 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:36:14 +0100 Subject: [PATCH 61/61] gh-279 Tweak javadoc --- src/main/java/org/lmdbjava/Env.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index edae4a97..29196cce 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -182,8 +182,9 @@ public static Env open(final File path, final int size, final EnvFla *

        Before and during this call, the caller MUST ensure that: * *

          - *
        • every {@link Txn} and {@link Cursor} obtained from this environment has already been - * closed; and + *
        • every {@link Txn} obtained from this environment has already been closed. + *
        • every {@link Cursor} associated with a read-write {@link Txn} obtained from this + * environment has already been closed. *
        • no other thread is executing any operation on this environment or on a handle * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as * {@code Dbi.get}. @@ -202,7 +203,7 @@ public static Env open(final File path, final int size, final EnvFla * lock, so the map is never unmapped while a read is in flight. * *

          If safeClose has been enabled on the {@link Env}, then this method will throw a {@link - * EnvInUseException} if transactions or cursors are still active. + * EnvInUseException} if transactions or RW cursors are still active. * *

          If safeClose has not been enabled then this method will perform the close regardless of * whether it is in use or not with the implications detailed above. @@ -223,8 +224,9 @@ public void close() { *

          Before and during this call, the caller MUST ensure that: * *

            - *
          • every {@link Txn} and {@link Cursor} obtained from this environment has already been - * closed; and + *
          • every {@link Txn} obtained from this environment has already been closed. + *
          • every {@link Cursor} associated with a read-write {@link Txn} obtained from this + * environment has already been closed. *
          • no other thread is executing any operation on this environment or on a handle * derived from it — including {@link #txnRead()} / {@link #txnWrite()} and reads such as * {@code Dbi.get}. @@ -243,7 +245,7 @@ public void close() { * lock, so the map is never unmapped while a read is in flight. * *

            If safeClose has been enabled on the {@link Env}, then this method will return false if - * transactions or cursors are still active. + * transactions or RW cursors are still active. * *

            If safeClose has not been enabled then this method will perform the close regardless of * whether it is in use or not with the implications detailed above, i.e. it has the same