diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java index 0336f698d8f..7445ab3f2cd 100644 --- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java +++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java @@ -156,6 +156,7 @@ private static Timeout calculateTimeout(@Nullable final Long timeoutMS) { public Timeout startServerSelectionTimeout() { long ms = getTimeoutSettings().getServerSelectionTimeoutMS(); - return StartTime.now().timeoutAfterOrInfiniteIfNegative(ms, MILLISECONDS); + Timeout serverSelectionTimeout = StartTime.now().timeoutAfterOrInfiniteIfNegative(ms, MILLISECONDS); + return serverSelectionTimeout.orEarlier(timeout); } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java b/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java index ebf8b86ce25..4a1164fb006 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java +++ b/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java @@ -105,26 +105,26 @@ public ServerTuple selectServer(final ServerSelector serverSelector, final Opera ServerSelector compositeServerSelector = getCompositeServerSelector(serverSelector); boolean selectionFailureLogged = false; - Timeout timeout = operationContext.getTimeoutContext().startServerSelectionTimeout(); + Timeout serverSelectionTimeout = operationContext.getTimeoutContext().startServerSelectionTimeout(); while (true) { CountDownLatch currentPhaseLatch = phase.get(); ClusterDescription currentDescription = description; - ServerTuple serverTuple = selectServer(compositeServerSelector, currentDescription, timeout); + ServerTuple serverTuple = selectServer(compositeServerSelector, currentDescription, serverSelectionTimeout); throwIfIncompatible(currentDescription); if (serverTuple != null) { return serverTuple; } - if (timeout.hasExpired()) { + if (serverSelectionTimeout.hasExpired()) { throw createTimeoutException(serverSelector, currentDescription); } if (!selectionFailureLogged) { - logServerSelectionFailure(serverSelector, currentDescription, timeout); + logServerSelectionFailure(serverSelector, currentDescription, serverSelectionTimeout); selectionFailureLogged = true; } connect(); - Timeout heartbeatLimitedTimeout = timeout.orEarlier(startMinWaitHeartbeatTimeout()); + Timeout heartbeatLimitedTimeout = serverSelectionTimeout.orEarlier(startMinWaitHeartbeatTimeout()); heartbeatLimitedTimeout.awaitOn(currentPhaseLatch, () -> format("waiting for a server that matches %s", serverSelector)); } diff --git a/driver-core/src/main/com/mongodb/internal/time/TimePoint.java b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java index cdc61cc268c..2cd42ccb955 100644 --- a/driver-core/src/main/com/mongodb/internal/time/TimePoint.java +++ b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java @@ -219,8 +219,12 @@ public int hashCode() { @Override public String toString() { + long remainingMs = nanos == null + ? -1 + : TimeUnit.MILLISECONDS.convert(currentNanos() - nanos, NANOSECONDS); return "TimePoint{" + "nanos=" + nanos + + "remainingMs=" + remainingMs + '}'; } } diff --git a/driver-core/src/main/com/mongodb/internal/time/Timeout.java b/driver-core/src/main/com/mongodb/internal/time/Timeout.java index f992bcf1e6c..7652ac4e26f 100644 --- a/driver-core/src/main/com/mongodb/internal/time/Timeout.java +++ b/driver-core/src/main/com/mongodb/internal/time/Timeout.java @@ -88,7 +88,7 @@ static Timeout infinite() { * @return a timeout that expires in the specified duration after now. */ static Timeout expiresIn(final long duration, final TimeUnit unit) { - // TODO (CSOT) confirm that all usages in final PR are non-negative + // TODO (CSOT) confirm that all usages in final PR always supply a non-negative duration if (duration < 0) { throw new AssertionError("Timeouts must not be in the past"); } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutAsyncProseTests.java b/driver-reactive-streams/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutAsyncProseTests.java new file mode 100644 index 00000000000..4ef16195dfe --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutAsyncProseTests.java @@ -0,0 +1,30 @@ +/* + * 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; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; + +public class ClientSideOperationsTimeoutAsyncProseTests extends ClientSideOperationsTimeoutProseTests { + + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return new SyncMongoClient(MongoClients.create(settings)); + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutProseTests.java new file mode 100644 index 00000000000..362ebd394b8 --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/internal/ClientSideOperationsTimeoutProseTests.java @@ -0,0 +1,88 @@ +/* + * 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; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoTimeoutException; +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * See + * Prose Tests. + */ +public class ClientSideOperationsTimeoutProseTests { + + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return MongoClients.create(settings); + } + + private long msElapsedSince(final long t1) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1); + } + + @NotNull + private MongoClientSettings createMongoClientSettings(final String connectionString) { + // All MongoClient instances created for tests MUST be configured + // with read/write concern majority, read preference primary, and + // TODO (CSOT): command monitoring enabled to listen for command_started events. + ConnectionString cs = new ConnectionString(connectionString); + MongoClientSettings.Builder builder = MongoClientSettings.builder() + .readConcern(ReadConcern.MAJORITY) + .writeConcern(WriteConcern.MAJORITY) + .readPreference(ReadPreference.primary()) + .applyConnectionString(cs); + return builder.build(); + } + + @ParameterizedTest + @ValueSource(strings = { + "mongodb://invalid/?serverSelectionTimeoutMS=10", + "mongodb://invalid/?timeoutMS=10&serverSelectionTimeoutMS=200", + "mongodb://invalid/?timeoutMS=200&serverSelectionTimeoutMS=10", + "mongodb://invalid/?timeoutMS=0&serverSelectionTimeoutMS=10", + }) + public void test8ServerSelectionPart1(final String connectionString) { + int timeoutBuffer = 100; // 5 in spec, Java is slower + // 1. Create a MongoClient + try (MongoClient mongoClient = createMongoClient(createMongoClientSettings(connectionString))) { + long start = System.nanoTime(); + // 2. Using client, execute: + Throwable throwable = assertThrows(MongoTimeoutException.class, () -> { + mongoClient.getDatabase("admin").runCommand(new BsonDocument("ping", new BsonInt32(1))); + }); + // Expect this to fail with a server selection timeout error after no more than 15ms [this is increased] + long elapsed = msElapsedSince(start); + assertTrue(throwable.getMessage().contains("while waiting for a server")); + assertTrue(elapsed < 10 + timeoutBuffer, "Took too long to time out, elapsedMS: " + elapsed); + } + } +}