/* * 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.stage.hash; import net.openhft.chronicle.core.Memory; import net.openhft.chronicle.core.OS; import net.openhft.chronicle.hash.ChronicleHash; import net.openhft.chronicle.hash.ChronicleHashClosedException; import java.lang.reflect.Field; import java.util.concurrent.TimeUnit; import static net.openhft.chronicle.hash.impl.BigSegmentHeader.LOCK_TIMEOUT_SECONDS; public abstract class ThreadLocalState { private static final Memory MEMORY = OS.memory(); private static final long CONTEXT_LOCK_OFFSET; private static final int CONTEXT_UNLOCKED = 0; private static final int CONTEXT_LOCKED_LOCALLY = 1; private static final int CONTEXT_CLOSED = 2; static { try { Field contextLockField = ThreadLocalState.class.getDeclaredField("contextLock"); contextLockField.setAccessible(true); CONTEXT_LOCK_OFFSET = MEMORY.getFieldOffset(contextLockField); } catch (NoSuchFieldException e) { throw new AssertionError(e); } } private volatile int contextLock = CONTEXT_UNLOCKED; public boolean iterationContextLockedInThisThread; /** * Returns {@code true} if this is the outer context lock in this thread, {@code false} if this * is a nested context. */ public boolean lockContextLocally(ChronicleHash<?, ?, ?, ?> hash) { // hash().isOpen() check guarantees no starvation of a thread calling chMap.close() and // trying to close this context by closeContext() method below, while the thread owning this // context frequently locks and unlocks it (e. g. in a loop). This is also the only check // for chMap openness during the whole context usage lifecycle. if (hash.isOpen() && MEMORY.compareAndSwapInt(this, CONTEXT_LOCK_OFFSET, CONTEXT_UNLOCKED, CONTEXT_LOCKED_LOCALLY)) { return true; } else { if (contextLock == CONTEXT_LOCKED_LOCALLY) return false; // Don't extract this hash().isOpen() and the one above, because they could return // different results: the first (above) could return true, the second (below) - false. if (contextLock == CONTEXT_CLOSED || !hash.isOpen()) throw new ChronicleHashClosedException(hash); throw new AssertionError("Unknown context lock state: " + contextLock); } } public void unlockContextLocally() { // Ensure all reads from mapped memory are done before thread calling chronicleMap.close() // frees resources potentially unmapping some memory from where those reads are performed. MEMORY.loadFence(); // Avoid volatile write to avoid expensive store-load barrier MEMORY.writeOrderedInt(this, CONTEXT_LOCK_OFFSET, CONTEXT_UNLOCKED); } public void closeContext(String chronicleHashIdentityString) { if (tryCloseContext()) return; // Unless there are bugs in this codebase, it could happen that // contextLock == CONTEXT_CLOSED here only if closeContext() has succeed, and the subsequent // contextHolder.clear() has failed in ChronicleHashResources.closeContext(), though this is // hardly imaginable: contextHolder.clear() couldn't fail with OutOfMemoryError (because // there are no allocations in this method) and StackOverflowError (because in this case // closeContext() would fail with StackOverflowError before). But anyway it's probably // a good idea to make this check rather than not to make. if (contextLock == CONTEXT_CLOSED) return; // If first attempt of closing a context (i. e. moving from unused to closed state) failed, // it means that the context is still in use. If this context belongs to the current thread, // this is a bug, because we cannot "wait" until context is unused in the same thread: if (owner() == Thread.currentThread()) { throw new IllegalStateException(chronicleHashIdentityString + ": Attempt to close a Chronicle Hash in the context " + "of not yet finished query or iteration"); } // If the context belongs to a different thread, wait until that thread finishes it's work // with the context: // Double the current timeout for segment locks "without timeout", that effectively // specifies maximum lock (hence context) holding time long timeoutMillis = TimeUnit.SECONDS.toMillis(LOCK_TIMEOUT_SECONDS) * 2; long lastTime = System.currentTimeMillis(); do { if (tryCloseContext()) return; // Unless there are bugs in this codebase, this should never happen. But anyway it's // probably a good idea to make this check rather than not to make. if (contextLock == CONTEXT_CLOSED) return; Thread.yield(); long now = System.currentTimeMillis(); if (now != lastTime) { lastTime = now; timeoutMillis--; } } while (timeoutMillis >= 0); throw new RuntimeException(chronicleHashIdentityString + ": Failed to close a context, belonging to the thread\n" + owner() + ", in the state: " + owner().getState() + "\n" + "Possible reasons:\n" + "- The context owner thread exited before closing this context. Ensure that you\n" + "always close opened Chronicle Map's contexts, the best way to do this is to use\n" + "try-with-resources blocks." + "- The context owner thread runs some context operation (e. g. a query) for\n" + "unexpectedly long time (at least " + LOCK_TIMEOUT_SECONDS + " seconds).\n" + "You should either redesign your logic to spend less time in Chronicle Map\n" + "contexts (recommended) or synchronize map.close() with queries externally,\n" + "so that close() is called only after all query operations finished.\n" + "- Iteration over a large Chronicle Map takes more than " + LOCK_TIMEOUT_SECONDS + " seconds.\n" + "In this case you should synchronize map.close() with iterations over the map\n" + "externally, so that close() is called only after all iterations are finished.\n" + "- This is a dead lock involving the context owner thread and this thread (from\n" + "which map.close() method is called. Make sure you always close Chronicle Map\n" + "contexts, preferably using try-with-resources blocks."); } private boolean tryCloseContext() { return MEMORY.compareAndSwapInt(this, CONTEXT_LOCK_OFFSET, CONTEXT_UNLOCKED, CONTEXT_CLOSED); } public abstract Thread owner(); }