diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml
index 20684680865..e2fd0ae362c 100644
--- a/config/spotbugs/exclude.xml
+++ b/config/spotbugs/exclude.xml
@@ -15,9 +15,13 @@
-->
+
+
* The Java SE API uses exceptions different from {@link InterruptedException} to communicate the same information:
diff --git a/driver-core/src/main/com/mongodb/assertions/Assertions.java b/driver-core/src/main/com/mongodb/assertions/Assertions.java index bf38638dc6d..e80e8189a90 100644 --- a/driver-core/src/main/com/mongodb/assertions/Assertions.java +++ b/driver-core/src/main/com/mongodb/assertions/Assertions.java @@ -205,7 +205,7 @@ public static boolean assertFalse(final boolean value) throws AssertionError { } /** - * @throws AssertionError Always + * @throws AssertionError Always. * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail()}. * This may be helpful in non-{@code void} methods. */ @@ -215,7 +215,7 @@ public static AssertionError fail() throws AssertionError { /** * @param msg The failure message. - * @throws AssertionError Always + * @throws AssertionError Always. * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail("failure message")}. * This may be helpful in non-{@code void} methods. */ @@ -223,6 +223,16 @@ public static AssertionError fail(final String msg) throws AssertionError { throw new AssertionError(assertNotNull(msg)); } + /** + * @param cause The {@linkplain AssertionError#getCause() cause}. + * @throws AssertionError Always. + * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail(cause)}. + * This may be helpful in non-{@code void} methods. + */ + public static AssertionError fail(final Throwable cause) throws AssertionError { + throw new AssertionError(null, cause); + } + /** * @param supplier the supplier to check * @return {@code supplier.get()} diff --git a/driver-core/src/main/com/mongodb/connection/AsyncTransportSettings.java b/driver-core/src/main/com/mongodb/connection/AsyncTransportSettings.java index 8e259392313..489ada57020 100644 --- a/driver-core/src/main/com/mongodb/connection/AsyncTransportSettings.java +++ b/driver-core/src/main/com/mongodb/connection/AsyncTransportSettings.java @@ -52,13 +52,15 @@ private Builder() { } /** - * The executor service, intended to be used exclusively by the mongo - * client. Closing the mongo client will result in {@linkplain ExecutorService#shutdown() orderly shutdown} - * of the executor service. - * - *When {@linkplain SslSettings#isEnabled() TLS is not enabled}, see + * The {@link ExecutorService}, intended to be used exclusively by the {@code MongoClient}. + *
+ * {@linkplain AutoCloseable#close() Closing} the {@code MongoClient} results in + * {@linkplain ExecutorService#shutdown() orderly shutdown} of the {@code executorService}. + * The application must not directly shut down the {@code executorService}. + *
+ * When {@linkplain SslSettings#isEnabled() TLS is not enabled}, see * {@link java.nio.channels.AsynchronousChannelGroup#withThreadPool(ExecutorService)} - * for additional requirements for the executor service. + * for additional requirements for the {@code executorService}. * * @param executorService the executor service * @return this diff --git a/driver-core/src/main/com/mongodb/connection/NettyTransportSettings.java b/driver-core/src/main/com/mongodb/connection/NettyTransportSettings.java index cb3a7c7c090..c80ed298540 100644 --- a/driver-core/src/main/com/mongodb/connection/NettyTransportSettings.java +++ b/driver-core/src/main/com/mongodb/connection/NettyTransportSettings.java @@ -85,8 +85,9 @@ public Builder socketChannelClass(final Class extends SocketChannel> socketCha /** * Sets the event loop group. - * - *
The application is responsible for shutting down the provided {@code eventLoopGroup}
+ *+ * The application is responsible for shutting down the provided {@code eventLoopGroup}. + * It must not be shut down before or concurrently with {@linkplain AutoCloseable#close() closing} the {@code MongoClient}. * * @param eventLoopGroup the event loop group that all channels created by this factory will be a part of * @return this diff --git a/driver-core/src/main/com/mongodb/internal/Locks.java b/driver-core/src/main/com/mongodb/internal/Locks.java index 042fc9fd69f..66160636e22 100644 --- a/driver-core/src/main/com/mongodb/internal/Locks.java +++ b/driver-core/src/main/com/mongodb/internal/Locks.java @@ -26,7 +26,7 @@ import static com.mongodb.internal.thread.InterruptionUtil.interruptAndCreateMongoInterruptedException; /** - *
This class is not part of the public API and may be removed or changed at any time
+ * This class is not part of the public API and may be removed or changed at any time. */ public final class Locks { public static void withLock(final Lock lock, final Runnable action) { @@ -41,8 +41,7 @@ public static void withInterruptibleLock(final StampedLock lock, final Runnable try { stamp = lock.writeLockInterruptibly(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new MongoInterruptedException("Interrupted waiting for lock", e); + throw interruptAndCreateMongoInterruptedException("Interrupted waiting for lock", e); } try { runnable.run(); diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java index f5fb6358b87..37b55afe6af 100644 --- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java +++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java @@ -20,6 +20,7 @@ import com.mongodb.internal.async.function.LoopControl; import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier; +import com.mongodb.internal.thread.AsyncClientExecutor; import java.util.function.BooleanSupplier; import java.util.function.Predicate; @@ -233,6 +234,8 @@ default {
* @param callback A consumer of a result, {@link SingleResultCallback#onResult(Object, Throwable) completed} after
* (in the happens-before order) the asynchronous function completes.
* @throws RuntimeException Never. Exceptions must be relayed to the {@code callback}.
- * @throws Error Never, on the best effort basis. Errors should be relayed to the {@code callback}.
+ * @throws Error Never, on the best-effort basis. Errors should be relayed to the {@code callback}.
*/
void apply(P a, SingleResultCallback
* This option may be used as a convenient way to utilize
* OpenSSL as an alternative to the TLS/SSL protocol implementation in a JDK.
@@ -203,13 +206,27 @@ public StreamFactory create(final SocketSettings socketSettings, final SslSettin
sslContext);
}
+ /**
+ * The {@linkplain AsyncClientExecutor} {@linkplain AsyncClientExecutor#backedBy(Executor) backed by} the same {@link EventLoopGroup}
+ * used by {@link #create(SocketSettings, SslSettings)} for a {@link StreamFactory}.
+ * That {@link EventLoopGroup} may be provided by an application via {@link NettyTransportSettings#getEventLoopGroup()}.
+ */
+ @Override
+ public AsyncClientExecutor getClientExecutor() {
+ return clientExecutor;
+ }
+
@Override
public void close() {
- if (ownsEventLoopGroup) {
- // ignore the returned Future. This is in line with MongoClient behavior to not block waiting for connections to be returned
- // to the pool
- eventLoopGroup.shutdownGracefully();
- }
+ try {
+ clientExecutor.close();
+ } finally {
+ if (ownsEventLoopGroup) {
+ // ignore the returned Future. This is in line with MongoClient behavior to not block waiting for connections to be returned
+ // to the pool
+ eventLoopGroup.shutdownGracefully();
+ }
+ }
}
@Override
@@ -236,6 +253,7 @@ private NettyStreamFactoryFactory(final Builder builder) {
socketChannelClass = builder.socketChannelClass == null ? NioSocketChannel.class : builder.socketChannelClass;
eventLoopGroup = builder.eventLoopGroup == null ? new NioEventLoopGroup() : builder.eventLoopGroup;
ownsEventLoopGroup = builder.eventLoopGroup == null;
+ clientExecutor = AsyncClientExecutor.backedBy(eventLoopGroup);
sslContext = builder.sslContext;
inetAddressResolver = builder.inetAddressResolver;
}
diff --git a/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java b/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
index e9907bb3953..4c1eda53e11 100644
--- a/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
+++ b/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
@@ -17,8 +17,7 @@
package com.mongodb.internal.diagnostics.logging;
/**
- * This class is not part of the public API. It may be removed or changed at any time.
- *
+ * This class is not part of the public API and may be removed or changed at any time.
*/
public interface Logger {
/**
diff --git a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java
index 52211611215..d327ad663c9 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java
@@ -327,7 +327,7 @@ static
+ * May be used to execute internal code that is not blocking, or application code.
+ * When an asynchronous client is used, the application code we may execute is supposed to not be blocking, but we cannot enforce that.
+ * If an application violates the contract, it bears the responsibility.
+ *
+ * Purposefully not {@link ExecutorService}, because it does not manage the underlying resources, if any.
+ * They must be managed externally to {@link AsyncClientExecutor}.
+ * Nonetheless, it is still {@link AutoCloseable}. See {@link #close()} for the details.
+ *
+ * This class is not part of the public API and may be removed or changed at any time.
+ *
+ * @see StreamFactoryFactory#getClientExecutor()
+ * @see CommonExecutor
+ */
+@ThreadSafe
+public interface AsyncClientExecutor extends AutoCloseable {
+ AsyncClientExecutor NO_OP = UnimplementedAsyncClientExecutor.instance();
+
+ /**
+ * @param executor The executor to use for executing tasks.
+ * If it is a {@link ScheduledExecutorService}, then it is also used for scheduling,
+ * otherwise {@link CommonExecutor} is used for scheduling.
+ */
+ static AsyncClientExecutor backedBy(final Executor executor) {
+ return new DefaultAsyncClientExecutor(executor);
+ }
+
+ /**
+ * The callback-based counterpart to {@link Thread#sleep(long, int)}.
+ *
+ * @param duration A non-{@linkplain Duration#isNegative() negative} duration.
+ * If {@code duration} is {@linkplain Duration#isZero() zero},
+ * the {@code callback} is {@linkplain SingleResultCallback#complete(SingleResultCallback) completed}
+ * by the {@link Thread} that invoked the method.
+ * Regardless of the {@link Duration}, {@link #close()} causes the {@code callback} to be completed with
+ * {@link RejectedExecutionException}.
+ */
+ void sleepAsync(Duration duration, SingleResultCallback
+ * We do not always have access to {@link ScheduledExecutorService} in a {@code MongoClient}.
+ * For example, even if {@link AsyncTransportSettings#getExecutorService()} is present, it is merely an {@link ExecutorService},
+ * and does not have to be a {@link ScheduledExecutorService}.
+ * {@link CommonExecutor} is always accessible and may be used to schedule tasks when a more suitable alternative does not exist,
+ * but must not be used to execute such scheduled tasks.
+ *
+ * This class is not part of the public API and may be removed or changed at any time.
+ */
+// VAKOTODO create a ticket and leave a TODO to use Cleaner when we are at Java SE 17 to shut down internal executors if the class is GCed.
+public final class CommonExecutor {
+ private static final Logger LOGGER = Loggers.getLogger("client");
+ private static final CommonExecutor INSTANCE = new CommonExecutor();
+
+ private final MongoScheduledThreadPoolExecutor singleThreadScheduler;
+
+ public static CommonExecutor commonExecutor() {
+ return INSTANCE;
+ }
+
+ private CommonExecutor() {
+ singleThreadScheduler = new MongoScheduledThreadPoolExecutor(1, new DaemonThreadFactory("CommonScheduler"));
+ }
+
+ /**
+ * @param task The task to be scheduled. If it is {@link Executor#execute(Runnable) executed}, then the execution is guaranteed
+ * to be done via the {@code executor}. However, if the {@code executor} is shut down, then it does not execute the {@code task},
+ * and there is nothing we can do about that.
+ * @param delay A non-{@linkplain Duration#isNegative() negative} delay.
+ * @param executor The {@link Executor} to use for {@code task} {@linkplain Runnable#run() execution},
+ * so that the {@code task} is not executed by a thread managed by {@link CommonExecutor}.
+ * @return The {@link ScheduledFuture} representing only
+ * the {@linkplain ScheduledExecutorService#schedule(Runnable, long, TimeUnit) scheduling part},
+ * and not the execution part done by the {@code executor}.
+ */
+ ScheduledFuture> schedule(final Runnable task, final Duration delay, final Executor executor) {
+ try {
+ return singleThreadScheduler.schedule(
+ () -> {
+ // Depending on the `executor` implementation, which may be provided by an application,
+ // invoking `executor.execute` may result in executing arbitrary code, including `task`, in the single thread
+ // managed by `singleThreadScheduler`. This, in turn, may affect the behavior of other `MongoClient` instances
+ // that use `CommonExecutor`. More specifically, this may happen if:
+ // - `executor.execute` executes arbitrary code in the thread invoking the method;
+ // - `executor.execute` executes `task` in the thread invoking the `execute` method;
+ // - `executor` is a `ThreadPoolExecutor` with a bounded work queue that is full,
+ // and with `ThreadPoolExecutor.CallerRunsPolicy`;
+ // - `executor` is a `ThreadPoolExecutor` with a custom `RejectedExecutionHandler` that may result in
+ // executing `task` in the thread invoking the `execute` method.
+ //
+ // We consider the risk of the above small enough to make the current approach favourable to the alternative
+ // of having to run one more thread per `MongoClient`.
+ try {
+ executor.execute(task);
+ } catch (Exception e) {
+ LOGGER.error(format("The executor %s, which was likely provided by the application, either failed to execute"
+ + " the scheduled task %s, or executed it in the same thread that invoked `execute`", executor, task), e);
+ }
+ },
+ delay.toNanos(), NANOSECONDS);
+ } catch (RejectedExecutionException e) {
+ throw fail(e.toString());
+ }
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/thread/DaemonThreadFactory.java b/driver-core/src/main/com/mongodb/internal/thread/DaemonThreadFactory.java
index e44fd0661c4..e64890148af 100644
--- a/driver-core/src/main/com/mongodb/internal/thread/DaemonThreadFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/thread/DaemonThreadFactory.java
@@ -22,10 +22,10 @@
/**
* Custom thread factory for scheduled executor service that creates daemon threads. Otherwise,
* applications that neglect to close the client will not exit.
- *
- * This class is not part of the public API and may be removed or changed at any time
+ * This class is not part of the public API and may be removed or changed at any time.
*/
-public class DaemonThreadFactory implements ThreadFactory {
+public final class DaemonThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
diff --git a/driver-core/src/main/com/mongodb/internal/thread/DefaultAsyncClientExecutor.java b/driver-core/src/main/com/mongodb/internal/thread/DefaultAsyncClientExecutor.java
new file mode 100644
index 00000000000..bf70de0bed0
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/thread/DefaultAsyncClientExecutor.java
@@ -0,0 +1,190 @@
+/*
+ * 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.thread;
+
+import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.annotations.ThreadSafe;
+import com.mongodb.internal.async.SingleResultCallback;
+import com.mongodb.lang.Nullable;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static com.mongodb.assertions.Assertions.assertFalse;
+import static com.mongodb.assertions.Assertions.assertNull;
+import static com.mongodb.internal.Locks.withLock;
+import static com.mongodb.internal.async.AsyncRunnable.beginAsync;
+import static com.mongodb.internal.thread.CommonExecutor.commonExecutor;
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+@ThreadSafe
+final class DefaultAsyncClientExecutor implements AsyncClientExecutor {
+ private final Executor backingExecutor;
+ private final Set This class is not part of the public API and may be removed or changed at any time
+ * The driver code never observes task failures through {@link Future}s that represent completion of tasks,
+ * which is why this class is useful.
+ *
+ * Handling a task failure when it is an {@link Error} this way enables applications to decide how to deal with it
+ * via {@link UncaughtExceptionHandler}. An {@link Error} is more likely to cause an invariant violation than an {@link Exception},
+ * because it is less likely to be taken into account in code. An {@link AssertionError} outright informs about an invariant violation.
+ * Furthermore, a {@link VirtualMachineError} not only may happen in a peculiar situation,
+ * but also may be asynchronous.
+ * That is why it may be a good idea for an application to terminate on {@link Error}.
+ * We cannot make such a decision for an application, but we must do our best to give it an opportunity to react to an {@link Error}.
+ *
+ * If there is no {@link UncaughtExceptionHandler}, then the failure is {@linkplain Throwable#printStackTrace(PrintStream) printed}
+ * to {@link System#err}, see {@link ThreadGroup#uncaughtException(Thread, Throwable)}.
+ */
+public final class MongoThreadPoolExecutor extends ThreadPoolExecutor {
+ public MongoThreadPoolExecutor(
+ final int corePoolSize,
+ final int maximumPoolSize,
+ final Duration keepAliveTime,
+ final BlockingQueue