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:
@@ -24,8 +24,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ChronicleHashResources is Runnable to be passed as "hunk" to {@link sun.misc.Cleaner}.
|
||||
@@ -33,51 +35,59 @@ import java.util.*;
|
||||
public abstract class ChronicleHashResources implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChronicleHashResources.class);
|
||||
|
||||
private static final int OPEN = 0;
|
||||
private static final int PARTIALLY_CLOSED = 1;
|
||||
private static final int COMPLETELY_CLOSED = 2;
|
||||
|
||||
/**
|
||||
* closed field is volatile and {@link #closed()} accessor is not synchronized, because it
|
||||
* is called on each ChronicleMap access from all threads, synchronization would make closed()
|
||||
* a bottleneck.
|
||||
* This field is volatile and {@link #closed()} accessor is not synchronized, because it
|
||||
* is called on each {@link VanillaChronicleHash} access from all threads, synchronization would
|
||||
* make {@code closed()} a bottleneck.
|
||||
*/
|
||||
private volatile boolean closed = false;
|
||||
private volatile int state = OPEN;
|
||||
private List<MemoryResource> memoryResources = new ArrayList<>();
|
||||
private List<Closeable> closeables = new ArrayList<>(1);
|
||||
private ArrayList<WeakReference<ContextHolder>> contexts = new ArrayList<>();
|
||||
private List<WeakReference<ContextHolder>> contexts = new ArrayList<>();
|
||||
/**
|
||||
* Identity String of the ChronicleHash, for which this ChronicleHashResources is created.
|
||||
* ChronicleHash couldn't be directly referenced, because it's a hunk for {@link
|
||||
* sun.misc.Cleaner}, and it would prevent the chronicleHash from ever becoming unreachable.
|
||||
* ChronicleHash couldn't be directly referenced, because {@code ChronicleHashResources} is a
|
||||
* hunk for {@link sun.misc.Cleaner}, and it would prevent the chronicleHash from ever becoming
|
||||
* unreachable.
|
||||
*/
|
||||
private String chronicleHashIdentityString;
|
||||
|
||||
List<MemoryResource> memoryResources() {
|
||||
return memoryResources;
|
||||
}
|
||||
|
||||
List<WeakReference<ContextHolder>> contexts() {
|
||||
return contexts;
|
||||
}
|
||||
|
||||
final synchronized long totalMemory() {
|
||||
if (closed)
|
||||
if (closed())
|
||||
return 0L;
|
||||
long totalMemory = 0L;
|
||||
for (MemoryResource memoryResource : memoryResources) {
|
||||
totalMemory += memoryResource.size;
|
||||
//noinspection ForLoopReplaceableByForEach -- allocation-free looping
|
||||
for (int i = 0; i < memoryResources.size(); i++) {
|
||||
totalMemory += memoryResources.get(i).size;
|
||||
}
|
||||
return totalMemory;
|
||||
}
|
||||
|
||||
final boolean closed() {
|
||||
return closed;
|
||||
return state != OPEN;
|
||||
}
|
||||
|
||||
private void checkOpen() {
|
||||
if (closed)
|
||||
if (closed())
|
||||
throw new ChronicleHashClosedException(chronicleHashIdentityString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Need to set {@link #chronicleHashIdentityString} via this setter rather than once in the
|
||||
* constructor, because {@code ChronicleHashResources} instance is created before it's
|
||||
* {@link net.openhft.chronicle.hash.ChronicleHash}.
|
||||
*/
|
||||
public final synchronized void setChronicleHashIdentityString(
|
||||
String chronicleHashIdentityString) {
|
||||
checkOpen();
|
||||
this.chronicleHashIdentityString = chronicleHashIdentityString;
|
||||
}
|
||||
|
||||
@@ -86,7 +96,7 @@ public abstract class ChronicleHashResources implements Runnable {
|
||||
memoryResources.add(new MemoryResource(address, size));
|
||||
}
|
||||
|
||||
public final synchronized void addCloseable(Closeable closeable) {
|
||||
final synchronized void addCloseable(Closeable closeable) {
|
||||
checkOpen();
|
||||
closeables.add(closeable);
|
||||
}
|
||||
@@ -111,20 +121,20 @@ public abstract class ChronicleHashResources implements Runnable {
|
||||
public void run() {
|
||||
Throwable thrown = null;
|
||||
try {
|
||||
if (closed)
|
||||
if (state == COMPLETELY_CLOSED)
|
||||
return;
|
||||
try {
|
||||
LOG.error("{} not closed manually, cleaned up from Cleaner",
|
||||
LOG.error("{} is not closed manually, cleaned up from Cleaner",
|
||||
chronicleHashIdentityString);
|
||||
} catch (Throwable t) {
|
||||
thrown = t;
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
if (closed) {
|
||||
if (state == COMPLETELY_CLOSED) {
|
||||
LOG.error("Somebody closed {} while it is processed by Cleaner, " +
|
||||
"this should be impossible", chronicleHashIdentityString);
|
||||
} else {
|
||||
thrown = Throwables.returnOrSuppress(thrown, releaseEverything());
|
||||
thrown = Throwables.returnOrSuppress(thrown, releaseEverything(true));
|
||||
}
|
||||
}
|
||||
if (thrown != null) {
|
||||
@@ -140,82 +150,152 @@ public abstract class ChronicleHashResources implements Runnable {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
// just don't fail anyway
|
||||
// Just don't fail anyway. We will have another attempt to close this ChronicleMap from
|
||||
// ChronicleHashCloseOnExitHook.
|
||||
}
|
||||
}
|
||||
|
||||
abstract Throwable releaseSystemResources();
|
||||
abstract void releaseMemoryResource(MemoryResource memoryResource) throws IOException;
|
||||
|
||||
Throwable releaseExtraSystemResources() {
|
||||
// nothing to release
|
||||
return null;
|
||||
}
|
||||
|
||||
public final boolean releaseManually() {
|
||||
if (closed)
|
||||
if (state == COMPLETELY_CLOSED)
|
||||
return false;
|
||||
synchronized (this) {
|
||||
if (closed)
|
||||
if (state == COMPLETELY_CLOSED)
|
||||
return false;
|
||||
Throwable thrown = releaseEverything();
|
||||
Throwable thrown = releaseEverything(false);
|
||||
if (thrown != null)
|
||||
throw Throwables.propagate(thrown);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private Throwable releaseEverything() {
|
||||
// It's important to set closed = true before calling closeContexts(), to ensure that
|
||||
// threads which access this chronicleHash for the first time concurrently with
|
||||
private Throwable releaseEverything(boolean releaseFromCleaner) {
|
||||
// It's important to set state = PARTIALLY_CLOSED before calling closeContexts(), to ensure
|
||||
// that threads which access this chronicleHash for the first time concurrently with
|
||||
// chronicleHash.close() will either fail to register the context via addContext() (because
|
||||
// checkOpen() is called there), or either the registered contexts will be visible and
|
||||
// closed in closeContexts()
|
||||
closed = true;
|
||||
// closed in closeContexts(). Also, it allows to not care about null elements of
|
||||
// memoryResources, contexts and closeables lists in addMemoryResource(), addContext() and
|
||||
// addCloseable() respectively.
|
||||
state = PARTIALLY_CLOSED;
|
||||
|
||||
// Paranoiac mode: methods closeContexts(), closeCloseables(), releaseSystemResources() and
|
||||
// Throwables.returnOrSuppress() should never throw any throwables (the first three should
|
||||
// return them instead of throwing), but we don't trust ourselves and wrap every call into
|
||||
// it's own try-finally block
|
||||
// Paranoiac mode: methods closeContexts(), closeCloseables(), releaseMemoryResources(),
|
||||
// releaseExtraSystemResources() and Throwables.returnOrSuppress() should never throw any
|
||||
// throwables (the first three should return them instead of throwing), but we don't trust
|
||||
// ourselves and wrap every call into it's own try-finally block.
|
||||
Throwable thrown = null;
|
||||
try {
|
||||
Throwable thrown = null;
|
||||
try {
|
||||
if (contexts != null)
|
||||
thrown = closeContexts();
|
||||
} finally {
|
||||
try {
|
||||
} catch (Throwable t) {
|
||||
thrown = t;
|
||||
} finally {
|
||||
try {
|
||||
if (closeables != null)
|
||||
thrown = Throwables.returnOrSuppress(thrown, closeCloseables());
|
||||
} finally {
|
||||
// It's important to close the system resources after contexts and closeables
|
||||
// because the latter may use those system resources
|
||||
thrown = Throwables.returnOrSuppress(thrown, releaseSystemResources());
|
||||
} catch (Throwable t) {
|
||||
try {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
} catch (Throwable stackOverflowError) {
|
||||
// It seems that only StackOverflowError could be thrown from
|
||||
// Throwables.returnOrSuppress(), but in this case `thrown` and `t` are likely
|
||||
// StackOverflowErrors too, so just return one of them without trying to call
|
||||
// any more methods, including Throwable.addSuppressed().
|
||||
thrown = thrown != null ? thrown : t;
|
||||
}
|
||||
}
|
||||
return thrown;
|
||||
} finally {
|
||||
// Make GC life easier
|
||||
closeables = null;
|
||||
contexts = null;
|
||||
memoryResources = null;
|
||||
}
|
||||
// It's important to close the system resources only after contexts and closeables are
|
||||
// closed successfully, because the latter may use those system resources. However if
|
||||
// releaseEverything() is called from Cleaner, the ChronicleMap object is already
|
||||
// unreachable, that means no contexts are accessing it anymore and the problem with closing
|
||||
// contexts could probably be StackOverflowError or other exotic Error, so in this case we
|
||||
// try to close the system resources anyway.
|
||||
if (thrown == null || releaseFromCleaner) {
|
||||
try {
|
||||
thrown = releaseSystemResources(thrown);
|
||||
} catch (Throwable stackOverflowError) {
|
||||
// It seems that only StackOverflowError could be thrown (rather than returned!)
|
||||
// from releaseSystemResources(), so just return any Throwable from
|
||||
// releaseEverything() without trying to call any more methods.
|
||||
thrown = thrown != null ? thrown : stackOverflowError;
|
||||
}
|
||||
}
|
||||
if (thrown == null) {
|
||||
state = COMPLETELY_CLOSED;
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
|
||||
private Throwable releaseSystemResources(Throwable thrown) {
|
||||
try {
|
||||
if (memoryResources != null)
|
||||
thrown = Throwables.returnOrSuppress(thrown, releaseMemoryResources());
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
} finally {
|
||||
try {
|
||||
thrown = Throwables.returnOrSuppress(thrown, releaseExtraSystemResources());
|
||||
} catch (Throwable t) {
|
||||
try {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
} catch (Throwable stackOverflowError) {
|
||||
// It seems that only StackOverflowError could be thrown from
|
||||
// Throwables.returnOrSuppress(), but in this case `thrown` and `t` are likely
|
||||
// StackOverflowErrors too, so just return one of them without trying to call
|
||||
// any more methods, including Throwable.addSuppressed().
|
||||
thrown = thrown != null ? thrown : t;
|
||||
}
|
||||
}
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
|
||||
private Throwable closeContexts() {
|
||||
Throwable thrown = null;
|
||||
// Indexed loop instead of for-each because we may be in the context of cleaning up after
|
||||
// OutOfMemoryError, and allocating Iterator object may lead to another OutOfMemoryError, so
|
||||
// we try to avoid any allocations
|
||||
//noinspection ForLoopReplaceableByForEach
|
||||
List<WeakReference<ContextHolder>> contexts = this.contexts;
|
||||
// Indexed loop instead of using Iterator because we may be in the context of cleaning up
|
||||
// after OutOfMemoryError, and allocating Iterator object may lead to another
|
||||
// OutOfMemoryError, so we try to avoid any allocations.
|
||||
for (int i = 0; i < contexts.size(); i++) {
|
||||
WeakReference<ContextHolder> contextHolderRef = contexts.get(i);
|
||||
try {
|
||||
ContextHolder contextHolder = contextHolderRef.get();
|
||||
if (contextHolder != null) {
|
||||
closeContext(contextHolder);
|
||||
// The context reference could have already been set to null in (*). See comments in
|
||||
// closeContext() with more explanations.
|
||||
if (contextHolderRef != null) {
|
||||
try {
|
||||
ContextHolder contextHolder = contextHolderRef.get();
|
||||
if (contextHolder != null) {
|
||||
closeContext(contextHolder);
|
||||
}
|
||||
// (*) Make GC life easier
|
||||
contexts.set(i, null);
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
}
|
||||
}
|
||||
// Forget about contexts only if all of them are successfully closed, i. e. no throwables
|
||||
// were thrown in the above loop.
|
||||
if (thrown == null) {
|
||||
// Make GC life easier
|
||||
this.contexts = null;
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
|
||||
private void closeContext(ContextHolder contextHolder) {
|
||||
ChainingInterface context = contextHolder.get();
|
||||
try {
|
||||
// The context could have already been cleared, if this is the second attempt to close
|
||||
// contexts, the first one failed e. g. with IllegalStateException on one of the contexts
|
||||
// (see comment (*) below in this method), it could have succeed for some contexts and
|
||||
// contextHolder.clear() is performed.
|
||||
if (context != null) {
|
||||
if (context.owner().isAlive()) {
|
||||
// Ensures that if the thread owning this context will come to access chronicleHash
|
||||
// concurrently with resource releasing operation, it will fail due to the check in
|
||||
@@ -224,26 +304,74 @@ public abstract class ChronicleHashResources implements Runnable {
|
||||
// this access session.
|
||||
context.closeContext(chronicleHashIdentityString);
|
||||
}
|
||||
} finally {
|
||||
// See ContextHolder's class-level Javadoc comment which explains motivation for this
|
||||
|
||||
// (*) Don't execute contextHolder.clear() from a finally section of a try block
|
||||
// wrapping context.closeContext(), because if context.closeContext() fails e. g. with
|
||||
// IllegalStateException because the context is currently used (this happens if
|
||||
// ChronicleMap.close() is called within try-with-resources block of it's own operation,
|
||||
// see MapCloseTest.closeInContextTest()), we may want to try to close this context
|
||||
// again from ChronicleHashCloseOnExitHook.
|
||||
|
||||
// See ContextHolder's class-level Javadoc comment that explains the motivation for
|
||||
// calling contextHolder.clear().
|
||||
contextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private Throwable closeCloseables() {
|
||||
Throwable thrown = null;
|
||||
// Indexed loop instead of for-each because we may be in the context of cleaning up after
|
||||
// OutOfMemoryError, and allocating Iterator object may lead to another OutOfMemoryError, so
|
||||
// we try to avoid any allocations
|
||||
//noinspection ForLoopReplaceableByForEach
|
||||
List<Closeable> closeables = this.closeables;
|
||||
// Indexed loop instead of using Iterator because we may be in the context of cleaning up
|
||||
// after OutOfMemoryError, and allocating Iterator object may lead to another
|
||||
// OutOfMemoryError, so we try to avoid any allocations.
|
||||
for (int i = 0; i < closeables.size(); i++) {
|
||||
Closeable closeable = closeables.get(i);
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
// The closeable could have already been set to null in (*), see comments in
|
||||
// closeContexts() method which has similar logic.
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
// (*) Make GC life easier
|
||||
closeables.set(i, null);
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Forget about closeables only if all of them are successfully closed, i. e. no throwables
|
||||
// were thrown in the above loop.
|
||||
if (thrown == null) {
|
||||
// Make GC life easier
|
||||
this.closeables = null;
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
|
||||
private Throwable releaseMemoryResources() {
|
||||
Throwable thrown = null;
|
||||
List<MemoryResource> memoryResources = this.memoryResources;
|
||||
// Indexed loop instead of using Iterator because we may be in the context of cleaning up
|
||||
// after OutOfMemoryError, and allocating Iterator object may lead to another
|
||||
// OutOfMemoryError, so we try to avoid any allocations.
|
||||
for (int i = 0; i < memoryResources.size(); i++) {
|
||||
MemoryResource memoryResource = memoryResources.get(i);
|
||||
// The memory resource could have already been nulled out in (*), see comments in
|
||||
// closeCloseables() method which has similar logic.
|
||||
if (memoryResource != null) {
|
||||
try {
|
||||
releaseMemoryResource(memoryResource);
|
||||
// (*) Make GC life easier
|
||||
memoryResources.set(i, null);
|
||||
} catch (Throwable t) {
|
||||
thrown = Throwables.returnOrSuppress(thrown, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Forget about memory resources only if all of them are successfully released, i. e. no
|
||||
// throwables were thrown in the above loop.
|
||||
if (thrown == null) {
|
||||
this.memoryResources = null;
|
||||
}
|
||||
return thrown;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user