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:
leventov
2016-12-26 14:27:32 +08:00
parent 4bb2decc34
commit fa70e216bd
10 changed files with 314 additions and 145 deletions
@@ -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