From 3da5b069e4e2de8afbb1ef731f35233fe3b060ec Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:25:30 +0000 Subject: [PATCH 1/4] gh-185 Add protection for closed env Change Cursor and Txn to hold the Env instance so it can be checked for its open/closed state. Add calls to env.checkNotClosed() in methods that call out to the LMDB lib to prevent the JVM crashing if the env has been closed. Add lots of tests involving env closure. --- src/main/java/org/lmdbjava/Cursor.java | 20 +- src/main/java/org/lmdbjava/Dbi.java | 14 +- src/main/java/org/lmdbjava/Env.java | 6 + src/main/java/org/lmdbjava/Txn.java | 21 + .../java/org/lmdbjava/CursorIterableTest.java | 54 ++ src/test/java/org/lmdbjava/CursorTest.java | 81 ++ src/test/java/org/lmdbjava/DbiTest.java | 910 ++++++++++-------- src/test/java/org/lmdbjava/EnvTest.java | 71 ++ src/test/java/org/lmdbjava/TxnTest.java | 45 + 9 files changed, 814 insertions(+), 408 deletions(-) diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index f165423b..7c349067 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -51,13 +51,15 @@ public final class Cursor implements AutoCloseable { private final KeyVal kv; private final Pointer ptrCursor; private Txn txn; + private final Env env; - Cursor(final Pointer ptr, final Txn txn) { + Cursor(final Pointer ptr, final Txn txn, final Env env) { requireNonNull(ptr); requireNonNull(txn); this.ptrCursor = ptr; this.txn = txn; this.kv = txn.newKeyVal(); + this.env = env; } /** @@ -72,8 +74,11 @@ public void close() { if (closed) { return; } - if (SHOULD_CHECK && !txn.isReadOnly()) { - txn.checkReady(); + if (SHOULD_CHECK) { + env.checkNotClosed(); + if (!txn.isReadOnly()) { + txn.checkReady(); + } } LIB.mdb_cursor_close(ptrCursor); kv.close(); @@ -91,6 +96,7 @@ public void close() { */ public long count() { if (SHOULD_CHECK) { + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); } @@ -109,6 +115,7 @@ public long count() { */ public void delete(final PutFlags... f) { if (SHOULD_CHECK) { + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); @@ -138,6 +145,7 @@ public boolean get(final T key, final T data, final SeekOp op) { if (SHOULD_CHECK) { requireNonNull(key); requireNonNull(op); + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); } @@ -168,6 +176,7 @@ public boolean get(final T key, final GetOp op) { if (SHOULD_CHECK) { requireNonNull(key); requireNonNull(op); + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); } @@ -238,6 +247,7 @@ public boolean put(final T key, final T val, final PutFlags... op) { if (SHOULD_CHECK) { requireNonNull(key); requireNonNull(val); + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); @@ -281,6 +291,7 @@ public void putMultiple(final T key, final T val, final int elements, requireNonNull(txn); requireNonNull(key); requireNonNull(val); + env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } @@ -311,6 +322,7 @@ public void putMultiple(final T key, final T val, final int elements, public void renew(final Txn newTxn) { if (SHOULD_CHECK) { requireNonNull(newTxn); + env.checkNotClosed(); checkNotClosed(); this.txn.checkReadOnly(); // existing newTxn.checkReadOnly(); @@ -339,6 +351,7 @@ public void renew(final Txn newTxn) { public T reserve(final T key, final int size, final PutFlags... op) { if (SHOULD_CHECK) { requireNonNull(key); + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); @@ -361,6 +374,7 @@ public T reserve(final T key, final int size, final PutFlags... op) { public boolean seek(final SeekOp op) { if (SHOULD_CHECK) { requireNonNull(op); + env.checkNotClosed(); checkNotClosed(); txn.checkReady(); } diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index 7ccb1940..f8be2239 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -104,6 +104,9 @@ public final class Dbi { */ public void close() { clean(); + if (SHOULD_CHECK) { + env.checkNotClosed(); + } LIB.mdb_dbi_close(env.pointer(), ptr); } @@ -199,6 +202,7 @@ public void drop(final Txn txn) { public void drop(final Txn txn, final boolean delete) { if (SHOULD_CHECK) { requireNonNull(txn); + env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } @@ -228,6 +232,7 @@ public T get(final Txn txn, final T key) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); + env.checkNotClosed(); txn.checkReady(); } txn.kv().keyIn(key); @@ -295,6 +300,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range, if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(range); + env.checkNotClosed(); txn.checkReady(); } final Comparator useComp; @@ -313,6 +319,9 @@ public CursorIterable iterate(final Txn txn, final KeyRange range, * @return the list of flags this Dbi was created with */ public List listFlags(final Txn txn) { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } final IntByReference resultPtr = new IntByReference(); checkRc(LIB.mdb_dbi_flags(txn.pointer(), ptr, resultPtr)); @@ -348,11 +357,12 @@ public List listFlags(final Txn txn) { public Cursor openCursor(final Txn txn) { if (SHOULD_CHECK) { requireNonNull(txn); + env.checkNotClosed(); txn.checkReady(); } final PointerByReference cursorPtr = new PointerByReference(); checkRc(LIB.mdb_cursor_open(txn.pointer(), ptr, cursorPtr)); - return new Cursor<>(cursorPtr.getValue(), txn); + return new Cursor<>(cursorPtr.getValue(), txn, env); } /** @@ -434,6 +444,7 @@ public T reserve(final Txn txn, final T key, final int size, if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); + env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } @@ -455,6 +466,7 @@ public T reserve(final Txn txn, final T key, final int size, public Stat stat(final Txn txn) { if (SHOULD_CHECK) { requireNonNull(txn); + env.checkNotClosed(); txn.checkReady(); } final MDB_stat stat = new MDB_stat(RUNTIME); diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 326763f6..008e7224 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -394,6 +394,12 @@ Pointer pointer() { return ptr; } + void checkNotClosed() { + if (closed) { + throw new AlreadyClosedException(); + } + } + private void validateDirectoryEmpty(final File path) { if (!path.exists()) { throw new InvalidCopyDestination("Path does not exist"); diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 36f8eaf9..ff0476c0 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -22,6 +22,7 @@ import static jnr.ffi.Memory.allocateDirect; import static jnr.ffi.NativeType.ADDRESS; +import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.Library.RUNTIME; import static org.lmdbjava.MaskedFlag.isSet; @@ -49,6 +50,7 @@ public final class Txn implements AutoCloseable { private final BufferProxy proxy; private final Pointer ptr; private final boolean readOnly; + private final Env env; private State state; Txn(final Env env, final Txn parent, final BufferProxy proxy, @@ -60,6 +62,7 @@ public final class Txn implements AutoCloseable { if (env.isReadOnly() && !this.readOnly) { throw new EnvIsReadOnly(); } + this.env = env; this.parent = parent; if (parent != null && parent.isReadOnly() != this.readOnly) { throw new IncompatibleParent(); @@ -76,6 +79,9 @@ public final class Txn implements AutoCloseable { * Aborts this transaction. */ public void abort() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } checkReady(); state = DONE; LIB.mdb_txn_abort(ptr); @@ -91,6 +97,9 @@ public void abort() { */ @Override public void close() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } if (state == RELEASED) { return; } @@ -105,6 +114,9 @@ public void close() { * Commits this transaction. */ public void commit() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } checkReady(); state = DONE; checkRc(LIB.mdb_txn_commit(ptr)); @@ -116,6 +128,9 @@ public void commit() { * @return A transaction ID, valid if input is an active transaction */ public long getId() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } return LIB.mdb_txn_id(ptr); } @@ -153,6 +168,9 @@ public T key() { * Renews a read-only transaction previously released by {@link #reset()}. */ public void renew() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } if (state != RESET) { throw new NotResetException(); } @@ -166,6 +184,9 @@ public void renew() { * can be reused upon calling {@link #renew()}. */ public void reset() { + if (SHOULD_CHECK) { + env.checkNotClosed(); + } checkReadOnly(); if (state != READY && state != DONE) { throw new ResetException(); diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 05819835..dd9ac322 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -63,6 +63,7 @@ import java.util.NoSuchElementException; import com.google.common.primitives.UnsignedBytes; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -302,6 +303,59 @@ public void removeOddElements() { verify(all(), 4, 8); } + @Test(expected = Env.AlreadyClosedException.class) + public void nextWithClosedEnvTest() { + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.next(); + } + } + } + + @Test(expected = Env.AlreadyClosedException.class) + public void removeWithClosedEnvTest() { + try (Txn txn = env.txnWrite()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + final KeyVal keyVal = c.next(); + assertThat(keyVal, Matchers.notNullValue()); + + env.close(); + c.remove(); + } + } + } + + @Test(expected = Env.AlreadyClosedException.class) + public void hasNextWithClosedEnvTest() { + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.hasNext(); + } + } + } + + @Test(expected = Env.AlreadyClosedException.class) + public void forEachRemainingWithClosedEnvTest() { + try (Txn txn = env.txnRead()) { + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + final Iterator> c = ci.iterator(); + + env.close(); + c.forEachRemaining(keyVal -> { + + }); + } + } + } + private void verify(final KeyRange range, final int... expected) { verify(range, null, expected); } diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 373b68e9..4f9fedc7 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -40,6 +40,8 @@ import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; import static org.lmdbjava.SeekOp.MDB_FIRST; import static org.lmdbjava.SeekOp.MDB_GET_BOTH; +import static org.lmdbjava.SeekOp.MDB_LAST; +import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; @@ -47,6 +49,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.function.Consumer; import org.junit.After; import org.junit.Before; @@ -96,6 +99,59 @@ public void closedCursorRejectsSubsequentGets() { } } + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsSeekFirstCall() { + doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsSeekLastCall() { + doEnvClosedTest(null, c -> c.seek(MDB_LAST)); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsSeekNextCall() { + doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsCloseCall() { + doEnvClosedTest(null, Cursor::close); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsFirstCall() { + doEnvClosedTest(null, Cursor::first); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsLastCall() { + doEnvClosedTest(null, Cursor::last); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsPrevCall() { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt(), is(1)); + assertThat(c.val().getInt(), is(10)); + c.next(); + }, + Cursor::prev); + } + + @Test(expected = Env.AlreadyClosedException.class) + public void closedEnvRejectsDeleteCall() { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt(), is(1)); + assertThat(c.val().getInt(), is(10)); + }, + Cursor::delete); + } + @Test public void count() { final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); @@ -338,4 +394,29 @@ public void testCursorByteBufferDuplicate() { } } } + + private void doEnvClosedTest(final Consumer> workBeforeEnvClosed, + final Consumer> workAfterEnvClose) { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + 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 txn = env.txnWrite()) { + try (Cursor c = db.openCursor(txn)) { + + if (workBeforeEnvClosed != null) { + workBeforeEnvClosed.accept(c); + } + + env.close(); + + if (workAfterEnvClose != null) { + workAfterEnvClose.accept(c); + } + } + } + } } diff --git a/src/test/java/org/lmdbjava/DbiTest.java b/src/test/java/org/lmdbjava/DbiTest.java index 689eab58..33735678 100644 --- a/src/test/java/org/lmdbjava/DbiTest.java +++ b/src/test/java/org/lmdbjava/DbiTest.java @@ -7,9 +7,9 @@ * 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. @@ -67,6 +67,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import org.agrona.concurrent.UnsafeBuffer; import org.hamcrest.Matchers; @@ -77,6 +78,7 @@ import org.junit.rules.TemporaryFolder; import org.lmdbjava.CursorIterable.KeyVal; import org.lmdbjava.Dbi.DbFullException; +import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Env.MapFullException; import org.lmdbjava.LmdbNativeException.ConstantDerivedException; @@ -85,411 +87,511 @@ */ public final class DbiTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - private Env env; - - @After - public void after() { - env.close(); - } - - @Before - public void before() throws IOException { - final File path = tmp.newFile(); - env = create() - .setMapSize(MEBIBYTES.toBytes(64)) - .setMaxReaders(2) - .setMaxDbs(2) - .open(path, MDB_NOSUBDIR); - } - - @Test(expected = ConstantDerivedException.class) - public void close() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - db.put(bb(1), bb(42)); - db.close(); - db.put(bb(2), bb(42)); // error - } - - @Test - public void customComparator() { - final Comparator reverseOrder = (o1, o2) -> { - final int lexicalOrder = ByteBufferProxy.PROXY_OPTIMAL.compare(o1, o2); - if (lexicalOrder == 0) { - return 0; - } - return lexicalOrder * -1; - }; - final Dbi db = env.openDbi(DB_1, reverseOrder, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - assertThat(db.put(txn, bb(2), bb(3)), is(true)); - assertThat(db.put(txn, bb(4), bb(6)), is(true)); - assertThat(db.put(txn, bb(6), bb(7)), is(true)); - assertThat(db.put(txn, bb(8), bb(7)), is(true)); - txn.commit(); - } - try (Txn txn = env.txnRead(); - CursorIterable ci = db.iterate(txn, atMost(bb(4)))) { - final Iterator> iter = ci.iterator(); - assertThat(iter.next().key().getInt(), is(8)); - assertThat(iter.next().key().getInt(), is(6)); - assertThat(iter.next().key().getInt(), is(4)); - } - } - - @Test(expected = DbFullException.class) - @SuppressWarnings("ResultOfObjectAllocationIgnored") - public void dbOpenMaxDatabases() { - env.openDbi("db1 is OK", MDB_CREATE); - env.openDbi("db2 is OK", MDB_CREATE); - env.openDbi("db3 fails", MDB_CREATE); - } - - @Test - public void dbiWithComparatorThreadSafety() { - final Dbi db = env.openDbi(DB_1, PROXY_OPTIMAL::compare, - MDB_CREATE); - - final List keys = range(0, 1_000).boxed().collect(toList()); - - final ExecutorService pool = Executors.newCachedThreadPool(); - final AtomicBoolean proceed = new AtomicBoolean(true); - final Future reader = pool.submit(() -> { - while (proceed.get()) { + @Rule + public final TemporaryFolder tmp = new TemporaryFolder(); + private Env env; + + @After + public void after() { + env.close(); + } + + @Before + public void before() throws IOException { + final File path = tmp.newFile(); + env = create() + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(2) + .setMaxDbs(2) + .open(path, MDB_NOSUBDIR); + } + + @Test(expected = ConstantDerivedException.class) + public void close() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + db.put(bb(1), bb(42)); + db.close(); + db.put(bb(2), bb(42)); // error + } + + @Test + public void customComparator() { + final Comparator reverseOrder = (o1, o2) -> { + final int lexicalOrder = ByteBufferProxy.PROXY_OPTIMAL.compare(o1, o2); + if (lexicalOrder == 0) { + return 0; + } + return lexicalOrder * -1; + }; + final Dbi db = env.openDbi(DB_1, reverseOrder, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + assertThat(db.put(txn, bb(2), bb(3)), is(true)); + assertThat(db.put(txn, bb(4), bb(6)), is(true)); + assertThat(db.put(txn, bb(6), bb(7)), is(true)); + assertThat(db.put(txn, bb(8), bb(7)), is(true)); + txn.commit(); + } + try (Txn txn = env.txnRead(); + CursorIterable ci = db.iterate(txn, atMost(bb(4)))) { + final Iterator> iter = ci.iterator(); + assertThat(iter.next().key().getInt(), is(8)); + assertThat(iter.next().key().getInt(), is(6)); + assertThat(iter.next().key().getInt(), is(4)); + } + } + + @Test(expected = DbFullException.class) + @SuppressWarnings("ResultOfObjectAllocationIgnored") + public void dbOpenMaxDatabases() { + env.openDbi("db1 is OK", MDB_CREATE); + env.openDbi("db2 is OK", MDB_CREATE); + env.openDbi("db3 fails", MDB_CREATE); + } + + @Test + public void dbiWithComparatorThreadSafety() { + final Dbi db = env.openDbi(DB_1, PROXY_OPTIMAL::compare, + MDB_CREATE); + + final List keys = range(0, 1_000).boxed().collect(toList()); + + final ExecutorService pool = Executors.newCachedThreadPool(); + final AtomicBoolean proceed = new AtomicBoolean(true); + final Future reader = pool.submit(() -> { + while (proceed.get()) { + try (Txn txn = env.txnRead()) { + db.get(txn, bb(50)); + } + } + }); + + for (final Integer key : keys) { + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(key), bb(3)); + txn.commit(); + } + } + + try (Txn txn = env.txnRead(); + CursorIterable ci = db.iterate(txn)) { + final Iterator> iter = ci.iterator(); + final List result = new ArrayList<>(); + while (iter.hasNext()) { + result.add(iter.next().key().getInt()); + } + + assertThat(result, Matchers.contains(keys.toArray(new Integer[0]))); + } + + proceed.set(false); + try { + reader.get(1, SECONDS); + pool.shutdown(); + pool.awaitTermination(1, SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new IllegalStateException(e); + } + } + + @Test + public void drop() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(1), bb(42)); + db.put(txn, bb(2), bb(42)); + assertThat(db.get(txn, bb(1)), not(nullValue())); + assertThat(db.get(txn, bb(2)), not(nullValue())); + db.drop(txn); + assertThat(db.get(txn, bb(1)), is(nullValue())); // data gone + assertThat(db.get(txn, bb(2)), is(nullValue())); + db.put(txn, bb(1), bb(42)); // ensure DB still works + db.put(txn, bb(2), bb(42)); + assertThat(db.get(txn, bb(1)), not(nullValue())); + assertThat(db.get(txn, bb(2)), not(nullValue())); + } + } + + @Test + public void dropAndDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi nameDb = env.openDbi((byte[]) null); + final byte[] dbNameBytes = DB_1.getBytes(UTF_8); + final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); + dbNameBuffer.put(dbNameBytes).flip(); + + try (Txn txn = env.txnWrite()) { + assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); + db.drop(txn, true); + assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); + txn.commit(); + } + } + + @Test + public void dropAndDeleteAnonymousDb() { + env.openDbi(DB_1, MDB_CREATE); + final Dbi nameDb = env.openDbi((byte[]) null); + final byte[] dbNameBytes = DB_1.getBytes(UTF_8); + final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); + dbNameBuffer.put(dbNameBytes).flip(); + + try (Txn txn = env.txnWrite()) { + assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); + nameDb.drop(txn, true); + assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); + txn.commit(); + } + + nameDb.close(); // explicit close after drop is OK + } + + @Test + public void getName() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + assertThat(db.getName(), is(DB_1.getBytes(UTF_8))); + } + + @Test + public void getNamesWhenDbisPresent() { + final byte[] dbHello = new byte[]{'h', 'e', 'l', 'l', 'o'}; + final byte[] dbWorld = new byte[]{'w', 'o', 'r', 'l', 'd'}; + env.openDbi(dbHello, MDB_CREATE); + env.openDbi(dbWorld, MDB_CREATE); + final List dbiNames = env.getDbiNames(); + assertThat(dbiNames, hasSize(2)); + assertThat(dbiNames.get(0), is(dbHello)); + assertThat(dbiNames.get(1), is(dbWorld)); + } + + @Test + public void getNamesWhenEmpty() { + final List dbiNames = env.getDbiNames(); + assertThat(dbiNames, empty()); + } + + @Test + public void listsFlags() { + final Dbi dbi = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, + MDB_REVERSEKEY); + + try (Txn txn = env.txnRead()) { + final List flags = dbi.listFlags(txn); + assertThat(flags, containsInAnyOrder(MDB_DUPSORT, MDB_REVERSEKEY)); + } + } + + @Test + public void putAbortGet() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + txn.abort(); + } + + try (Txn txn = env.txnWrite()) { + assertNull(db.get(txn, bb(5))); + } + } + + @Test + public void putAndGetAndDeleteWithInternalTx() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + db.put(bb(5), bb(5)); + try (Txn txn = env.txnRead()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertNotNull(found); + assertThat(txn.val().getInt(), is(5)); + } + assertThat(db.delete(bb(5)), is(true)); + assertThat(db.delete(bb(5)), is(false)); + try (Txn txn = env.txnRead()) { - db.get(txn, bb(50)); + assertNull(db.get(txn, bb(5))); + } + } + + @Test + public void putCommitGet() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + txn.commit(); + } + + try (Txn txn = env.txnWrite()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertNotNull(found); + assertThat(txn.val().getInt(), is(5)); + } + } + + @Test + public void putCommitGetByteArray() throws IOException { + final File path = tmp.newFile(); + try (Env envBa = create(PROXY_BA) + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(1) + .setMaxDbs(2) + .open(path, MDB_NOSUBDIR)) { + final Dbi db = envBa.openDbi(DB_1, MDB_CREATE); + try (Txn txn = envBa.txnWrite()) { + db.put(txn, ba(5), ba(5)); + txn.commit(); + } + try (Txn txn = envBa.txnWrite()) { + final byte[] found = db.get(txn, ba(5)); + assertNotNull(found); + assertThat(new UnsafeBuffer(txn.val()).getInt(0), is(5)); + } + } + } + + @Test + public void putDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + assertThat(db.delete(txn, bb(5)), is(true)); + + assertNull(db.get(txn, bb(5))); + txn.abort(); } - } - }); - - for (final Integer key : keys) { - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(key), bb(3)); - txn.commit(); - } - } - - try (Txn txn = env.txnRead(); - CursorIterable ci = db.iterate(txn)) { - final Iterator> iter = ci.iterator(); - final List result = new ArrayList<>(); - while (iter.hasNext()) { - result.add(iter.next().key().getInt()); - } - - assertThat(result, Matchers.contains(keys.toArray(new Integer[0]))); - } - - proceed.set(false); - try { - reader.get(1, SECONDS); - pool.shutdown(); - pool.awaitTermination(1, SECONDS); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - throw new IllegalStateException(e); - } - } - - @Test - public void drop() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(1), bb(42)); - db.put(txn, bb(2), bb(42)); - assertThat(db.get(txn, bb(1)), not(nullValue())); - assertThat(db.get(txn, bb(2)), not(nullValue())); - db.drop(txn); - assertThat(db.get(txn, bb(1)), is(nullValue())); // data gone - assertThat(db.get(txn, bb(2)), is(nullValue())); - db.put(txn, bb(1), bb(42)); // ensure DB still works - db.put(txn, bb(2), bb(42)); - assertThat(db.get(txn, bb(1)), not(nullValue())); - assertThat(db.get(txn, bb(2)), not(nullValue())); - } - } - - @Test - public void dropAndDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); - final byte[] dbNameBytes = DB_1.getBytes(UTF_8); - final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); - dbNameBuffer.put(dbNameBytes).flip(); - - try (Txn txn = env.txnWrite()) { - assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); - db.drop(txn, true); - assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); - txn.commit(); - } - } - - @Test - public void dropAndDeleteAnonymousDb() { - env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); - final byte[] dbNameBytes = DB_1.getBytes(UTF_8); - final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); - dbNameBuffer.put(dbNameBytes).flip(); - - try (Txn txn = env.txnWrite()) { - assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); - nameDb.drop(txn, true); - assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); - txn.commit(); - } - - nameDb.close(); // explicit close after drop is OK - } - - @Test - public void getName() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - assertThat(db.getName(), is(DB_1.getBytes(UTF_8))); - } - - @Test - public void getNamesWhenDbisPresent() { - final byte[] dbHello = new byte[]{'h', 'e', 'l', 'l', 'o'}; - final byte[] dbWorld = new byte[]{'w', 'o', 'r', 'l', 'd'}; - env.openDbi(dbHello, MDB_CREATE); - env.openDbi(dbWorld, MDB_CREATE); - final List dbiNames = env.getDbiNames(); - assertThat(dbiNames, hasSize(2)); - assertThat(dbiNames.get(0), is(dbHello)); - assertThat(dbiNames.get(1), is(dbWorld)); - } - - @Test - public void getNamesWhenEmpty() { - final List dbiNames = env.getDbiNames(); - assertThat(dbiNames, empty()); - } - - @Test - public void listsFlags() { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, - MDB_REVERSEKEY); - - try (Txn txn = env.txnRead()) { - final List flags = dbi.listFlags(txn); - assertThat(flags, containsInAnyOrder(MDB_DUPSORT, MDB_REVERSEKEY)); - } - } - - @Test - public void putAbortGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(5), bb(5)); - txn.abort(); - } - - try (Txn txn = env.txnWrite()) { - assertNull(db.get(txn, bb(5))); - } - } - - @Test - public void putAndGetAndDeleteWithInternalTx() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - db.put(bb(5), bb(5)); - try (Txn txn = env.txnRead()) { - final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().getInt(), is(5)); - } - assertThat(db.delete(bb(5)), is(true)); - assertThat(db.delete(bb(5)), is(false)); - - try (Txn txn = env.txnRead()) { - assertNull(db.get(txn, bb(5))); - } - } - - @Test - public void putCommitGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(5), bb(5)); - txn.commit(); - } - - try (Txn txn = env.txnWrite()) { - final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().getInt(), is(5)); - } - } - - @Test - public void putCommitGetByteArray() throws IOException { - final File path = tmp.newFile(); - try (Env envBa = create(PROXY_BA) - .setMapSize(MEBIBYTES.toBytes(64)) - .setMaxReaders(1) - .setMaxDbs(2) - .open(path, MDB_NOSUBDIR)) { - final Dbi db = envBa.openDbi(DB_1, MDB_CREATE); - try (Txn txn = envBa.txnWrite()) { - db.put(txn, ba(5), ba(5)); - txn.commit(); - } - try (Txn txn = envBa.txnWrite()) { - final byte[] found = db.get(txn, ba(5)); - assertNotNull(found); - assertThat(new UnsafeBuffer(txn.val()).getInt(0), is(5)); - } - } - } - - @Test - public void putDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(5), bb(5)); - assertThat(db.delete(txn, bb(5)), is(true)); - - assertNull(db.get(txn, bb(5))); - txn.abort(); - } - } - - @Test - public void putDuplicateDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); - - try (Txn txn = env.txnWrite()) { - db.put(txn, bb(5), bb(5)); - db.put(txn, bb(5), bb(6)); - db.put(txn, bb(5), bb(7)); - assertThat(db.delete(txn, bb(5), bb(6)), is(true)); - assertThat(db.delete(txn, bb(5), bb(6)), is(false)); - assertThat(db.delete(txn, bb(5), bb(5)), is(true)); - assertThat(db.delete(txn, bb(5), bb(5)), is(false)); + } + + @Test + public void putDuplicateDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + db.put(txn, bb(5), bb(6)); + db.put(txn, bb(5), bb(7)); + assertThat(db.delete(txn, bb(5), bb(6)), is(true)); + assertThat(db.delete(txn, bb(5), bb(6)), is(false)); + assertThat(db.delete(txn, bb(5), bb(5)), is(true)); + assertThat(db.delete(txn, bb(5), bb(5)), is(false)); + + try (Cursor cursor = db.openCursor(txn)) { + final ByteBuffer key = bb(5); + cursor.get(key, MDB_SET_KEY); + assertThat(cursor.count(), is(1L)); + } + txn.abort(); + } + } + + @Test + public void putReserve() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Cursor cursor = db.openCursor(txn)) { final ByteBuffer key = bb(5); - cursor.get(key, MDB_SET_KEY); - assertThat(cursor.count(), is(1L)); - } - txn.abort(); - } - } - - @Test - public void putReserve() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - final ByteBuffer key = bb(5); - try (Txn txn = env.txnWrite()) { - assertNull(db.get(txn, key)); - final ByteBuffer val = db.reserve(txn, key, 32, MDB_NOOVERWRITE); - val.putLong(MAX_VALUE); - assertNotNull(db.get(txn, key)); - txn.commit(); - } - try (Txn txn = env.txnWrite()) { - final ByteBuffer val = db.get(txn, key); - assertThat(val.capacity(), is(32)); - assertThat(val.getLong(), is(MAX_VALUE)); - assertThat(val.getLong(8), is(0L)); - } - } - - @Test - public void putZeroByteValueForNonMdbDupSortDatabase() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - final ByteBuffer val = allocateDirect(0); - db.put(txn, bb(5), val); - txn.commit(); - } - - try (Txn txn = env.txnRead()) { - final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().capacity(), is(0)); - } - } - - @Test - public void returnValueForNoDupData() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); - try (Txn txn = env.txnWrite()) { - // ok - assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA), is(true)); - assertThat(db.put(txn, bb(5), bb(7), MDB_NODUPDATA), is(true)); - assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA), is(false)); - } - } - - @Test - public void returnValueForNoOverwrite() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - // ok - assertThat(db.put(txn, bb(5), bb(6), MDB_NOOVERWRITE), is(true)); - // fails, but gets exist val - assertThat(db.put(txn, bb(5), bb(8), MDB_NOOVERWRITE), is(false)); - assertThat(txn.val().getInt(0), is(6)); - } - } - - @Test - public void stats() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - db.put(bb(1), bb(42)); - db.put(bb(2), bb(42)); - db.put(bb(3), bb(42)); - final Stat stat; - try (Txn txn = env.txnRead()) { - stat = db.stat(txn); - } - assertThat(stat, is(notNullValue())); - assertThat(stat.branchPages, is(0L)); - assertThat(stat.depth, is(1)); - assertThat(stat.entries, is(3L)); - assertThat(stat.leafPages, is(1L)); - assertThat(stat.overflowPages, is(0L)); - assertThat(stat.pageSize, is(4_096)); - } - - @Test(expected = MapFullException.class) - @SuppressWarnings("PMD.PreserveStackTrace") - public void testMapFullException() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - final ByteBuffer v; - try { - v = allocateDirect(1_024 * 1_024 * 1_024); - } catch (final OutOfMemoryError e) { - // Travis CI OS X build cannot allocate this much memory, so assume OK - throw new MapFullException(); - } - db.put(txn, bb(1), v); - } - } - - @Test - public void testParallelWritesStress() { - if (getProperty("os.name").startsWith("Windows")) { - return; // Windows VMs run this test too slowly - } - - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - // Travis CI has 1.5 cores for legacy builds - nCopies(2, null).parallelStream() - .forEach(ignored -> { - for (int i = 0; i < 15_000; i++) { - db.put(bb(i), bb(i)); - } - }); - } + try (Txn txn = env.txnWrite()) { + assertNull(db.get(txn, key)); + final ByteBuffer val = db.reserve(txn, key, 32, MDB_NOOVERWRITE); + val.putLong(MAX_VALUE); + assertNotNull(db.get(txn, key)); + txn.commit(); + } + try (Txn txn = env.txnWrite()) { + final ByteBuffer val = db.get(txn, key); + assertThat(val.capacity(), is(32)); + assertThat(val.getLong(), is(MAX_VALUE)); + assertThat(val.getLong(8), is(0L)); + } + } + + @Test + public void putZeroByteValueForNonMdbDupSortDatabase() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + final ByteBuffer val = allocateDirect(0); + db.put(txn, bb(5), val); + txn.commit(); + } + + try (Txn txn = env.txnRead()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertNotNull(found); + assertThat(txn.val().capacity(), is(0)); + } + } + + @Test + public void returnValueForNoDupData() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite()) { + // ok + assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA), is(true)); + assertThat(db.put(txn, bb(5), bb(7), MDB_NODUPDATA), is(true)); + assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA), is(false)); + } + } + + @Test + public void returnValueForNoOverwrite() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + // ok + assertThat(db.put(txn, bb(5), bb(6), MDB_NOOVERWRITE), is(true)); + // fails, but gets exist val + assertThat(db.put(txn, bb(5), bb(8), MDB_NOOVERWRITE), is(false)); + assertThat(txn.val().getInt(0), is(6)); + } + } + + @Test + public void stats() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + db.put(bb(1), bb(42)); + db.put(bb(2), bb(42)); + db.put(bb(3), bb(42)); + final Stat stat; + try (Txn txn = env.txnRead()) { + stat = db.stat(txn); + } + assertThat(stat, is(notNullValue())); + assertThat(stat.branchPages, is(0L)); + assertThat(stat.depth, is(1)); + assertThat(stat.entries, is(3L)); + assertThat(stat.leafPages, is(1L)); + assertThat(stat.overflowPages, is(0L)); + assertThat(stat.pageSize, is(4_096)); + } + + @Test(expected = MapFullException.class) + @SuppressWarnings("PMD.PreserveStackTrace") + public void testMapFullException() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + final ByteBuffer v; + try { + v = allocateDirect(1_024 * 1_024 * 1_024); + } catch (final OutOfMemoryError e) { + // Travis CI OS X build cannot allocate this much memory, so assume OK + throw new MapFullException(); + } + db.put(txn, bb(1), v); + } + } + + @Test + public void testParallelWritesStress() { + if (getProperty("os.name").startsWith("Windows")) { + return; // Windows VMs run this test too slowly + } + + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + // Travis CI has 1.5 cores for legacy builds + nCopies(2, null).parallelStream() + .forEach(ignored -> { + for (int i = 0; i < 15_000; i++) { + db.put(bb(i), bb(i)); + } + }); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsOpenCall() { + env.close(); + env.openDbi(DB_1, MDB_CREATE); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsCloseCall() { + doEnvClosedTest( + null, + (db, txn) -> db.close()); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsGetCall() { + doEnvClosedTest( + (db, txn) -> { + final ByteBuffer valBuf = db.get(txn, bb(1)); + assertThat(valBuf.getInt(), is(10)); + }, + (db, txn) -> db.get(txn, bb(2))); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsPutCall() { + doEnvClosedTest( + null, + (db, txn) -> db.put(bb(5), bb(50))); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsPutWithTxnCall() { + doEnvClosedTest( + null, + (db, txn) -> { + db.put(txn, bb(5), bb(50)); + }); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsIterateCall() { + doEnvClosedTest(null, Dbi::iterate); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsDropCall() { + doEnvClosedTest( + null, + Dbi::drop); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsDropAndDeleteCall() { + doEnvClosedTest( + null, + (db, txn) -> db.drop(txn, true)); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsOpenCursorCall() { + doEnvClosedTest( + null, + Dbi::openCursor); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsReserveCall() { + doEnvClosedTest( + null, + (db, txn) -> db.reserve(txn, bb(1), 32, MDB_NOOVERWRITE)); + } + + @Test(expected = AlreadyClosedException.class) + public void closedEnvRejectsStatCall() { + doEnvClosedTest(null, Dbi::stat); + } + + private void doEnvClosedTest(final BiConsumer, Txn> workBeforeEnvClosed, + final BiConsumer, Txn> workAfterEnvClose) { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + 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 txn = env.txnWrite()) { + + if (workBeforeEnvClosed != null) { + workBeforeEnvClosed.accept(db, txn); + } + + env.close(); + + if (workAfterEnvClose != null) { + workAfterEnvClose.accept(db, txn); + } + } + } } diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index bf766ff7..39e4d3a5 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -40,7 +40,11 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Iterator; import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Rule; @@ -152,6 +156,73 @@ public void cannotSyncOnceClosed() throws IOException { env.sync(false); } + @Test + public void cannotReadOnceClosed() throws IOException, InterruptedException, ExecutionException { + final File path = tmp.newFile(); + final Env env = create() + .setMaxReaders(5) + .open(path, MDB_NOSUBDIR); + + final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); + + dbi.put(bb(1), bb(10)); + dbi.put(bb(2), bb(20)); + + try (Txn roTxn = env.txnRead()) { + assertThat(dbi.get(roTxn, bb(1)).getInt(), is(10)); + assertThat(dbi.get(roTxn, bb(2)).getInt(), is(20)); + } + + System.out.println("Done puts"); + + final CountDownLatch firstGetFinishedLatch = new CountDownLatch(1); + final CountDownLatch envClosedLatch = new CountDownLatch(1); + + final CompletableFuture future = CompletableFuture.runAsync(() -> { + + System.out.println("Running async task"); + try (Txn roTxn = env.txnRead()) { + try (CursorIterable iterable = dbi.iterate(roTxn, KeyRange.all())) { + final Iterator> iterator = iterable.iterator(); + + try { + System.out.println("Getting first entry"); + CursorIterable.KeyVal keyVal = iterator.next(); + assertThat(keyVal.val().getInt(), is(10)); + + firstGetFinishedLatch.countDown(); + + try { + System.out.println("Waiting for env to close"); + envClosedLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + assertThat(env.isClosed(), is(true)); + + System.out.println("Getting second entry, but env is now closed"); + keyVal = iterator.next(); + assertThat(keyVal.val().getInt(), is(20)); + } catch (RuntimeException e) { + e.printStackTrace(); + } + } + } + }); + + System.out.println("Waiting for cursor to get first entry"); + firstGetFinishedLatch.await(); + + System.out.println("Closing env"); + env.close(); + envClosedLatch.countDown(); + + System.out.println("Waiting for completion"); + future.get(); + } + @Test public void copyDirectoryBased() throws IOException { final File dest = tmp.newFolder(); diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 69f0815f..85ff73fe 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -230,6 +230,42 @@ public void txConstructionDeniedIfEnvClosed() { env.txnRead(); } + @Test(expected = AlreadyClosedException.class) + public void txRenewDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + txnRead.close(); + env.close(); + txnRead.renew(); + } + + @Test(expected = AlreadyClosedException.class) + public void txCloseDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.close(); + } + + @Test(expected = AlreadyClosedException.class) + public void txCommitDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.commit(); + } + + @Test(expected = AlreadyClosedException.class) + public void txAbortDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.abort(); + } + + @Test(expected = AlreadyClosedException.class) + public void txResetDeniedIfEnvClosed() { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.reset(); + } + @Test public void txParent() { try (Txn txRoot = env.txnWrite(); @@ -239,6 +275,15 @@ public void txParent() { } } + @Test(expected = AlreadyClosedException.class) + public void txParentDeniedIfEnvClosed() { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot)) { + env.close(); + txChild.getParent(); + } + } + @Test(expected = IncompatibleParent.class) public void txParentROChildRWIncompatible() { try (Txn txRoot = env.txnRead()) { From 303c9c054c65da50681ec2898b5d3851c09e2e69 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:52:13 +0000 Subject: [PATCH 2/4] gh-185 Add env.checkNotClosed to Dbi.put --- src/main/java/org/lmdbjava/Dbi.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index f8be2239..49909979 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -402,6 +402,7 @@ public boolean put(final Txn txn, final T key, final T val, requireNonNull(txn); requireNonNull(key); requireNonNull(val); + env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } From 1bc841eb76d830818fe417dba2756c897dc04965 Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:54:54 +0000 Subject: [PATCH 3/4] gh-185 Remove redundant test --- src/test/java/org/lmdbjava/EnvTest.java | 67 ------------------------- 1 file changed, 67 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 39e4d3a5..b681ec27 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -156,73 +156,6 @@ public void cannotSyncOnceClosed() throws IOException { env.sync(false); } - @Test - public void cannotReadOnceClosed() throws IOException, InterruptedException, ExecutionException { - final File path = tmp.newFile(); - final Env env = create() - .setMaxReaders(5) - .open(path, MDB_NOSUBDIR); - - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); - - dbi.put(bb(1), bb(10)); - dbi.put(bb(2), bb(20)); - - try (Txn roTxn = env.txnRead()) { - assertThat(dbi.get(roTxn, bb(1)).getInt(), is(10)); - assertThat(dbi.get(roTxn, bb(2)).getInt(), is(20)); - } - - System.out.println("Done puts"); - - final CountDownLatch firstGetFinishedLatch = new CountDownLatch(1); - final CountDownLatch envClosedLatch = new CountDownLatch(1); - - final CompletableFuture future = CompletableFuture.runAsync(() -> { - - System.out.println("Running async task"); - try (Txn roTxn = env.txnRead()) { - try (CursorIterable iterable = dbi.iterate(roTxn, KeyRange.all())) { - final Iterator> iterator = iterable.iterator(); - - try { - System.out.println("Getting first entry"); - CursorIterable.KeyVal keyVal = iterator.next(); - assertThat(keyVal.val().getInt(), is(10)); - - firstGetFinishedLatch.countDown(); - - try { - System.out.println("Waiting for env to close"); - envClosedLatch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - - assertThat(env.isClosed(), is(true)); - - System.out.println("Getting second entry, but env is now closed"); - keyVal = iterator.next(); - assertThat(keyVal.val().getInt(), is(20)); - } catch (RuntimeException e) { - e.printStackTrace(); - } - } - } - }); - - System.out.println("Waiting for cursor to get first entry"); - firstGetFinishedLatch.await(); - - System.out.println("Closing env"); - env.close(); - envClosedLatch.countDown(); - - System.out.println("Waiting for completion"); - future.get(); - } - @Test public void copyDirectoryBased() throws IOException { final File dest = tmp.newFolder(); From 46e41ff399e41b9a6993f26f90f5b6e09a1c965a Mon Sep 17 00:00:00 2001 From: at055612 <22818309+at055612@users.noreply.github.com> Date: Thu, 4 Nov 2021 17:12:55 +0000 Subject: [PATCH 4/4] gh-185 Remove unused imports --- src/test/java/org/lmdbjava/EnvTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index b681ec27..bf766ff7 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -40,11 +40,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Iterator; import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Rule;