diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java index ba3b8eb0ac..fbf183d05f 100644 --- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java +++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java @@ -289,6 +289,10 @@ public long getWriteTimeoutMS() { public int getConnectTimeoutMs() { final long connectTimeoutMS = getTimeoutSettings().getConnectTimeoutMS(); + if (isMaintenanceContext) { + return (int) connectTimeoutMS; + } + return Math.toIntExact(Timeout.nullAsInfinite(timeout).call(MILLISECONDS, () -> connectTimeoutMS, (ms) -> connectTimeoutMS == 0 ? ms : Math.min(ms, connectTimeoutMS), diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java index 8a0152c942..2619a3c2c1 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java @@ -81,11 +81,11 @@ public SocksSocket(@Nullable final Socket socket, final ProxySettings proxySetti } @Override - public void connect(final SocketAddress endpoint, final int timeoutMs) throws IOException { + public void connect(final SocketAddress endpoint, final int connectTimeoutMs) throws IOException { // `Socket` requires `IllegalArgumentException` - isTrueArgument("timeoutMs", timeoutMs >= 0); + isTrueArgument("connectTimeoutMs", connectTimeoutMs >= 0); try { - Timeout timeout = Timeout.expiresIn(timeoutMs, MILLISECONDS, ZERO_DURATION_MEANS_INFINITE); + Timeout timeout = Timeout.expiresIn(connectTimeoutMs, MILLISECONDS, ZERO_DURATION_MEANS_INFINITE); InetSocketAddress unresolvedAddress = (InetSocketAddress) endpoint; assertTrue(unresolvedAddress.isUnresolved()); this.remoteAddress = unresolvedAddress; diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java index 5cb042eaad..a04747820f 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java @@ -47,6 +47,7 @@ import com.mongodb.event.ConnectionClosedEvent; import com.mongodb.event.ConnectionCreatedEvent; import com.mongodb.event.ConnectionReadyEvent; +import com.mongodb.internal.connection.InternalStreamConnection; import com.mongodb.internal.connection.ServerHelper; import com.mongodb.internal.connection.TestCommandListener; import com.mongodb.internal.connection.TestConnectionPoolListener; @@ -908,6 +909,89 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() { assertEquals(1, failedEvents.size()); } + /** + * Not a prose spec test. However, it is additional test case for better coverage. + *

+ * From the spec: + * - When doing `minPoolSize` maintenance, `connectTimeoutMS` is used as the timeout for socket establishment. + */ + @Test + @DisplayName("Should use connectTimeoutMS when establishing connection in background") + public void shouldUseConnectTimeoutMsWhenEstablishingConnectionInBackground() { + assumeTrue(serverVersionAtLeast(4, 4)); + + collectionHelper.runAdminCommand("{" + + " configureFailPoint: \"failCommand\"," + + " mode: \"alwaysOn\"," + + " data: {" + + " failCommands: [\"hello\", \"isMaster\"]," + + " blockConnection: true," + + " blockTimeMS: " + 500 + + " }" + + "}"); + + try (MongoClient ignored = createMongoClient(getMongoClientSettingsBuilder() + .applyToConnectionPoolSettings(builder -> builder.minSize(1)) + // Use a very short timeout to ensure that the connection establishment will fail on the first handshake command. + .timeout(10, TimeUnit.MILLISECONDS))) { + InternalStreamConnection.setRecordEverything(true); + + // Wait for the connection to start establishment in the background. + sleep(1000); + } + + List commandStartedEvents = commandListener.getCommandStartedEvents("isMaster"); + assertEquals(1, commandStartedEvents.size()); + + List commandFailedEvents = commandListener.getCommandFailedEvents("isMaster"); + assertEquals(1, commandFailedEvents.size()); + assertInstanceOf(MongoOperationTimeoutException.class, commandFailedEvents.get(0).getThrowable()); + } + + /** + * Not a prose spec test. However, it is additional test case for better coverage. + *

+ * From the spec: + * - When doing `minPoolSize` maintenance, `connectTimeoutMS` is used as the timeout for socket establishment. After the connection + * is established, if timeoutMS is set at the MongoClient level, it MUST be used as the timeout for all commands sent as part of + * the MongoDB or authentication handshakes + *

+ * Therefore, if timeoutMS expires before connection establishment begins, connectTimeoutMS should not be used to start a TCP connection, + * since the connection will be closed immediately on the first handshake command due to the expired timeout. The timeout MUST be + * refreshed after each command. + */ + @Test + @DisplayName("Should throw MongoOperationTimeoutException when establishing connection in background if timeoutMs expired before starting connect") + public void shouldThrowMongoOperationTimeoutWhenEstablishingConnectionInBackgroundIfTimeoutMsExpiredBeforeStartingConnect() { + assumeTrue(serverVersionAtLeast(4, 4)); + + collectionHelper.runAdminCommand("{" + + " configureFailPoint: \"failCommand\"," + + " mode: \"alwaysOn\"," + + " data: {" + + " failCommands: [\"hello\", \"isMaster\"]," + + " blockConnection: true," + + " blockTimeMS: " + 500 + + " }" + + "}"); + + TestConnectionPoolListener connectionPoolListener = new TestConnectionPoolListener(); + try (MongoClient ignored = createMongoClient(getMongoClientSettingsBuilder() + .applyToConnectionPoolSettings(builder -> builder.minSize(1)) + .applyToConnectionPoolSettings(builder -> { + builder.addConnectionPoolListener(connectionPoolListener); + }) + // Use a very short timeout to ensure that the connection establishment will fail before the first handshake command. + .timeout(1, TimeUnit.MILLISECONDS))) { + InternalStreamConnection.setRecordEverything(true); + + // Wait for the connection to start establishment in the background. + sleep(1000); + } + List commandStartedEvents = commandListener.getCommandStartedEvents("isMaster"); + assertEquals(0, commandStartedEvents.size()); + } + private static Stream test8ServerSelectionArguments() { return Stream.of( Arguments.of(Named.of("serverSelectionTimeoutMS honored if timeoutMS is not set",