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..49909979 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); } /** @@ -392,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(); } @@ -434,6 +445,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 +467,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/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()) {