/*
* Copyright (C) 2012, 2016 higherfrequencytrading.com
* Copyright (C) 2016 Roman Leventov
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.openhft.chronicle.hash.impl;
import net.openhft.chronicle.hash.ChronicleHashClosedException;
import net.openhft.chronicle.hash.impl.stage.hash.ChainingInterface;
import net.openhft.chronicle.hash.impl.util.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* ChronicleHashResources is Runnable to be passed as "hunk" to {@link sun.misc.Cleaner}.
*/
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;
/**
* 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 int state = OPEN;
private List<MemoryResource> memoryResources = new ArrayList<>();
private List<Closeable> closeables = new ArrayList<>(1);
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 {@code ChronicleHashResources} is a
* hunk for {@link sun.misc.Cleaner}, and it would prevent the chronicleHash from ever becoming
* unreachable.
*/
private String chronicleHashIdentityString;
List<WeakReference<ContextHolder>> contexts() {
return contexts;
}
final synchronized long totalMemory() {
if (closed())
return 0L;
long totalMemory = 0L;
//noinspection ForLoopReplaceableByForEach -- allocation-free looping
for (int i = 0; i < memoryResources.size(); i++) {
totalMemory += memoryResources.get(i).size;
}
return totalMemory;
}
final boolean closed() {
return state != OPEN;
}
private void checkOpen() {
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;
}
final synchronized void addMemoryResource(long address, long size) {
checkOpen();
memoryResources.add(new MemoryResource(address, size));
}
final synchronized void addCloseable(Closeable closeable) {
checkOpen();
closeables.add(closeable);
}
final synchronized void addContext(ContextHolder contextHolder) {
checkOpen();
expungeStateContexts();
contexts.add(new WeakReference<>(contextHolder));
}
private void expungeStateContexts() {
contexts.removeIf(ref -> {
ContextHolder contextHolder = ref.get();
return contextHolder == null || !contextHolder.get().owner().isAlive();
});
}
/**
* This run() method is called from {@link sun.misc.Cleaner}, hence must not fail
*/
@Override
public void run() {
Throwable thrown = null;
try {
if (state == COMPLETELY_CLOSED)
return;
try {
LOG.error("{} is not closed manually, cleaned up from Cleaner",
chronicleHashIdentityString);
} catch (Throwable t) {
thrown = t;
} finally {
synchronized (this) {
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(true));
}
}
if (thrown != null) {
try {
LOG.error("Error on releasing resources of " + chronicleHashIdentityString,
thrown);
} catch (Throwable t) {
// This may occur if we are in shutdown hooks, and the log service has
// already been shut down. Try to fall back to printStackTrace().
thrown.addSuppressed(t);
thrown.printStackTrace();
}
}
}
} catch (Throwable ignore) {
// Just don't fail anyway. We will have another attempt to close this ChronicleMap from
// ChronicleHashCloseOnExitHook.
}
}
abstract void releaseMemoryResource(MemoryResource memoryResource) throws IOException;
Throwable releaseExtraSystemResources() {
// nothing to release
return null;
}
public final boolean releaseManually() {
if (state == COMPLETELY_CLOSED)
return false;
synchronized (this) {
if (state == COMPLETELY_CLOSED)
return false;
Throwable thrown = releaseEverything(false);
if (thrown != null)
throw Throwables.propagate(thrown);
return true;
}
}
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(). 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(), 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 {
if (contexts != null)
thrown = closeContexts();
} catch (Throwable t) {
thrown = t;
} finally {
try {
if (closeables != null)
thrown = Throwables.returnOrSuppress(thrown, closeCloseables());
} 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;
}
}
}
// 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;
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);
// 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);
}
}
}
// 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();
// 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
// context.lockContextLocally() method. If the thread owning this context is
// currently accessing chronicleHash, closeContext() will spin-wait until the end of
// this access session.
context.closeContext(chronicleHashIdentityString);
}
// (*) 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;
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);
// 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;
}
}