| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.lucene.search; |
| |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| import org.apache.lucene.store.AlreadyClosedException; |
| |
| /** |
| * Utility class to safely share instances of a certain type across multiple threads, while |
| * periodically refreshing them. This class ensures each reference is closed only once all threads |
| * have finished using it. It is recommended to consult the documentation of {@link |
| * ReferenceManager} implementations for their {@link #maybeRefresh()} semantics. |
| * |
| * @param <G> the concrete type that will be {@link #acquire() acquired} and {@link #release(Object) |
| * released}. |
| * @lucene.experimental |
| */ |
| public abstract class ReferenceManager<G> implements Closeable { |
| |
| private static final String REFERENCE_MANAGER_IS_CLOSED_MSG = "this ReferenceManager is closed"; |
| |
| protected volatile G current; |
| |
| private final Lock refreshLock = new ReentrantLock(); |
| |
| private final List<RefreshListener> refreshListeners = new CopyOnWriteArrayList<>(); |
| |
| private void ensureOpen() { |
| if (current == null) { |
| throw new AlreadyClosedException(REFERENCE_MANAGER_IS_CLOSED_MSG); |
| } |
| } |
| |
| private synchronized void swapReference(G newReference) throws IOException { |
| ensureOpen(); |
| final G oldReference = current; |
| current = newReference; |
| release(oldReference); |
| } |
| |
| /** |
| * Decrement reference counting on the given reference. |
| * |
| * @throws IOException if reference decrement on the given resource failed. |
| */ |
| protected abstract void decRef(G reference) throws IOException; |
| |
| /** |
| * Refresh the given reference if needed. Returns {@code null} if no refresh was needed, otherwise |
| * a new refreshed reference. |
| * |
| * @throws AlreadyClosedException if the reference manager has been {@link #close() closed}. |
| * @throws IOException if the refresh operation failed |
| */ |
| protected abstract G refreshIfNeeded(G referenceToRefresh) throws IOException; |
| |
| /** |
| * Try to increment reference counting on the given reference. Return true if the operation was |
| * successful. |
| * |
| * @throws AlreadyClosedException if the reference manager has been {@link #close() closed}. |
| */ |
| protected abstract boolean tryIncRef(G reference) throws IOException; |
| |
| /** |
| * Obtain the current reference. You must match every call to acquire with one call to {@link |
| * #release}; it's best to do so in a finally clause, and set the reference to {@code null} to |
| * prevent accidental usage after it has been released. |
| * |
| * @throws AlreadyClosedException if the reference manager has been {@link #close() closed}. |
| */ |
| public final G acquire() throws IOException { |
| G ref; |
| |
| do { |
| if ((ref = current) == null) { |
| throw new AlreadyClosedException(REFERENCE_MANAGER_IS_CLOSED_MSG); |
| } |
| if (tryIncRef(ref)) { |
| return ref; |
| } |
| if (getRefCount(ref) == 0 && current == ref) { |
| assert ref != null; |
| /* if we can't increment the reader but we are |
| still the current reference the RM is in a |
| illegal states since we can't make any progress |
| anymore. The reference is closed but the RM still |
| holds on to it as the actual instance. |
| This can only happen if somebody outside of the RM |
| decrements the refcount without a corresponding increment |
| since the RM assigns the new reference before counting down |
| the reference. */ |
| throw new IllegalStateException( |
| "The managed reference has already closed - this is likely a bug when the reference count is modified outside of the ReferenceManager"); |
| } |
| } while (true); |
| } |
| |
| /** |
| * Closes this ReferenceManager to prevent future {@link #acquire() acquiring}. A reference |
| * manager should be closed if the reference to the managed resource should be disposed or the |
| * application using the {@link ReferenceManager} is shutting down. The managed resource might not |
| * be released immediately, if the {@link ReferenceManager} user is holding on to a previously |
| * {@link #acquire() acquired} reference. The resource will be released once when the last |
| * reference is {@link #release(Object) released}. Those references can still be used as if the |
| * manager was still active. |
| * |
| * <p>Applications should not {@link #acquire() acquire} new references from this manager once |
| * this method has been called. {@link #acquire() Acquiring} a resource on a closed {@link |
| * ReferenceManager} will throw an {@link AlreadyClosedException}. |
| * |
| * @throws IOException if the underlying reader of the current reference could not be closed |
| */ |
| @Override |
| public final synchronized void close() throws IOException { |
| if (current != null) { |
| // make sure we can call this more than once |
| // closeable javadoc says: |
| // if this is already closed then invoking this method has no effect. |
| swapReference(null); |
| afterClose(); |
| } |
| } |
| |
| /** Returns the current reference count of the given reference. */ |
| protected abstract int getRefCount(G reference); |
| |
| /** |
| * Called after close(), so subclass can free any resources. |
| * |
| * @throws IOException if the after close operation in a sub-class throws an {@link IOException} |
| */ |
| protected void afterClose() throws IOException {} |
| |
| private void doMaybeRefresh() throws IOException { |
| // it's ok to call lock() here (blocking) because we're supposed to get here |
| // from either maybeRefresh() or maybeRefreshBlocking(), after the lock has |
| // already been obtained. Doing that protects us from an accidental bug |
| // where this method will be called outside the scope of refreshLock. |
| // Per ReentrantLock's javadoc, calling lock() by the same thread more than |
| // once is ok, as long as unlock() is called a matching number of times. |
| refreshLock.lock(); |
| boolean refreshed = false; |
| try { |
| final G reference = acquire(); |
| try { |
| notifyRefreshListenersBefore(); |
| G newReference = refreshIfNeeded(reference); |
| if (newReference != null) { |
| assert newReference != reference |
| : "refreshIfNeeded should return null if refresh wasn't needed"; |
| try { |
| swapReference(newReference); |
| refreshed = true; |
| } finally { |
| if (!refreshed) { |
| release(newReference); |
| } |
| } |
| } |
| } finally { |
| release(reference); |
| notifyRefreshListenersRefreshed(refreshed); |
| } |
| afterMaybeRefresh(); |
| } finally { |
| refreshLock.unlock(); |
| } |
| } |
| |
| /** |
| * You must call this (or {@link #maybeRefreshBlocking()}), periodically, if you want that {@link |
| * #acquire()} will return refreshed instances. |
| * |
| * <p><b>Threads</b>: it's fine for more than one thread to call this at once. Only the first |
| * thread will attempt the refresh; subsequent threads will see that another thread is already |
| * handling refresh and will return immediately. Note that this means if another thread is already |
| * refreshing then subsequent threads will return right away without waiting for the refresh to |
| * complete. |
| * |
| * <p>If this method returns true it means the calling thread either refreshed or that there were |
| * no changes to refresh. If it returns false it means another thread is currently refreshing. |
| * |
| * @throws IOException if refreshing the resource causes an {@link IOException} |
| * @throws AlreadyClosedException if the reference manager has been {@link #close() closed}. |
| */ |
| public final boolean maybeRefresh() throws IOException { |
| ensureOpen(); |
| |
| // Ensure only 1 thread does refresh at once; other threads just return immediately: |
| final boolean doTryRefresh = refreshLock.tryLock(); |
| if (doTryRefresh) { |
| try { |
| doMaybeRefresh(); |
| } finally { |
| refreshLock.unlock(); |
| } |
| } |
| |
| return doTryRefresh; |
| } |
| |
| /** |
| * You must call this (or {@link #maybeRefresh()}), periodically, if you want that {@link |
| * #acquire()} will return refreshed instances. |
| * |
| * <p><b>Threads</b>: unlike {@link #maybeRefresh()}, if another thread is currently refreshing, |
| * this method blocks until that thread completes. It is useful if you want to guarantee that the |
| * next call to {@link #acquire()} will return a refreshed instance. Otherwise, consider using the |
| * non-blocking {@link #maybeRefresh()}. |
| * |
| * @throws IOException if refreshing the resource causes an {@link IOException} |
| * @throws AlreadyClosedException if the reference manager has been {@link #close() closed}. |
| */ |
| public final void maybeRefreshBlocking() throws IOException { |
| ensureOpen(); |
| |
| // Ensure only 1 thread does refresh at once |
| refreshLock.lock(); |
| try { |
| doMaybeRefresh(); |
| } finally { |
| refreshLock.unlock(); |
| } |
| } |
| |
| /** |
| * Called after a refresh was attempted, regardless of whether a new reference was in fact |
| * created. |
| * |
| * @throws IOException if a low level I/O exception occurs |
| */ |
| protected void afterMaybeRefresh() throws IOException {} |
| |
| /** |
| * Release the reference previously obtained via {@link #acquire()}. |
| * |
| * <p><b>NOTE:</b> it's safe to call this after {@link #close()}. |
| * |
| * @throws IOException if the release operation on the given resource throws an {@link |
| * IOException} |
| */ |
| public final void release(G reference) throws IOException { |
| assert reference != null; |
| decRef(reference); |
| } |
| |
| private void notifyRefreshListenersBefore() throws IOException { |
| for (RefreshListener refreshListener : refreshListeners) { |
| refreshListener.beforeRefresh(); |
| } |
| } |
| |
| private void notifyRefreshListenersRefreshed(boolean didRefresh) throws IOException { |
| for (RefreshListener refreshListener : refreshListeners) { |
| refreshListener.afterRefresh(didRefresh); |
| } |
| } |
| |
| /** Adds a listener, to be notified when a reference is refreshed/swapped. */ |
| public void addListener(RefreshListener listener) { |
| if (listener == null) { |
| throw new NullPointerException("Listener must not be null"); |
| } |
| refreshListeners.add(listener); |
| } |
| |
| /** Remove a listener added with {@link #addListener(RefreshListener)}. */ |
| public void removeListener(RefreshListener listener) { |
| if (listener == null) { |
| throw new NullPointerException("Listener must not be null"); |
| } |
| refreshListeners.remove(listener); |
| } |
| |
| /** Use to receive notification when a refresh has finished. See {@link #addListener}. */ |
| public interface RefreshListener { |
| |
| /** Called right before a refresh attempt starts. */ |
| void beforeRefresh() throws IOException; |
| |
| /** |
| * Called after the attempted refresh; if the refresh did open a new reference then didRefresh |
| * will be true and {@link #acquire()} is guaranteed to return the new reference. |
| */ |
| void afterRefresh(boolean didRefresh) throws IOException; |
| } |
| } |