mirror of
https://github.com/OpenHFT/Chronicle-Map.git
synced 2021-09-13 01:26:12 +00:00
Improvements to ChronicleMap resource closing subsystem, making it more robust in exotic scenarios (OutOfMemoryError, StackOverflowError) and be able to close after incorrect attempt to close when a context is open in the same thread
This commit is contained in:
@@ -17,11 +17,13 @@
|
||||
|
||||
package net.openhft.chronicle.map;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import net.openhft.chronicle.bytes.NoBytesStore;
|
||||
import net.openhft.chronicle.core.OS;
|
||||
import net.openhft.chronicle.core.values.IntValue;
|
||||
import net.openhft.chronicle.hash.serialization.impl.StringSizedReader;
|
||||
import net.openhft.chronicle.hash.serialization.impl.StringUtf8DataAccess;
|
||||
import net.openhft.chronicle.map.impl.QueryContextInterface;
|
||||
import net.openhft.chronicle.values.Values;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
@@ -33,10 +35,15 @@ import org.junit.runners.Parameterized;
|
||||
import sun.misc.Cleaner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -45,27 +52,29 @@ public class MemoryLeaksTest {
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][] {
|
||||
{ false, false }, { false, true }, { true, false }, { true, true }
|
||||
});
|
||||
List<Boolean> booleans = Arrays.asList(false, true);
|
||||
// Test with all possible combinations of three boolean parameters.
|
||||
return Lists.cartesianProduct(booleans, booleans, booleans)
|
||||
.stream().map(List::toArray).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
private boolean replicated;
|
||||
private boolean persisted;
|
||||
private ChronicleMapBuilder<IntValue, String> builder;
|
||||
private boolean closeWithinContext;
|
||||
/**
|
||||
* Accounting {@link CountedStringReader} creation and finalization. All serializers,
|
||||
* created since the map creation, should become unreachable after map.close() or collection by
|
||||
* Cleaner, it means that map contexts (referencing serializers) are collected by the GC
|
||||
*/
|
||||
private final AtomicInteger serializerCount = new AtomicInteger();
|
||||
private final List<WeakReference<CountedStringReader>> serializers = new ArrayList<>();
|
||||
|
||||
public MemoryLeaksTest(boolean replicated, boolean persisted) {
|
||||
this.replicated = replicated;
|
||||
public MemoryLeaksTest(boolean replicated, boolean persisted, boolean closeWithinContext) {
|
||||
this.persisted = persisted;
|
||||
this.closeWithinContext = closeWithinContext;
|
||||
builder = ChronicleMap
|
||||
.of(IntValue.class, String.class)
|
||||
.valueReaderAndDataAccess(new CountedStringReader(), new StringUtf8DataAccess());
|
||||
@@ -89,10 +98,13 @@ public class MemoryLeaksTest {
|
||||
throws IOException, InterruptedException {
|
||||
long nativeMemoryUsedBeforeMap = nativeMemoryUsed();
|
||||
int serializersBeforeMap = serializerCount.get();
|
||||
WeakReference<ChronicleMap<IntValue, String>> ref = new WeakReference<>(getMap());
|
||||
Assert.assertNotNull(ref.get());
|
||||
long expectedNativeMemory = nativeMemoryUsedBeforeMap + ref.get().offHeapMemoryUsed();
|
||||
ChronicleMap<IntValue, String> map = getMap();
|
||||
long expectedNativeMemory = nativeMemoryUsedBeforeMap + map.offHeapMemoryUsed();
|
||||
assertEquals(expectedNativeMemory, nativeMemoryUsed());
|
||||
tryCloseFromContext(map);
|
||||
WeakReference<ChronicleMap<IntValue, String>> ref = new WeakReference<>(map);
|
||||
Assert.assertNotNull(ref.get());
|
||||
map = null;
|
||||
// Wait until Map is collected by GC
|
||||
while (ref.get() != null) {
|
||||
System.gc();
|
||||
@@ -120,17 +132,40 @@ public class MemoryLeaksTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(timeout = 60_000)
|
||||
public void testExplicitChronicleMapCloseReleasesMemory()
|
||||
throws IOException, InterruptedException {
|
||||
long nativeMemoryUsedBeforeMap = nativeMemoryUsed();
|
||||
int serializersBeforeMap = serializerCount.get();
|
||||
try (ChronicleMap<IntValue, String> map = getMap()) {
|
||||
ChronicleMap<IntValue, String> map = getMap();
|
||||
// One serializer should be copied to the map's valueReader field, another is copied from
|
||||
// the map's valueReader field to the context
|
||||
Assert.assertTrue(serializerCount.get() >= serializersBeforeMap + 2);
|
||||
Assert.assertNotEquals(0, map.offHeapMemoryUsed());
|
||||
try {
|
||||
long expectedNativeMemory = nativeMemoryUsedBeforeMap + map.offHeapMemoryUsed();
|
||||
assertEquals(expectedNativeMemory, nativeMemoryUsed());
|
||||
} finally {
|
||||
tryCloseFromContext(map);
|
||||
map.close();
|
||||
}
|
||||
assertEquals(nativeMemoryUsedBeforeMap, nativeMemoryUsed());
|
||||
// Wait until chronicle map context (hence serializers) is collected by the GC
|
||||
for (int i = 0; i < 6_000; i++) {
|
||||
if (serializerCount.get() == serializersBeforeMap + 1)
|
||||
break;
|
||||
System.gc();
|
||||
byte[] garbage = new byte[10_000_000];
|
||||
Thread.sleep(10);
|
||||
}
|
||||
Assert.assertEquals(serializersBeforeMap + 1, serializerCount.get());
|
||||
// This assertion ensures GC doesn't reclaim the map before or during the loop iteration
|
||||
// above, to ensure that we test that the direct memory and contexts are released because
|
||||
// of the manual map.close(), despite the "leak" of the map object itself.
|
||||
Assert.assertEquals(0, map.offHeapMemoryUsed());
|
||||
map = null;
|
||||
// After cleaning the reference to the map, the last serializer, which was held in it's
|
||||
// field, should be cleaned as well
|
||||
for (int i = 0; i < 6_000; i++) {
|
||||
if (serializerCount.get() == serializersBeforeMap)
|
||||
break;
|
||||
@@ -158,13 +193,39 @@ public class MemoryLeaksTest {
|
||||
return map;
|
||||
}
|
||||
|
||||
private void tryCloseFromContext(ChronicleMap<IntValue, String> map) {
|
||||
// Test that the map could still be successfully closed and no leaks are introduced
|
||||
// by an attempt to close the map from within context.
|
||||
if (closeWithinContext) {
|
||||
IntValue key = Values.newHeapInstance(IntValue.class);
|
||||
try (ExternalMapQueryContext<IntValue, String, ?> c = map.queryContext(key)) {
|
||||
c.updateLock().lock();
|
||||
try {
|
||||
map.close();
|
||||
} catch (IllegalStateException expected) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CountedStringReader extends StringSizedReader {
|
||||
|
||||
private final String creationStackTrace;
|
||||
private final Cleaner cleaner;
|
||||
|
||||
CountedStringReader() {
|
||||
serializerCount.incrementAndGet();
|
||||
serializers.add(new WeakReference<>(this));
|
||||
cleaner = Cleaner.create(this, serializerCount::decrementAndGet);
|
||||
try (StringWriter stringWriter = new StringWriter();
|
||||
PrintWriter printWriter = new PrintWriter(stringWriter)) {
|
||||
new Exception().printStackTrace(printWriter);
|
||||
printWriter.flush();
|
||||
creationStackTrace = stringWriter.toString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user