Skip to content

Commit 017ef34

Browse files
committed
gh-279 Add more tests, improve javadoc
1 parent eefc87f commit 017ef34

6 files changed

Lines changed: 275 additions & 30 deletions

File tree

‎src/main/java/org/lmdbjava/Dbi.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2016-2025 The LmdbJava Open Source Project
2+
* Copyright © 2016-2026 The LmdbJava Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -408,6 +408,9 @@ public Cursor<T> openCursor(final Txn<T> txn) {
408408
/**
409409
* Starts a new read-write transaction and puts the key/data pair.
410410
*
411+
* <p>NOTE: If this is called while this thread already has an open write transaction, it will
412+
* block indefinitely.
413+
*
411414
* @param key key to store in the database (not null)
412415
* @param val value to store in the database (not null)
413416
* @see #put(Txn, Object, Object, PutFlagSet)

‎src/main/java/org/lmdbjava/Env.java‎

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,26 @@
4545
import org.lmdbjava.Library.MDB_stat;
4646

4747
/**
48-
* LMDB environment.
48+
* An LMDB environment that includes one or more databases ({@link Dbi}s). The {@link Env} manages
49+
* the transactions and databases. An {@link Env} can only have one concurrent write transaction but
50+
* supports multiple concurrent read transactions.
51+
*
52+
* <p><strong>WARNING</strong>: LMDBJava's and LMDB's performance comes from their low-level memory
53+
* access, but this requires that you strictly adhere to the various contracts set out when using
54+
* environments, databases, transactions, and cursors. Incorrect use of LMDBJava can lead to
55+
* segmentation faults that can crash your application.
56+
*
57+
* <p>By default, LMDBJava performs some checks, for example, checking that the {@link Env} is not
58+
* closed when opening a transaction. It is possible, however, for race conditions to occur if you
59+
* close the {@link Env} after one of these checks has been performed and before the transaction is
60+
* opened. Note, these checks can be disabled by setting the {@link #DISABLE_CHECKS_PROP} system
61+
* property to {@code true}. This may be beneficial in performance-critical applications.
62+
*
63+
* <p>{@link Builder#setSafeClose()} can also be used to add additional checks that ensure the
64+
* {@link Env} is not closed while transactions/cursors are in use.
65+
*
66+
* <p>It is the responsibility of the user to ensure that the {@link Env} is not closed while
67+
* transactions or cursors are in use.
4968
*
5069
* @param <T> buffer type
5170
*/
@@ -79,6 +98,7 @@ public final class Env<T> implements AutoCloseable {
7998
/** True if this Env has been created on the basis of only ever being used by a single thread. */
8099
private final boolean isSingleThreaded;
81100

101+
/** If true, close will be prevented if there are open txns/cursors. */
82102
private final boolean safeClose;
83103

84104
private Env(
@@ -450,10 +470,11 @@ boolean isSafeClose() {
450470
}
451471

452472
/**
453-
* Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}.
473+
* Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. This
474+
* method is used for both opening an existing database or creating a new one.
454475
*
455-
* <p>The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if you need to create a
456-
* new database before opening it.
476+
* <p>The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if the database does not
477+
* already exist, and you need to create it before opening it.
457478
*
458479
* @return A new builder instance for creating/opening a {@link Dbi}.
459480
*/
@@ -641,6 +662,9 @@ public void sync(final boolean force) {
641662
* @return a transaction (never null)
642663
* @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)}
643664
* <p>Obtain a transaction with the requested parent and flags.
665+
* <p>Must not race a concurrent {@link #close()} on another thread: the closed-check and the
666+
* native transaction start are not atomic, so a close occurring between them can crash the
667+
* JVM (see {@link #close()}).
644668
*/
645669
@Deprecated
646670
public Txn<T> txn(final Txn<T> parent, final TxnFlags... flags) {
@@ -649,9 +673,18 @@ public Txn<T> txn(final Txn<T> parent, final TxnFlags... flags) {
649673
}
650674

651675
/**
652-
* Obtain a transaction with the requested parent and flags.
676+
* Obtain a read-write transaction with the requested parent and flags.
653677
*
654-
* @param parent parent transaction (may be null if no parent)
678+
* <p>Must not race a concurrent {@link #close()} on another thread: the closed-check and the
679+
* native transaction start are not atomic, so a close occurring between them can crash the JVM
680+
* (see {@link #close()}).
681+
*
682+
* <p>When using a parent transaction, any committed changes will only be visible to the parent
683+
* transaction and will only be fully committed to the {@link Dbi} if the root transaction is
684+
* committed. Aborting this transaction will not roll back changes already made by the parent
685+
* transaction.
686+
*
687+
* @param parent parent transaction (maybe null if no parent)
655688
* @return a transaction (never null)
656689
*/
657690
public Txn<T> txn(final Txn<T> parent) {
@@ -662,11 +695,25 @@ public Txn<T> txn(final Txn<T> parent) {
662695
/**
663696
* Obtain a transaction with the requested parent and flags.
664697
*
665-
* @param parent parent transaction (may be null if no parent)
698+
* <p>If you want a read-write transaction, you can instead call {@link #txn(Txn)}. To obtain a
699+
* read-only transaction, ensure {@link TxnFlags#MDB_RDONLY_TXN} is present in the {@link
700+
* TxnFlagSet}.
701+
*
702+
* <p>Must not race a concurrent {@link #close()} on another thread: the closed-check and the
703+
* native transaction start are not atomic, so a close occurring between them can crash the JVM
704+
* (see {@link #close()}).
705+
*
706+
* <p>When using a parent transaction, any committed changes will only be visible to the parent
707+
* transaction and will only be fully committed to the {@link Dbi} if the root transaction is
708+
* committed. Aborting this transaction will not roll back changes already made by the parent
709+
* transaction.
710+
*
711+
* @param parent parent transaction (maybe null if no parent)
666712
* @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags
667-
* is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet}
713+
* is used frequently, it is recommended to hold a static instance of the {@link TxnFlagSet}
668714
* for re-use.
669715
* @return a transaction (never null)
716+
* @throws Env.AlreadyClosedException if this environment has already been closed.
670717
*/
671718
public Txn<T> txn(final Txn<T> parent, final TxnFlagSet flags) {
672719
checkNotClosed();
@@ -676,7 +723,12 @@ public Txn<T> txn(final Txn<T> parent, final TxnFlagSet flags) {
676723
/**
677724
* Obtain a read-only transaction.
678725
*
726+
* <p>Must not race a concurrent {@link #close()} on another thread: the closed-check and the
727+
* native transaction start are not atomic, so a close occurring between them can crash the JVM
728+
* (see {@link #close()}).
729+
*
679730
* @return a read-only transaction
731+
* @throws Env.AlreadyClosedException if this environment has already been closed.
680732
*/
681733
public Txn<T> txnRead() {
682734
checkNotClosed();
@@ -686,7 +738,12 @@ public Txn<T> txnRead() {
686738
/**
687739
* Obtain a read-write transaction.
688740
*
741+
* <p>Must not race a concurrent {@link #close()} on another thread: the closed-check and the
742+
* native transaction start are not atomic, so a close occurring between them can crash the JVM
743+
* (see {@link #close()}).
744+
*
689745
* @return a read-write transaction
746+
* @throws Env.AlreadyClosedException if this environment has already been closed
690747
*/
691748
public Txn<T> txnWrite() {
692749
checkNotClosed();
@@ -1108,12 +1165,12 @@ public Builder<T> setSingleThreaded(final boolean singleThreaded) {
11081165
/**
11091166
* Enables the opt-in "safe close" for the resulting {@link Env}.
11101167
*
1111-
* <p>When enabled, the environment tracks its live transactions and cursors so that closure of
1112-
* the {@link Env} is prevented if transactions or cursors are active. This adds a small amount
1113-
* of bookkeeping on transaction start/close; it is <strong>disabled by default</strong> so
1114-
* applications that already manage their own threading (the common low-latency case) pay
1115-
* nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if
1116-
* transactions or cursors are active.
1168+
* <p>When enabled, the environment tracks its live transactions and read-write cursors so that
1169+
* closure of the {@link Env} is prevented if transactions or cursors are active. This adds a
1170+
* small amount of bookkeeping on transaction start/close; it is <strong>disabled by
1171+
* default</strong> so applications that already manage their own threading (the common
1172+
* low-latency case) pay nothing. When enabled, {@link Env#close()} will throw a {@link
1173+
* EnvInUseException} if transactions or cursors are active.
11171174
*
11181175
* @return the builder
11191176
*/
@@ -1125,12 +1182,12 @@ public Builder<T> setSafeClose() {
11251182
/**
11261183
* Enables the opt-in "safe close" for the resulting {@link Env}.
11271184
*
1128-
* <p>When enabled, the environment tracks its live transactions and cursors so that closure of
1129-
* the {@link Env} is prevented if transactions or cursors are active. This adds a small amount
1130-
* of bookkeeping on transaction start/close; it is <strong>disabled by default</strong> so
1131-
* applications that already manage their own threading (the common low-latency case) pay
1132-
* nothing. When enabled, {@link Env#close()} will throw a {@link EnvInUseException} if
1133-
* transactions or cursors are active.
1185+
* <p>When enabled, the environment tracks its live transactions and read-write cursors so that
1186+
* closure of the {@link Env} is prevented if transactions or cursors are active. This adds a
1187+
* small amount of bookkeeping on transaction start/close; it is <strong>disabled by
1188+
* default</strong> so applications that already manage their own threading (the common
1189+
* low-latency case) pay nothing. When enabled, {@link Env#close()} will throw a {@link
1190+
* EnvInUseException} if transactions or cursors are active.
11341191
*
11351192
* @param safeClose true to enable cursor/transaction tracking.
11361193
* @return the builder

‎src/main/java/org/lmdbjava/Txn.java‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,21 @@
3131
import jnr.ffi.Pointer;
3232

3333
/**
34-
* LMDB transaction.
34+
* An LMDB ACID transaction.
35+
*
36+
* <p>A transaction belongs to an {@link Env} and must be closed before the {@link Env} is closed.
37+
* Only one concurrent write transaction is supported. Attempts to open another write transaction
38+
* will block until the open write transaction is closed.
39+
*
40+
* <p>{@link Txn#commit()} must be called to commit any changes made within the transaction.
41+
*
42+
* <p>Uncommitted changes can be rolled back by either calling {@link Txn#close()} or calling {@link
43+
* Txn#abort()}.
44+
*
45+
* <p>Closing a transaction without first calling {@link Txn#commit()} will perform an implicit
46+
* rollback of any uncommitted changes made within the transaction.
47+
*
48+
* <p>Transactions can be nested
3549
*
3650
* @param <T> buffer type
3751
*/
@@ -43,8 +57,8 @@ public final class Txn<T> implements AutoCloseable {
4357
private final Pointer ptr;
4458
private final boolean readOnly;
4559
private final Env<T> env;
46-
private State state;
47-
private RefCounter.RefCounterReleaser refCounterReleaser;
60+
private final RefCounter.RefCounterReleaser refCounterReleaser;
61+
private volatile State state;
4862

4963
Txn(final Env<T> env, final Txn<T> parent, final BufferProxy<T> proxy, final TxnFlagSet flags) {
5064

@@ -77,7 +91,13 @@ public final class Txn<T> implements AutoCloseable {
7791
}
7892
}
7993

80-
/** Aborts this transaction. */
94+
/**
95+
* Aborts this transaction.
96+
*
97+
* <p>If this is a read-write transaction, and you have any open {@link Cursor}s against this
98+
* transaction, they <strong>MUST</strong> be closed first, else you will not be able to close the
99+
* cursor after this transaction has been committed.
100+
*/
81101
public void abort() {
82102
if (SHOULD_CHECK) {
83103
env.checkNotClosed();
@@ -86,8 +106,8 @@ public void abort() {
86106
state = DONE;
87107
LIB.mdb_txn_abort(ptr);
88108

89-
// TODO It is not clear whether this method should call refCounterReleaser.release() like close
90-
// does
109+
// No call to refCounterReleaser.release() here because the keyVal is still open
110+
// and the txn can still be reset.
91111
}
92112

93113
/**
@@ -99,6 +119,10 @@ public void abort() {
99119
*
100120
* <p>Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for
101121
* each read-only buffer (ie the key and value).
122+
*
123+
* <p>If this is a read-write transaction, and you have any open {@link Cursor}s against this
124+
* transaction, they <strong>MUST</strong> be closed first, else you will not be able to close the
125+
* cursor after this transaction has been closed.
102126
*/
103127
@Override
104128
public void close() {
@@ -122,6 +146,10 @@ public void close() {
122146
*
123147
* <p>If you have an open cursor using this transaction, you must close the cursor before
124148
* committing.
149+
*
150+
* <p>If this is a read-write transaction, and you have any open {@link Cursor}s against this
151+
* transaction, they <strong>MUST</strong> be closed first, else you will not be able to close the
152+
* cursor after this transaction has been committed.
125153
*/
126154
public void commit() {
127155
if (SHOULD_CHECK) {

‎src/test/java/org/lmdbjava/EnvTest.java‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ void closeWithOpenWriteTxn() {
894894
}
895895

896896
@Test
897-
void closeWithOpenCursor() {
897+
void closeWithOpenRWCursor() {
898898
final Path file = tempDir.createTempFile();
899899
final Env<ByteBuffer> env =
900900
Env.create()
@@ -922,6 +922,35 @@ void closeWithOpenCursor() {
922922
// can't close the env as we are unable to close the cursor
923923
}
924924

925+
@Test
926+
void closeWithOpenROCursor() {
927+
final Path file = tempDir.createTempFile();
928+
final Env<ByteBuffer> env =
929+
Env.create()
930+
.setMapSize(1, ByteUnit.MEBIBYTES)
931+
.setMaxDbs(1)
932+
.setMaxReaders(1)
933+
.setEnvFlags(MDB_NOSUBDIR)
934+
.setSafeClose(true)
935+
.setSingleThreaded(true)
936+
.open(file);
937+
938+
final Dbi<ByteBuffer> dbi =
939+
env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open();
940+
941+
// Open but don't close
942+
final Txn<ByteBuffer> readTxn = env.txnRead();
943+
final Cursor<ByteBuffer> cursor = dbi.openCursor(readTxn);
944+
945+
// Close the txn but not the cursor. LMDB will implicitly close the cursor as it is RO.
946+
readTxn.close();
947+
948+
// Close env with no exception as it does not track RO cursors.
949+
env.close();
950+
951+
Assertions.assertThatThrownBy(cursor::close).isInstanceOf(Env.AlreadyClosedException.class);
952+
}
953+
925954
/**
926955
* Regression for the intermittent close-during-read SIGSEGV (lmdbjava#253 / lmdbjava#279). With
927956
* safe close enabled, {@link Env#close()} must never unmap the memory map while another thread is

‎src/test/java/org/lmdbjava/TargetNameTest.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2016-2025 The LmdbJava Open Source Project
2+
* Copyright © 2016-2026 The LmdbJava Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package org.lmdbjava;
1817

1918
import static org.assertj.core.api.Assertions.assertThat;

0 commit comments

Comments
 (0)