/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.resource; import alluxio.Constants; import alluxio.clock.Clock; import alluxio.clock.SystemClock; import com.google.common.base.Preconditions; import io.netty.util.internal.chmv8.ConcurrentHashMapV8; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.Iterator; import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** * A dynamic pool that manages the resources. It clears old resources. * It accepts a min and max capacity. * * When acquiring resources, the most recently used resource is returned. * * @param <T> the type of the resource */ @ThreadSafe public abstract class DynamicResourcePool<T> implements Pool<T> { private static final Logger LOG = LoggerFactory.getLogger(DynamicResourcePool.class); /** * A wrapper on the resource to include the last time at which it was used. * * @param <R> the resource type */ protected class ResourceInternal<R> { /** The resource. */ private R mResource; /** The last access time in ms. */ private long mLastAccessTimeMs; /** * @param lastAccessTimeMs the last access time in ms */ public void setLastAccessTimeMs(long lastAccessTimeMs) { mLastAccessTimeMs = lastAccessTimeMs; } /** * @return the last access time in ms */ public long getLastAccessTimeMs() { return mLastAccessTimeMs; } /** * Creates a {@link ResourceInternal} instance. * * @param resource the resource */ public ResourceInternal(R resource) { mResource = resource; mLastAccessTimeMs = mClock.millis(); } } /** * Options to initialize a Dynamic resource pool. */ public static final class Options { /** The max capacity. */ private int mMaxCapacity = 1024; /** The min capacity. */ private int mMinCapacity = 1; /** The initial delay. */ private long mInitialDelayMs = 100; /** The gc interval. */ private long mGcIntervalMs = 120 * Constants.SECOND_MS; /** The gc executor. */ private ScheduledExecutorService mGcExecutor; /** * @return the max capacity */ public int getMaxCapacity() { return mMaxCapacity; } /** * @return the min capacity */ public int getMinCapacity() { return mMinCapacity; } /** * @return the initial delay */ public long getInitialDelayMs() { return mInitialDelayMs; } /** * @return the gc interval */ public long getGcIntervalMs() { return mGcIntervalMs; } /** * @return the gc executor */ public ScheduledExecutorService getGcExecutor() { return mGcExecutor; } /** * @param maxCapacity the max capacity * @return the updated object */ public Options setMaxCapacity(int maxCapacity) { Preconditions.checkArgument(maxCapacity >= 1); mMaxCapacity = maxCapacity; return this; } /** * @param minCapacity the min capacity * @return the updated object */ public Options setMinCapacity(int minCapacity) { Preconditions.checkArgument(minCapacity >= 0); mMinCapacity = minCapacity; return this; } /** * @param initialDelayMs the initial delay * @return the updated object */ public Options setInitialDelayMs(long initialDelayMs) { Preconditions.checkArgument(initialDelayMs >= 0); mInitialDelayMs = initialDelayMs; return this; } /** * @param gcIntervalMs the gc interval * @return the updated object */ public Options setGcIntervalMs(long gcIntervalMs) { Preconditions.checkArgument(gcIntervalMs > 0); mGcIntervalMs = gcIntervalMs; return this; } /** * @param gcExecutor the gc executor * @return updated object */ public Options setGcExecutor(ScheduledExecutorService gcExecutor) { mGcExecutor = gcExecutor; return this; } private Options() { } // prevents instantiation /** * @return the default option */ public static Options defaultOptions() { return new Options(); } } private final ReentrantLock mLock = new ReentrantLock(); private final Condition mNotEmpty = mLock.newCondition(); /** The max capacity. */ private final int mMaxCapacity; /** The min capacity. */ private final int mMinCapacity; // Tracks the resources that are available ordered by lastAccessTime (the head is // the most recently used resource). // These are the resources that acquire() will take. // This is always a subset of the other data structure mResources. @GuardedBy("mLock") private final Deque<ResourceInternal<T>> mAvailableResources; // Tracks all the resources that are not closed. // put/delete operations are guarded by "mLock" so that we can control its size to be within // a [min, max] range. mLock is reused for simplicity. A separate lock can be used if we see // any performance overhead. private final ConcurrentHashMapV8<T, ResourceInternal<T>> mResources = new ConcurrentHashMapV8<>(32); // Thread to scan mAvailableResources to close those resources that are old. private ScheduledExecutorService mExecutor; private ScheduledFuture<?> mGcFuture; protected Clock mClock = new SystemClock(); /** * Creates a dynamic pool instance. * * @param options the options */ public DynamicResourcePool(Options options) { mExecutor = Preconditions.checkNotNull(options.getGcExecutor(), "executor"); mMaxCapacity = options.getMaxCapacity(); mMinCapacity = options.getMinCapacity(); mAvailableResources = new ArrayDeque<>(Math.min(mMaxCapacity, 32)); mGcFuture = mExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { List<T> resourcesToGc = new ArrayList<T>(); try { mLock.lock(); if (mResources.size() <= mMinCapacity) { return; } int currentSize = mResources.size(); Iterator<ResourceInternal<T>> iterator = mAvailableResources.iterator(); while (iterator.hasNext()) { ResourceInternal<T> next = iterator.next(); if (shouldGc(next)) { resourcesToGc.add(next.mResource); iterator.remove(); mResources.remove(next.mResource); currentSize--; if (currentSize <= mMinCapacity) { break; } } } } finally { mLock.unlock(); } for (T resource : resourcesToGc) { LOG.info("Resource {} is garbage collected.", resource); closeResource(resource); } } }, options.getInitialDelayMs(), options.getGcIntervalMs(), TimeUnit.MILLISECONDS); } /** * Acquire a resource of type {code T} from the pool. * * @return the acquired resource */ @Override public T acquire() throws IOException { try { return acquire(100 /* no timeout */, TimeUnit.DAYS); } catch (TimeoutException e) { // Never should timeout in acquire(). throw new RuntimeException(e); } } /** * Acquires a resource of type {code T} from the pool. * * This method is like {@link #acquire()}, but it will time out if an object cannot be * acquired before the specified amount of time. * * @param time an amount of time to wait * @param unit the unit to use for time * @return a resource taken from the pool * @throws TimeoutException if it fails to acquire because of time out */ @Override public T acquire(long time, TimeUnit unit) throws TimeoutException, IOException { long endTimeMs = mClock.millis() + unit.toMillis(time); // Try to take a resource without blocking ResourceInternal<T> resource = poll(); if (resource != null) { return checkHealthyAndRetry(resource.mResource, endTimeMs); } if (!isFull()) { // If the resource pool is empty but capacity is not yet full, create a new resource. T newResource = createNewResource(); ResourceInternal<T> resourceInternal = new ResourceInternal<>(newResource); if (add(resourceInternal)) { return newResource; } else { closeResource(newResource); } } // Otherwise, try to take a resource from the pool, blocking if none are available. try { mLock.lock(); while (true) { resource = poll(); if (resource != null) { break; } long currTimeMs = mClock.millis(); try { if (currTimeMs >= endTimeMs || !mNotEmpty .await(endTimeMs - currTimeMs, TimeUnit.MILLISECONDS)) { throw new TimeoutException("Acquire resource times out."); } } catch (InterruptedException e) { // Restore the interrupt flag so that it can be handled later. Thread.currentThread().interrupt(); } } } finally { mLock.unlock(); } return checkHealthyAndRetry(resource.mResource, endTimeMs); } /** * Releases the resource to the pool. It expects the resource to be released was acquired from * this pool. * {@link DynamicResourcePool#release(Object)} and {@link DynamicResourcePool#acquire()} must be * paired. Do not release the resource acquired multiple times. The behavior is undefined if * that happens. * * @param resource the resource to release */ @Override public void release(T resource) { // We don't need to acquire mLock here because the resource is guaranteed not to be removed // if it is not available (i.e. not in mAvailableResources list). if (!mResources.containsKey(resource)) { throw new IllegalArgumentException( "Resource " + resource.toString() + " was not acquired from this resource pool."); } ResourceInternal<T> resourceInternal = mResources.get(resource); resourceInternal.setLastAccessTimeMs(mClock.millis()); try { mLock.lock(); mAvailableResources.addFirst(resourceInternal); mNotEmpty.signal(); } finally { mLock.unlock(); } } /** * Closes the pool and clears all the resources. The resource pool should not be used after this. */ @Override public void close() { try { mLock.lock(); if (mAvailableResources.size() != mResources.size()) { LOG.warn("{} resources are not released when closing the resource pool.", mResources.size() - mAvailableResources.size()); } for (ResourceInternal<T> resourceInternal : mAvailableResources) { closeResource(resourceInternal.mResource); } mAvailableResources.clear(); } finally { mLock.unlock(); } mGcFuture.cancel(true); } @Override public int size() { return mResources.size(); } /** * @return true if the pool is full */ private boolean isFull() { return mResources.size() >= mMaxCapacity; } /** * Adds a newly created resource to the pool. The resource is not available when it is added. * * @param resource * @return true if the resource is successfully added */ private boolean add(ResourceInternal<T> resource) { try { mLock.lock(); if (mResources.size() >= mMaxCapacity) { return false; } else { mResources.put(resource.mResource, resource); return true; } } finally { mLock.unlock(); } } /** * Removes an existing resource from the pool. * * @param resource */ private void remove(T resource) { try { mLock.lock(); mResources.remove(resource); } finally { mLock.unlock(); } } /** * @return the most recently used resource and null if there are no free resources */ private ResourceInternal<T> poll() { try { mLock.lock(); return mAvailableResources.pollFirst(); } finally { mLock.unlock(); } } /** * Check whether the resource is healthy. If not retry. When this called, the resource * is not in mAvailableResources. * * @param resource the resource to check * @param endTimeMs the end time to wait till * @return the resource * @throws TimeoutException if it times out to wait for a resource */ private T checkHealthyAndRetry(T resource, long endTimeMs) throws TimeoutException, IOException { if (isHealthy(resource)) { return resource; } else { LOG.info("Clearing unhealthy resource {}.", resource); remove(resource); closeResource(resource); return acquire(endTimeMs - mClock.millis(), TimeUnit.MILLISECONDS); } } // The following functions should be overridden by implementations. /** * @param resourceInternal the resource to check * @return true if the resource should be garbage collected */ protected abstract boolean shouldGc(ResourceInternal<T> resourceInternal); /** * Checks whether a resource is healthy or not. * * @param resource the resource to check * @return true if the resource is healthy */ protected abstract boolean isHealthy(T resource); /** * Closes the resource. After this, the resource should not be used. It is not guaranteed that * the resource is closed after the function returns. * * @param resource the resource to close */ protected abstract void closeResource(T resource); /** * Similar as above but this guarantees that the resource is closed after the function returns * unless it fails to close. * * @param resource the resource to close */ protected abstract void closeResourceSync(T resource); /** * Creates a new resource. * * @return the newly created resource */ protected abstract T createNewResource() throws IOException; }