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/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..bf42ebb555e 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 9bed9d84a6e..83c2055d014 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 {
+ /**
+ * The returned {@link AsyncClientExecutor} must not be used, as its methods {@linkplain Assertions#fail() fail}.
+ */
+ static AsyncClientExecutor unimplemented() {
+ return 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}.
+ * {@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.
+ *
+ * Must not be used to execute blocking or application code. The only exception is {@link #uncaughtError(Error)}.
+ *
+ * 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();
+
+ /**
+ * Exists solely to execute {@link ThreadGroup#uncaughtException(Thread, Throwable)},
+ * which may execute application code and may be blocking.
+ *
+ * @see #uncaughtError(Error)
+ */
+ private final ExecutorService uncaughtExceptionHandlerExecutor;
+ private final MongoScheduledThreadPoolExecutor scheduler;
+
+ public static CommonExecutor commonExecutor() {
+ return INSTANCE;
+ }
+
+ private CommonExecutor() {
+ // `uncaughtExceptionHandlerExecutor` is the only executor that must not be `MongoThreadPoolExecutor`/`MongoScheduledThreadPoolExecutor`
+ uncaughtExceptionHandlerExecutor = new ThreadPoolExecutor(
+ 0, 1, 5, SECONDS, new LinkedBlockingQueue<>(), new DaemonThreadFactory("CommonUncaughtExceptionHandler"));
+ scheduler = new MongoScheduledThreadPoolExecutor(1, new DaemonThreadFactory("CommonScheduler"), LOGGER);
+ }
+
+ /**
+ * This method may be used in a situation when we cannot simply propagate an {@link Error} to the application.
+ * Handling an {@link Error} this way enables applications to decide how to deal with it via the
+ * {@linkplain Thread#getDefaultUncaughtExceptionHandler() default uncaught exception handler}.
+ *
+ * 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 peculiar situations,
+ * 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}.
+ *
+ * This method does not block. It also does not complete abruptly on the best-effort basis.
+ */
+ void uncaughtError(final Error e) {
+ Thread t = Thread.currentThread();
+ uncaughtExceptionHandlerExecutor.execute(() -> {
+ try {
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ } catch (Throwable ignored) {
+ // we are free to ignore the exception, and should ignore it
+ }
+ });
+ }
+
+ /**
+ * @param command The command 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 command},
+ * and there is nothing we can do about that, though {@link MongoScheduledThreadPoolExecutor#afterExecute(Runnable, Throwable)}
+ * logs the problem if {@link Executor#execute(Runnable)} completes abruptly.
+ * @param delay A non-{@linkplain Duration#isNegative() negative} delay.
+ * @param executor The {@link Executor} to use for {@code command} {@linkplain Runnable#run() execution},
+ * so that the {@code command} 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 command, final Duration delay, final Executor executor) {
+ try {
+ return scheduler.schedule(() -> executor.execute(command), 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
+ *
+ */
+public final class MongoThreadPoolExecutor extends ThreadPoolExecutor {
+ private final Logger logger;
+
+ public MongoThreadPoolExecutor(
+ final int corePoolSize,
+ final int maximumPoolSize,
+ final Duration keepAliveTime,
+ final BlockingQueue