Skip to content

Hold strong reference to all buffers passed to LmdbJava (fixes #207) #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
<version>0.9.29-1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand All @@ -110,6 +116,8 @@
</usedDependencies>
<ignoredDependencies>
<ignoredDependency>com.github.jnr:jffi</ignoredDependency>
<ignoredDependency>org.mockito:mockito-core</ignoredDependency>
<ignoredDependency>org.mockito:mockito-inline</ignoredDependency>
</ignoredDependencies>
</configuration>
</plugin>
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/org/lmdbjava/Cursor.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public boolean get(final T key, final T data, final SeekOp op) {
checkRc(rc);
kv.keyOut();
kv.valOut();
ReferenceUtil.reachabilityFence0(key);
return true;
}

Expand Down Expand Up @@ -192,6 +193,7 @@ public boolean get(final T key, final GetOp op) {
checkRc(rc);
kv.keyOut();
kv.valOut();
ReferenceUtil.reachabilityFence0(key);
return true;
}

Expand Down Expand Up @@ -266,6 +268,8 @@ public boolean put(final T key, final T val, final PutFlags... op) {
return false;
}
checkRc(rc);
ReferenceUtil.reachabilityFence0(key);
ReferenceUtil.reachabilityFence0(val);
return true;
}

Expand Down Expand Up @@ -304,6 +308,8 @@ public void putMultiple(final T key, final T val, final int elements,
final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(),
dataPtr, mask);
checkRc(rc);
ReferenceUtil.reachabilityFence0(key);
ReferenceUtil.reachabilityFence0(val);
}

/**
Expand Down Expand Up @@ -362,6 +368,7 @@ public T reserve(final T key, final int size, final PutFlags... op) {
checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(),
flags));
kv.valOut();
ReferenceUtil.reachabilityFence0(key);
return val();
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/lmdbjava/Dbi.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ public boolean delete(final Txn<T> txn, final T key, final T val) {
return false;
}
checkRc(rc);
ReferenceUtil.reachabilityFence0(key);
ReferenceUtil.reachabilityFence0(val);
return true;
}

Expand Down Expand Up @@ -239,6 +241,7 @@ public T get(final Txn<T> txn, final T key) {
return null;
}
checkRc(rc);
ReferenceUtil.reachabilityFence0(key);
return txn.kv().valOut(); // marked as out in LMDB C docs
}

Expand Down Expand Up @@ -386,6 +389,8 @@ public boolean put(final Txn<T> txn, final T key, final T val,
return false;
}
checkRc(rc);
ReferenceUtil.reachabilityFence0(key);
ReferenceUtil.reachabilityFence0(val);
return true;
}

Expand Down Expand Up @@ -421,6 +426,7 @@ public T reserve(final Txn<T> txn, final T key, final int size,
checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv()
.pointerVal(), flags));
txn.kv().valOut(); // marked as in,out in LMDB C docs
ReferenceUtil.reachabilityFence0(key);
return txn.val();
}

Expand Down
55 changes: 55 additions & 0 deletions src/main/java/org/lmdbjava/ReferenceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*-
* #%L
* LmdbJava
* %%
* Copyright (C) 2016 - 2023 The LmdbJava Open Source Project
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

package org.lmdbjava;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public final class ReferenceUtil {
/**
* Ensures that the object referenced by the given reference remains
* <a href="package-summary.html#reachability"><em>strongly reachable</em></a>,
* regardless of any prior actions of the program that might otherwise cause
* the object to become unreachable; thus, the referenced object is not
* reclaimable by garbage collection at least until after the invocation of
* this method.
*
* <p> Recent versions of the JDK have a nasty habit of prematurely deciding objects are unreachable.
* see: https://stackoverflow.com/questions/26642153/finalize-called-on-strongly-reachable-object-in-java-8
* The Java 9 method Reference.reachabilityFence offers a solution to this problem.
*
* <p> This method is always implemented as a synchronization on {@code ref}, not as
* {@code Reference.reachabilityFence} for consistency across platforms and to allow building on JDK 6-8.
* <b>It is the caller's responsibility to ensure that this synchronization will not cause deadlock.</b>
*
* @param ref the reference. If {@code null}, this method has no effect.
* @see https://github.com/netty/netty/pull/8410
*/
@SuppressFBWarnings({"ESync_EMPTY_SYNC", "UC_USELESS_VOID_METHOD"})
public static void reachabilityFence0(Object ref) {
if (ref != null) {
synchronized (ref) {
// Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521
}
}
}

private ReferenceUtil() {}
}
111 changes: 111 additions & 0 deletions src/test/java/org/lmdbjava/GarbageCollectionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*-
* #%L
* LmdbJava
* %%
* Copyright (C) 2016 - 2023 The LmdbJava Open Source Project
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

package org.lmdbjava;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;

import static java.nio.ByteBuffer.allocateDirect;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import static org.lmdbjava.DbiFlags.MDB_CREATE;
import static org.lmdbjava.Env.create;

@SuppressFBWarnings({"DM_GC", "RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"})
@SuppressWarnings("PMD.DoNotCallGarbageCollectionExplicitly")
public class GarbageCollectionTest {

private static final String DB_NAME = "my DB";
private static final String KEY_PREFIX = "Uncorruptedkey";
private static final String VAL_PREFIX = "Uncorruptedval";

@Rule
public final TemporaryFolder tmp = new TemporaryFolder();
private void putBuffer(Dbi<ByteBuffer> db, Txn<ByteBuffer> txn, int i) {
ByteBuffer key = allocateDirect(24);
ByteBuffer val = allocateDirect(24);
key.put((KEY_PREFIX+i).getBytes(UTF_8)).flip();
val.put((VAL_PREFIX+i).getBytes(UTF_8)).flip();
db.put(txn, key, val);
}
@Test
public void buffersNotGarbageCollectedTest() throws IOException {
final File path = tmp.newFolder();

final Env<ByteBuffer> env = create()
.setMapSize(2_085_760_999)
.setMaxDbs(1)
.open(path);
final Dbi<ByteBuffer> db = env.openDbi(DB_NAME, MDB_CREATE);

// Trigger compilation and whatnot
try (Txn<ByteBuffer> txn = env.txnWrite()) {
for (int i = 0; i < 5000; i++) {
putBuffer(db, txn, i);
}
txn.commit();
}
// Call gc before writing to lmdb and after last reference to buffer by changing the behavior of mask
try (MockedStatic<MaskedFlag> mockedStatic = Mockito.mockStatic(MaskedFlag.class)) {
mockedStatic.when(MaskedFlag::mask).thenAnswer(invocationOnMock -> {
System.gc();
return 0;
});
try (Txn<ByteBuffer> txn = env.txnWrite()) {
for (int i = 0; i < 1000; i++) {
putBuffer(db, txn, i);
}
txn.commit();
}
}

// Find corrupt keys
try (Txn<ByteBuffer> txn = env.txnRead()) {
try (Cursor<ByteBuffer> c = db.openCursor(txn)) {
if (c.first()) {
do {
byte[] rkey = new byte[c.key().remaining()];
c.key().get(rkey);
byte[] rval = new byte[c.val().remaining()];
c.val().get(rval);
String skey = new String(rkey, UTF_8);
String sval = new String(rval, UTF_8);
if (!skey.startsWith("Uncorruptedkey")) {
fail("Found corrupt key " + skey);
}
if (!sval.startsWith("Uncorruptedval")) {
fail("Found corrupt val " + sval);
}
} while (c.next());
}
}
}
env.close();
}
}