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
+ * 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