com.mongodb.internal.connection.ConcurrentPool Maven / Gradle / Ivy
Show all versions of mongodb-driver-core Show documentation
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed 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 com.mongodb.internal.connection;
import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoServerUnavailableException;
import com.mongodb.MongoTimeoutException;
import com.mongodb.annotations.ThreadSafe;
import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.time.TimePoint;
import com.mongodb.internal.time.Timeout;
import com.mongodb.lang.Nullable;
import java.util.Deque;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.Locks.lockInterruptibly;
import static com.mongodb.internal.Locks.lockInterruptiblyUnfair;
import static com.mongodb.internal.Locks.withUnfairLock;
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
import static com.mongodb.internal.thread.InterruptionUtil.interruptAndCreateMongoInterruptedException;
/**
* A concurrent pool implementation.
*
* This class is not part of the public API and may be removed or changed at any time
*/
public class ConcurrentPool implements Pool {
/**
* {@link Integer#MAX_VALUE}.
*/
public static final int INFINITE_SIZE = Integer.MAX_VALUE;
private final int maxSize;
private final ItemFactory itemFactory;
private final Deque available = new ConcurrentLinkedDeque<>();
private final StateAndPermits stateAndPermits;
private final String poolClosedMessage;
/**
* Factory for creating and closing pooled items.
*
* @param
*/
public interface ItemFactory {
T create();
void close(T t);
boolean shouldPrune(T t);
}
/**
* Initializes a new pool of objects.
*
* @param maxSize max to hold to at any given time, must be positive.
* @param itemFactory factory used to create and close items in the pool
*/
public ConcurrentPool(final int maxSize, final ItemFactory itemFactory) {
this(maxSize, itemFactory, "The pool is closed");
}
public ConcurrentPool(final int maxSize, final ItemFactory itemFactory, final String poolClosedMessage) {
assertTrue(maxSize > 0);
this.maxSize = maxSize;
this.itemFactory = itemFactory;
stateAndPermits = new StateAndPermits(maxSize, this::poolClosedException);
this.poolClosedMessage = notNull("poolClosedMessage", poolClosedMessage);
}
/**
* Return an instance of T to the pool. This method simply calls {@code release(t, false)}
* Must not throw {@link Exception}s.
*
* @param t item to return to the pool
*/
@Override
public void release(final T t) {
release(t, false);
}
/**
* call done when you are done with an object from the pool if there is room and the object is ok will get added
* Must not throw {@link Exception}s.
*
* @param t item to return to the pool
* @param prune true if the item should be closed, false if it should be put back in the pool
*/
@Override
public void release(final T t, final boolean prune) {
if (t == null) {
throw new IllegalArgumentException("Can not return a null item to the pool");
}
if (stateAndPermits.closed()) {
close(t);
return;
}
if (prune) {
close(t);
} else {
available.addLast(t);
}
stateAndPermits.releasePermit();
}
/**
* Is equivalent to {@link #get(long, TimeUnit)} called with an infinite timeout.
*
* @return An object from the pool.
*/
@Override
public T get() {
return get(-1, TimeUnit.MILLISECONDS);
}
/**
* Gets an object from the pool. Blocks until an object is available, or the specified {@code timeout} expires,
* or the pool is {@linkplain #close() closed}/{@linkplain #pause(Supplier) paused}.
*
* @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}.
* @param timeUnit the time unit of the timeout
* @return An object from the pool, or null if can't get one in the given waitTime
* @throws MongoTimeoutException if the timeout has been exceeded
*/
@Override
public T get(final long timeout, final TimeUnit timeUnit) {
if (!stateAndPermits.acquirePermit(timeout, timeUnit)) {
throw new MongoTimeoutException(String.format("Timeout waiting for a pooled item after %d %s", timeout, timeUnit));
}
T t = available.pollLast();
if (t == null) {
t = createNewAndReleasePermitIfFailure();
}
return t;
}
/**
* This method is similar to {@link #get(long, TimeUnit)} with 0 timeout.
* The difference is that it never creates a new element
* and returns {@code null} instead of throwing {@link MongoTimeoutException}.
*/
@Nullable
T getImmediateUnfair() {
T element = null;
if (stateAndPermits.acquirePermitImmediateUnfair()) {
element = available.pollLast();
if (element == null) {
stateAndPermits.releasePermit();
}
}
return element;
}
public void prune() {
// restrict number of iterations to the current size in order to avoid an infinite loop in the presence of concurrent releases
// back to the pool
int maxIterations = available.size();
int numIterations = 0;
for (T cur : available) {
if (itemFactory.shouldPrune(cur) && available.remove(cur)) {
close(cur);
}
numIterations++;
if (numIterations == maxIterations) {
break;
}
}
}
/**
* Try to populate this pool with items so that {@link #getCount()} is not smaller than {@code minSize}.
* The {@code initAndRelease} action throwing an exception causes this method to stop and re-throw that exception.
*
* @param initAndRelease An action applied to non-{@code null} new items.
* If an exception is thrown by the action, the action must {@linkplain #release(Object, boolean) prune} the item.
* Otherwise, the action must {@linkplain #release(Object) release} the item.
*/
public void ensureMinSize(final int minSize, final Consumer initAndRelease) {
while (getCount() < minSize) {
if (!stateAndPermits.acquirePermit(0, TimeUnit.MILLISECONDS)) {
break;
}
initAndRelease.accept(createNewAndReleasePermitIfFailure());
}
}
private T createNewAndReleasePermitIfFailure() {
try {
T newMember = itemFactory.create();
if (newMember == null) {
throw new MongoInternalException("The factory for the pool created a null item");
}
return newMember;
} catch (Exception e) {
stateAndPermits.releasePermit();
throw e;
}
}
/**
* @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}.
*/
@VisibleForTesting(otherwise = PRIVATE)
boolean acquirePermit(final long timeout, final TimeUnit timeUnit) {
return stateAndPermits.acquirePermit(timeout, timeUnit);
}
/**
* Clears the pool of all objects.
* Must not throw {@link Exception}s.
*/
@Override
public void close() {
if (stateAndPermits.close()) {
Iterator iter = available.iterator();
while (iter.hasNext()) {
T t = iter.next();
close(t);
iter.remove();
}
}
}
int getMaxSize() {
return maxSize;
}
public int getInUseCount() {
return maxSize - stateAndPermits.permits();
}
public int getAvailableCount() {
return available.size();
}
public int getCount() {
return getInUseCount() + getAvailableCount();
}
public String toString() {
return "pool: maxSize: " + sizeToString(maxSize)
+ " availableCount " + getAvailableCount()
+ " inUseCount " + getInUseCount();
}
/**
* Must not throw {@link Exception}s, so swallow exceptions from {@link ItemFactory#close(Object)}.
*/
private void close(final T t) {
try {
itemFactory.close(t);
} catch (Exception e) {
// ItemFactory.close() really should not throw
}
}
void ready() {
stateAndPermits.ready();
}
void pause(final Supplier causeSupplier) {
stateAndPermits.pause(causeSupplier);
}
/**
* @see #isPoolClosedException(Throwable)
*/
MongoServerUnavailableException poolClosedException() {
return new MongoServerUnavailableException(poolClosedMessage);
}
/**
* @see #poolClosedException()
*/
static boolean isPoolClosedException(final Throwable e) {
return e instanceof MongoServerUnavailableException;
}
/**
* Package-access methods are thread-safe,
* and only they should be called outside of the {@link StateAndPermits}'s code.
*/
@ThreadSafe
private static final class StateAndPermits {
private final Supplier poolClosedExceptionSupplier;
private final ReentrantLock lock;
private final Condition permitAvailableOrClosedOrPausedCondition;
private volatile boolean paused;
private volatile boolean closed;
private final int maxPermits;
private volatile int permits;
/** When there are not enough available permits to serve all threads requesting a permit, threads are queued and wait on
* {@link #permitAvailableOrClosedOrPausedCondition}. Because of this waiting, we want threads to acquire the lock fairly,
* to avoid a situation when some threads are sitting in the queue for a long time while others barge in and acquire
* the lock without waiting in the queue. Fair locking reduces high percentiles of {@link #acquirePermit(long, TimeUnit)} latencies
* but reduces its throughput: it makes latencies roughly equally high for everyone, while keeping them lower than the highest
* latencies with unfair locking. The fair approach is in accordance with the
*
* connection pool specification.
*
* When there are enough available permits to serve all threads requesting a permit, threads still have to acquire the lock,
* and still are queued, but since they are not waiting on {@link #permitAvailableOrClosedOrPausedCondition},
* threads spend less time in the queue. This results in having smaller high percentiles
* of {@link #acquirePermit(long, TimeUnit)} latencies, and we do not want to sacrifice the throughput
* to further reduce the high percentiles by acquiring the lock fairly.
*
* While there is a chance that the expressed reasoning is flawed, it is supported by the results of experiments reported in
* comments in JAVA-4452.
*
* {@link ReentrantReadWriteLock#hasWaiters(Condition)} requires holding the lock to be called, therefore we cannot use it
* to discriminate between the two cases described above, and we use {@link #waitersEstimate} instead.
* This approach results in sometimes acquiring a lock unfairly when it should have been acquired fairly, and vice versa.
* But it appears to be a good enough compromise, that results in having enough throughput when there are enough
* available permits and tolerable high percentiles of latencies when there are not enough available permits.
*
* It may seem viable to use {@link #permits} > 0 as a way to decide that there are likely no waiters,
* but benchmarking shows that with this approach high percentiles of contended {@link #acquirePermit(long, TimeUnit)} latencies
* (when the number of threads that use the pool is higher than the maximum pool size) become similar to a situation when no
* fair locking is used. That is, this approach does not result in the behavior we want.
*/
private final AtomicInteger waitersEstimate;
@Nullable
private Supplier causeSupplier;
StateAndPermits(final int maxPermits, final Supplier poolClosedExceptionSupplier) {
this.poolClosedExceptionSupplier = poolClosedExceptionSupplier;
lock = new ReentrantLock(true);
permitAvailableOrClosedOrPausedCondition = lock.newCondition();
paused = false;
closed = false;
this.maxPermits = maxPermits;
permits = maxPermits;
waitersEstimate = new AtomicInteger();
causeSupplier = null;
}
int permits() {
return permits;
}
boolean acquirePermitImmediateUnfair() {
return withUnfairLock(lock, () -> {
throwIfClosedOrPaused();
if (permits > 0) {
//noinspection NonAtomicOperationOnVolatileField
permits--;
return true;
} else {
return false;
}
});
}
/**
* This method also emulates the eager {@link InterruptedException} behavior of
* {@link java.util.concurrent.Semaphore#tryAcquire(long, TimeUnit)}.
*
* @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}.
*/
boolean acquirePermit(final long timeout, final TimeUnit unit) throws MongoInterruptedException {
long remainingNanos = unit.toNanos(timeout);
if (waitersEstimate.get() == 0) {
lockInterruptiblyUnfair(lock);
} else {
lockInterruptibly(lock);
}
try {
while (permits == 0
// the absence of short-circuiting is of importance
& !throwIfClosedOrPaused()) {
try {
waitersEstimate.incrementAndGet();
if (timeout < 0 || remainingNanos == Long.MAX_VALUE) {
permitAvailableOrClosedOrPausedCondition.await();
} else if (remainingNanos >= 0) {
remainingNanos = permitAvailableOrClosedOrPausedCondition.awaitNanos(remainingNanos);
} else {
return false;
}
} catch (InterruptedException e) {
throw interruptAndCreateMongoInterruptedException(null, e);
} finally {
waitersEstimate.decrementAndGet();
}
}
assertTrue(permits > 0);
//noinspection NonAtomicOperationOnVolatileField
permits--;
return true;
} finally {
lock.unlock();
}
}
void releasePermit() {
withUnfairLock(lock, () -> {
assertTrue(permits < maxPermits);
//noinspection NonAtomicOperationOnVolatileField
permits++;
permitAvailableOrClosedOrPausedCondition.signal();
});
}
void pause(final Supplier causeSupplier) {
withUnfairLock(lock, () -> {
if (!paused) {
this.paused = true;
permitAvailableOrClosedOrPausedCondition.signalAll();
}
this.causeSupplier = assertNotNull(causeSupplier);
});
}
void ready() {
if (paused) {
withUnfairLock(lock, () -> {
this.paused = false;
this.causeSupplier = null;
});
}
}
/**
* @return {@code true} if and only if the state changed as a result of the operation.
*/
boolean close() {
if (!closed) {
return withUnfairLock(lock, () -> {
if (!closed) {
closed = true;
permitAvailableOrClosedOrPausedCondition.signalAll();
return true;
}
return false;
});
}
return false;
}
/**
* This method must be called by a {@link Thread} that holds the {@link #lock}.
*
* @return {@code false} which means that the method did not throw.
* The method returns to allow using it conveniently as part of a condition check when waiting on a {@link Condition}.
* Short-circuiting operators {@code &&} and {@code ||} must not be used with this method to ensure that it is called.
* @throws MongoServerUnavailableException If and only if {@linkplain #close() closed}.
* @throws MongoException If and only if {@linkplain #pause(Supplier) paused}
* and not {@linkplain #close() closed}. The exception is specified via the {@link #pause(Supplier)} method
* and may be a subtype of {@link MongoException}.
*/
boolean throwIfClosedOrPaused() {
if (closed) {
throw poolClosedExceptionSupplier.get();
}
if (paused) {
throw assertNotNull(assertNotNull(causeSupplier).get());
}
return false;
}
boolean closed() {
return closed;
}
}
/**
* @return {@link Integer#toString()} if {@code size} is not {@link #INFINITE_SIZE}, otherwise returns {@code "infinite"}.
*/
static String sizeToString(final int size) {
return size == INFINITE_SIZE ? "infinite" : Integer.toString(size);
}
}