diff --git a/.evergreen/setup-env.bash b/.evergreen/setup-env.bash
index a8a7840292c..c8a96b0300c 100644
--- a/.evergreen/setup-env.bash
+++ b/.evergreen/setup-env.bash
@@ -1,5 +1,15 @@
# Java configurations for evergreen
+# On Windows Evergreen hosts `OS` is a native environment variable set to
+# "Windows_NT". It is not set on other platforms, so default it from `uname`
+# to avoid an unbound variable error under `set -u`.
+if [ -z "${OS:-}" ]; then
+ case "$(uname -s)" in
+ CYGWIN*|MINGW*|MSYS*|Windows_NT) OS="Windows_NT" ;;
+ *) OS="$(uname -s)" ;;
+ esac
+fi
+
if [ "Windows_NT" == "$OS" ]; then
export JDK8="/cygdrive/c/java/jdk8"
export JDK11="/cygdrive/c/java/jdk11"
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
index 99694a9649d..27de4840633 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -1003,8 +1003,14 @@ private static void rethrowIfError(final Throwable t) {
private void throwTranslatedWriteException(final Throwable e, final OperationContext operationContext) {
rethrowIfError(e);
- if (e instanceof MongoSocketWriteTimeoutException && operationContext.getTimeoutContext().hasTimeoutMS()) {
- throw createMongoTimeoutException(e);
+ if (operationContext.getTimeoutContext().hasTimeoutMS()) {
+ if (e instanceof MongoSocketWriteTimeoutException) {
+ throw createMongoTimeoutException(e);
+ } else if (e instanceof SocketTimeoutException) {
+ // A blocking (SSL) write can surface the socket read-timeout as a bare SocketTimeoutException;
+ // mirror translateReadException so it is reported as a timeout rather than a generic write error.
+ throw createMongoTimeoutException(createWriteTimeoutException((SocketTimeoutException) e));
+ }
}
if (e instanceof MongoException) {
@@ -1032,6 +1038,10 @@ private Throwable translateReadFailure(final Throwable e, final OperationContext
return translateReadException(e, operationContext);
}
+ private MongoSocketWriteTimeoutException createWriteTimeoutException(final SocketTimeoutException e) {
+ return new MongoSocketWriteTimeoutException("Timeout while sending message", getServerAddress(), e);
+ }
+
private MongoException translateReadException(final Throwable e, final OperationContext operationContext) {
if (operationContext.getTimeoutContext().hasTimeoutMS()) {
if (e instanceof SocketTimeoutException) {
diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java
index 80c09a5cf01..868dfac4521 100644
--- a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java
+++ b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java
@@ -84,6 +84,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -837,4 +838,19 @@ private static OperationContext applySessionContext(final OperationContext opera
public static OperationContext getOperationContext(final ReadPreference readPreference) {
return applySessionContext(OPERATION_CONTEXT, readPreference);
}
+
+ /** Factor by which CSOT timeout/block values are widened on Windows, whose slower TLS setup eats tight budgets. */
+ public static final int WINDOWS_CSOT_TIMEOUT_MULTIPLIER = 10;
+
+ public static boolean isWindows() {
+ return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).startsWith("windows");
+ }
+
+ /**
+ * Scales a CSOT timeout / failpoint-block value by {@link #WINDOWS_CSOT_TIMEOUT_MULTIPLIER} on Windows (unchanged
+ * elsewhere). Scale a test's timeout and its blockTimeMS together to preserve which command times out.
+ */
+ public static int scaleForWindows(final int millis) {
+ return isWindows() ? millis * WINDOWS_CSOT_TIMEOUT_MULTIPLIER : millis;
+ }
}
diff --git a/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java
index 228dc3ede76..16c0c2f856b 100644
--- a/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java
+++ b/driver-core/src/test/functional/com/mongodb/client/model/mql/TypeMqlValuesFunctionalTest.java
@@ -190,8 +190,8 @@ public void asStringTest() {
}
@Test
- public void asStringTestNestedPre82() {
- assumeTrue(serverVersionLessThan(8, 2));
+ public void asStringTestNestedPre83() {
+ assumeTrue(serverVersionLessThan(8, 3));
// Arrays and documents are not (yet) supported:
assertThrows(MongoCommandException.class, () ->
@@ -202,7 +202,7 @@ public void asStringTestNestedPre82() {
@Test
public void asStringTestNested() {
- assumeTrue(serverVersionAtLeast(8, 2));
+ assumeTrue(serverVersionAtLeast(8, 3));
assertExpression("[1,2]", ofIntegerArray(1, 2).asString());
assertExpression("{\"a\":1}", of(Document.parse("{a: 1}")).asString());
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy
index 3cdabf31da3..c9d0525ad19 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy
@@ -25,6 +25,7 @@ import com.mongodb.MongoSocketException
import com.mongodb.MongoSocketReadException
import com.mongodb.MongoSocketReadTimeoutException
import com.mongodb.MongoSocketWriteException
+import com.mongodb.MongoSocketWriteTimeoutException
import com.mongodb.ReadConcern
import com.mongodb.ServerAddress
import com.mongodb.async.FutureResultCallback
@@ -476,6 +477,25 @@ class InternalStreamConnectionSpecification extends Specification {
connection.isClosed()
}
+ def 'Should throw timeout exception with underlying socket exception as a cause when Stream.write throws SocketTimeoutException'() {
+ given:
+ stream.write(_, _) >> { throw new SocketTimeoutException() }
+ def connection = getOpenedConnection()
+ def (buffers, messageId) = helper.hello()
+
+ when:
+ connection.sendMessage(buffers, messageId, OPERATION_CONTEXT.withTimeoutContext(
+ new TimeoutContext(TIMEOUT_SETTINGS_WITH_INFINITE_TIMEOUT)))
+
+ then:
+ def timeoutException = thrown(MongoOperationTimeoutException)
+ def mongoSocketWriteTimeoutException = timeoutException.getCause()
+ mongoSocketWriteTimeoutException instanceof MongoSocketWriteTimeoutException
+ mongoSocketWriteTimeoutException.getCause() instanceof SocketTimeoutException
+
+ connection.isClosed()
+ }
+
def 'Should wrap MongoSocketReadTimeoutException with MongoOperationTimeoutException'() {
given:
stream.read(_, _) >> { throw new MongoSocketReadTimeoutException("test", new ServerAddress(), null) }
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 7828ecde684..63bacfc7596 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -86,6 +86,7 @@
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
import static com.mongodb.ClusterFixture.isLoadBalanced;
import static com.mongodb.ClusterFixture.isStandalone;
+import static com.mongodb.ClusterFixture.scaleForWindows;
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static com.mongodb.ClusterFixture.sleep;
import static com.mongodb.client.Fixture.getDefaultDatabaseName;
@@ -110,6 +111,14 @@
/**
* See
* Prose Tests.
+ *
+ *
Each test declares its timeout/block/sleep values as local {@code int} variables (in milliseconds) after the
+ * assume checks. Tests where a fresh-client foreground operation races connection establishment wrap those values in
+ * {@link ClusterFixture#scaleForWindows(int)}, which widens them on the slower Windows + TLS CI hosts so setup does
+ * not consume the tight budget. Tests are intentionally left unscaled (plain literals) when they assert a
+ * fast-timeout upper bound or a timeout-precedence relationship, exercise pool-wait / connection-close behaviour
+ * (where a background op holds the pool connection and scaling would outlive {@code close()}), or already use an
+ * ample timeout; such tests carry a brief "not scaled" comment explaining why.
*/
@SuppressWarnings("checkstyle:VisibilityModifier")
public abstract class AbstractClientSideOperationsTimeoutProseTest {
@@ -146,6 +155,11 @@ public void testBackgroundConnectionPoolingTimeoutMSUsedForHandshakeCommands() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeTrue(isAuthenticated());
+ // not scaled: background-pool handshake under connectTimeoutMS; blockTimeMS must stay above timeoutMS so the
+ // handshake times out and the connection is closed.
+ int blockTimeMS = 150;
+ int timeoutMS = 100;
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
+ " mode: {"
@@ -154,7 +168,7 @@ public void testBackgroundConnectionPoolingTimeoutMSUsedForHandshakeCommands() {
+ " data: {"
+ " failCommands: [\"saslContinue\"],"
+ " blockConnection: true,"
- + " blockTimeMS: 150,"
+ + " blockTimeMS: " + blockTimeMS + ","
+ " appName: \"timeoutBackgroundPoolTest\""
+ " }"
+ "}");
@@ -167,7 +181,7 @@ public void testBackgroundConnectionPoolingTimeoutMSUsedForHandshakeCommands() {
builder.minSize(1);
builder.addConnectionPoolListener(connectionPoolListener);
})
- .timeout(100, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
assertDoesNotThrow(() ->
connectionPoolListener.waitForEvents(asList(ConnectionCreatedEvent.class, ConnectionClosedEvent.class),
@@ -182,13 +196,17 @@ public void testBackgroundConnectionPoolingTimeoutMSIsRefreshedForEachHandshakeC
assumeTrue(serverVersionAtLeast(4, 4));
assumeTrue(isAuthenticated());
+ // not scaled: background-pool handshake; verifies the per-command timeout is refreshed while establishing.
+ int blockTimeMS = 150;
+ int timeoutMS = 250;
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
+ " mode: \"alwaysOn\","
+ " data: {"
+ " failCommands: [\"hello\", \"isMaster\", \"saslContinue\"],"
+ " blockConnection: true,"
- + " blockTimeMS: 150,"
+ + " blockTimeMS: " + blockTimeMS + ","
+ " appName: \"refreshTimeoutBackgroundPoolTest\""
+ " }"
+ "}");
@@ -201,7 +219,7 @@ public void testBackgroundConnectionPoolingTimeoutMSIsRefreshedForEachHandshakeC
builder.minSize(1);
builder.addConnectionPoolListener(connectionPoolListener);
})
- .timeout(250, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
assertDoesNotThrow(() ->
connectionPoolListener.waitForEvents(asList(ConnectionCreatedEvent.class, ConnectionReadyEvent.class),
@@ -214,6 +232,9 @@ public void testBackgroundConnectionPoolingTimeoutMSIsRefreshedForEachHandshakeC
public void testBlockingIterationMethodsTailableCursor() {
assumeTrue(serverVersionAtLeast(4, 4));
+ int blockTimeMS = scaleForWindows(150);
+ int timeoutMS = scaleForWindows(250);
+
collectionHelper.create(namespace.getCollectionName(),
new CreateCollectionOptions().capped(true).sizeInBytes(10 * 1024 * 1024));
collectionHelper.insertDocuments(singletonList(BsonDocument.parse("{x: 1}")), WriteConcern.MAJORITY);
@@ -223,12 +244,12 @@ public void testBlockingIterationMethodsTailableCursor() {
+ " data: {"
+ " failCommands: [\"getMore\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 150
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(250, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoCollection collection = client.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
@@ -252,8 +273,12 @@ public void testBlockingIterationMethodsChangeStream() {
assumeTrue(isDiscoverableReplicaSet());
assumeFalse(isAsync()); // Async change stream cursor is non-deterministic for cursor::next
+ int blockTimeMS = scaleForWindows(150);
+ int timeoutMS = scaleForWindows(250);
+ int sleepMS = 2000;
+
BsonTimestamp startTime = new BsonTimestamp((int) Instant.now().getEpochSecond(), 0);
- sleep(2000);
+ sleep(sleepMS);
collectionHelper.insertDocuments(singletonList(BsonDocument.parse("{x: 1}")), WriteConcern.MAJORITY);
collectionHelper.runAdminCommand("{"
@@ -262,12 +287,12 @@ public void testBlockingIterationMethodsChangeStream() {
+ " data: {"
+ " failCommands: [\"getMore\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 150
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(250, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName()).withReadPreference(ReadPreference.primary());
@@ -295,13 +320,16 @@ public void testBlockingIterationMethodsChangeStream() {
public void testGridFSUploadViaOpenUploadStreamTimeout() {
assumeTrue(serverVersionAtLeast(4, 4));
+ int blockTimeMS = scaleForWindows(205);
+ int timeoutMS = scaleForWindows(200);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 205
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -309,7 +337,7 @@ public void testGridFSUploadViaOpenUploadStreamTimeout() {
filesCollectionHelper.create();
try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(200, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoDatabase database = client.getDatabase(namespace.getDatabaseName());
GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME);
@@ -325,13 +353,16 @@ public void testGridFSUploadViaOpenUploadStreamTimeout() {
public void testAbortingGridFsUploadStreamTimeout() throws Throwable {
assumeTrue(serverVersionAtLeast(4, 4));
+ int blockTimeMS = scaleForWindows(320);
+ int timeoutMS = scaleForWindows(300);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"delete\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 320
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -339,7 +370,7 @@ public void testAbortingGridFsUploadStreamTimeout() throws Throwable {
filesCollectionHelper.create();
try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(300, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoDatabase database = client.getDatabase(namespace.getDatabaseName());
GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME).withChunkSizeBytes(2);
@@ -355,6 +386,9 @@ public void testAbortingGridFsUploadStreamTimeout() throws Throwable {
public void testGridFsDownloadStreamTimeout() {
assumeTrue(serverVersionAtLeast(4, 4));
+ int blockTimeMS = scaleForWindows(500);
+ int timeoutMS = scaleForWindows(300);
+
chunksCollectionHelper.create();
filesCollectionHelper.create();
@@ -382,12 +416,12 @@ public void testGridFsDownloadStreamTimeout() {
+ " data: {"
+ " failCommands: [\"find\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(300, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoDatabase database = client.getDatabase(namespace.getDatabaseName());
GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME).withChunkSizeBytes(2);
@@ -434,6 +468,11 @@ public void test8ServerSelectionHandshake(final String ignoredTestName, final in
assumeTrue(serverVersionAtLeast(4, 4));
assumeTrue(isAuthenticated());
+ // not scaled: asserts a fast-timeout upper bound (maxElapsedMS) and timeoutMS-vs-serverSelectionTimeoutMS
+ // precedence; scaling would change what is being asserted.
+ int blockTimeMS = 600;
+ int maxElapsedMS = 350;
+
MongoCredential credential = getConnectionString().getCredential();
assertNotNull(credential);
assertNull(credential.getAuthenticationMechanism());
@@ -446,7 +485,7 @@ public void test8ServerSelectionHandshake(final String ignoredTestName, final in
+ " data: {"
+ " failCommands: [\"saslContinue\"],"
+ " blockConnection: true,"
- + " blockTimeMS: 600"
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -462,7 +501,7 @@ public void test8ServerSelectionHandshake(final String ignoredTestName, final in
.insertOne(new Document("x", 1));
});
long elapsed = msElapsedSince(start);
- assertTrue(elapsed <= 350, "Took too long to time out, elapsedMS: " + elapsed);
+ assertTrue(elapsed <= maxElapsedMS, "Took too long to time out, elapsedMS: " + elapsed);
}
}
@@ -473,18 +512,22 @@ public void test9EndSessionClientTimeout() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+ int blockTimeMS = scaleForWindows(500);
+ int timeoutMS = scaleForWindows(250);
+ int maxElapsedMS = scaleForWindows(300);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"abortTransaction\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder().retryWrites(false)
- .timeout(250, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoDatabase database = mongoClient.getDatabase(namespace.getDatabaseName());
MongoCollection collection = database
.getCollection(namespace.getCollectionName());
@@ -495,7 +538,7 @@ public void test9EndSessionClientTimeout() {
long start = System.nanoTime();
session.close();
long elapsed = msElapsedSince(start);
- assertTrue(elapsed <= 300, "Took too long to time out, elapsedMS: " + elapsed);
+ assertTrue(elapsed <= maxElapsedMS, "Took too long to time out, elapsedMS: " + elapsed);
}
}
CommandFailedEvent abortTransactionEvent = assertDoesNotThrow(() ->
@@ -510,13 +553,17 @@ public void test9EndSessionSessionTimeout() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+ int blockTimeMS = scaleForWindows(400);
+ int defaultTimeoutMS = scaleForWindows(300);
+ int maxElapsedMS = scaleForWindows(400);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"abortTransaction\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 400
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -525,14 +572,14 @@ public void test9EndSessionSessionTimeout() {
.getCollection(namespace.getCollectionName());
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(300, TimeUnit.MILLISECONDS).build())) {
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS).build())) {
session.startTransaction();
collection.insertOne(session, new Document("x", 1));
long start = System.nanoTime();
session.close();
long elapsed = msElapsedSince(start);
- assertTrue(elapsed <= 400, "Took too long to time out, elapsedMS: " + elapsed);
+ assertTrue(elapsed <= maxElapsedMS, "Took too long to time out, elapsedMS: " + elapsed);
}
}
CommandFailedEvent abortTransactionEvent = assertDoesNotThrow(() ->
@@ -545,13 +592,17 @@ public void test9EndSessionSessionTimeout() {
public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithCommit() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+
+ int blockTimeMS = scaleForWindows(25);
+ int defaultTimeoutMS = scaleForWindows(300);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 25
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -559,12 +610,11 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithCommit() {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
- int defaultTimeout = 300;
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) {
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS).build())) {
session.startTransaction();
collection.insertOne(session, new Document("x", 1));
- sleep(defaultTimeout);
+ sleep(defaultTimeoutMS);
assertDoesNotThrow(session::commitTransaction);
}
@@ -577,13 +627,17 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithCommit() {
public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithAbort() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+
+ int blockTimeMS = scaleForWindows(25);
+ int defaultTimeoutMS = scaleForWindows(300);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 25
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -591,12 +645,11 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithAbort() {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
- int defaultTimeout = 300;
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) {
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS).build())) {
session.startTransaction();
collection.insertOne(session, new Document("x", 1));
- sleep(defaultTimeout);
+ sleep(defaultTimeoutMS);
assertDoesNotThrow(session::close);
}
@@ -610,18 +663,22 @@ public void test10ConvenientTransactions() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
assumeFalse(isAsync());
+
+ int blockTimeMS = scaleForWindows(200);
+ int timeoutMS = scaleForWindows(150);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 2 },"
+ " data: {"
+ " failCommands: [\"insert\", \"abortTransaction\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 200
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(150, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
@@ -645,13 +702,17 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
assumeFalse(isAsync());
+
+ int blockTimeMS = scaleForWindows(25);
+ int defaultTimeoutMS = scaleForWindows(200);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 25
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -659,13 +720,12 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
- int defaultTimeout = 200;
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) {
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS).build())) {
assertThrows(MongoOperationTimeoutException.class,
() -> session.withTransaction(() -> {
collection.insertOne(session, new Document("x", 1));
- sleep(defaultTimeout);
+ sleep(defaultTimeoutMS);
return true;
})
);
@@ -679,13 +739,17 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
assumeFalse(isAsync());
+
+ int blockTimeMS = scaleForWindows(25);
+ int defaultTimeoutMS = scaleForWindows(200);
+
collectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"failCommand\","
+ " mode: \"alwaysOn\","
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 25
+ + " blockTimeMS: " + blockTimeMS
+ " errorCode: " + 24
+ " errorLabels: [\"TransientTransactionError\"]"
+ " }"
@@ -695,13 +759,12 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
- int defaultTimeout = 200;
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) {
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS).build())) {
assertThrows(MongoOperationTimeoutException.class,
() -> session.withTransaction(() -> {
collection.insertOne(session, new Document("x", 1));
- sleep(defaultTimeout);
+ sleep(defaultTimeoutMS);
return true;
})
);
@@ -714,6 +777,11 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
@SuppressWarnings("try")
protected void test11MultiBatchBulkWrites() throws InterruptedException {
assumeTrue(serverVersionAtLeast(8, 0));
+
+ // not scaled: timeoutMS is already ample relative to connection setup.
+ int blockTimeMS = 2020;
+ int timeoutMS = 4000;
+
try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder())) {
// a workaround for https://jira.mongodb.org/browse/DRIVERS-2997, remove this block when the aforementioned bug is fixed
client.getDatabase(namespace.getDatabaseName()).drop();
@@ -724,12 +792,11 @@ protected void test11MultiBatchBulkWrites() throws InterruptedException {
+ " data: {"
+ " failCommands: [\"bulkWrite\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 2020
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
- long timeout = 4000;
- try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder().timeout(timeout, TimeUnit.MILLISECONDS));
+ try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder().timeout(timeoutMS, TimeUnit.MILLISECONDS));
FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
MongoDatabase db = client.getDatabase(namespace.getDatabaseName());
db.drop();
@@ -757,19 +824,22 @@ public void shouldNotIncludeWtimeoutMsOfWriteConcernToInitialAndSubsequentCommit
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+ // not scaled: asserts wTimeoutMS is excluded from the commitTransaction command; not a setup-race timing test.
+ int defaultTimeoutMS = 200;
+ int wTimeoutMS = 100;
+
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder())) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
- int defaultTimeout = 200;
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS)
.build())) {
session.startTransaction(TransactionOptions.builder()
- .writeConcern(WriteConcern.ACKNOWLEDGED.withWTimeout(100, TimeUnit.MILLISECONDS))
+ .writeConcern(WriteConcern.ACKNOWLEDGED.withWTimeout(wTimeoutMS, TimeUnit.MILLISECONDS))
.build());
collection.insertOne(session, new Document("x", 1));
- sleep(defaultTimeout);
+ sleep(defaultTimeoutMS);
assertDoesNotThrow(session::commitTransaction);
//repeat commit.
@@ -796,11 +866,18 @@ public void shouldNotIncludeWtimeoutMsOfWriteConcernToInitialAndSubsequentCommit
public void shouldIgnoreWaitQueueTimeoutMSWhenTimeoutMsIsSet() {
assumeTrue(serverVersionAtLeast(4, 4));
+ // not scaled: a background op holds the single pool connection for blockTimeMS; scaling it would keep the
+ // connection checked out past client close() and trip the "all connections closed" assertion.
+ int timeoutMS = 500;
+ int maxWaitTimeMS = 1;
+ int blockTimeMS = 450;
+ int sleepMS = 150;
+
//given
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(500, TimeUnit.MILLISECONDS)
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS)
.applyToConnectionPoolSettings(builder -> builder
- .maxWaitTime(1, TimeUnit.MILLISECONDS)
+ .maxWaitTime(maxWaitTimeMS, TimeUnit.MILLISECONDS)
.maxSize(1)
))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
@@ -812,12 +889,12 @@ public void shouldIgnoreWaitQueueTimeoutMSWhenTimeoutMsIsSet() {
+ " data: {"
+ " failCommands: [\"find\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 450
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
executor.execute(() -> collection.find().first());
- sleep(150);
+ sleep(sleepMS);
//when && then
assertDoesNotThrow(() -> collection.find().first());
@@ -832,9 +909,15 @@ public void shouldIgnoreWaitQueueTimeoutMSWhenTimeoutMsIsSet() {
public void shouldThrowOperationTimeoutExceptionWhenConnectionIsNotAvailableAndTimeoutMSIsSet() {
assumeTrue(serverVersionAtLeast(4, 4));
+ // not scaled: a background op holds the single pool connection for blockTimeMS; scaling it would keep the
+ // connection checked out past client close() and trip the "all connections closed" assertion.
+ int timeoutMS = 100;
+ int blockTimeMS = 500;
+ int sleepMS = 100;
+
//given
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(100, TimeUnit.MILLISECONDS)
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS)
.applyToConnectionPoolSettings(builder -> builder
.maxSize(1)
))) {
@@ -847,12 +930,12 @@ public void shouldThrowOperationTimeoutExceptionWhenConnectionIsNotAvailableAndT
+ " data: {"
+ " failCommands: [\"find\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
executor.execute(() -> collection.withTimeout(0, TimeUnit.MILLISECONDS).find().first());
- sleep(100);
+ sleep(sleepMS);
//when && then
assertThrows(MongoOperationTimeoutException.class, () -> collection.find().first());
@@ -867,10 +950,16 @@ public void shouldThrowOperationTimeoutExceptionWhenConnectionIsNotAvailableAndT
public void shouldUseWaitQueueTimeoutMSWhenTimeoutIsNotSet() {
assumeTrue(serverVersionAtLeast(4, 4));
+ // not scaled: timeoutMS is not set here (exercises waitQueueTimeoutMS), and a background op holds the pool
+ // connection for blockTimeMS.
+ int maxWaitTimeMS = 20;
+ int blockTimeMS = 400;
+ int sleepMS = 200;
+
//given
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
.applyToConnectionPoolSettings(builder -> builder
- .maxWaitTime(20, TimeUnit.MILLISECONDS)
+ .maxWaitTime(maxWaitTimeMS, TimeUnit.MILLISECONDS)
.maxSize(1)
))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
@@ -882,12 +971,12 @@ public void shouldUseWaitQueueTimeoutMSWhenTimeoutIsNotSet() {
+ " data: {"
+ " failCommands: [\"find\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 400
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
executor.execute(() -> collection.find().first());
- sleep(200);
+ sleep(sleepMS);
//when & then
assertThrows(MongoTimeoutException.class, () -> collection.find().first());
@@ -903,6 +992,11 @@ public void testKillCursorsIsNotExecutedAfterGetMoreNetworkErrorWhenTimeoutMsIsN
assumeTrue(serverVersionAtLeast(4, 4));
assumeTrue(isLoadBalanced());
+ // not scaled: exercises socket readTimeoutMS (not CSOT timeoutMS) and killCursors behaviour after a network
+ // error, independent of connection-setup timing.
+ int blockTimeMS = 600;
+ int readTimeoutMS = 500;
+
collectionHelper.create(namespace.getCollectionName(), new CreateCollectionOptions());
collectionHelper.insertDocuments(new Document(), new Document());
collectionHelper.runAdminCommand("{"
@@ -911,13 +1005,13 @@ public void testKillCursorsIsNotExecutedAfterGetMoreNetworkErrorWhenTimeoutMsIsN
+ " data: {"
+ " failCommands: [\"getMore\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 600
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
.retryReads(true)
- .applyToSocketSettings(builder -> builder.readTimeout(500, TimeUnit.MILLISECONDS)))) {
+ .applyToSocketSettings(builder -> builder.readTimeout(readTimeoutMS, TimeUnit.MILLISECONDS)))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName()).withReadPreference(ReadPreference.primary());
@@ -949,6 +1043,11 @@ public void testKillCursorsIsNotExecutedAfterGetMoreNetworkError() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeTrue(isLoadBalanced());
+ // not scaled: verifies killCursors is not executed after a getMore network error, independent of
+ // connection-setup timing.
+ int blockTimeMS = 600;
+ int timeoutMS = 500;
+
collectionHelper.create(namespace.getCollectionName(), new CreateCollectionOptions());
collectionHelper.insertDocuments(new Document(), new Document());
collectionHelper.runAdminCommand("{"
@@ -957,12 +1056,12 @@ public void testKillCursorsIsNotExecutedAfterGetMoreNetworkError() {
+ " data: {"
+ " failCommands: [\"getMore\" ],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 600
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder()
- .timeout(500, TimeUnit.MILLISECONDS))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName()).withReadPreference(ReadPreference.primary());
@@ -994,16 +1093,20 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
+ int defaultTimeoutMS = scaleForWindows(200);
+ int blockTimeMS = scaleForWindows(500);
+ int sleepMS = 200;
+
try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder())) {
MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder()
- .defaultTimeout(200, TimeUnit.MILLISECONDS)
+ .defaultTimeout(defaultTimeoutMS, TimeUnit.MILLISECONDS)
.build())) {
session.startTransaction(TransactionOptions.builder().build());
collection.insertOne(session, new Document("x", 1));
- sleep(200);
+ sleep(sleepMS);
assertDoesNotThrow(session::commitTransaction);
@@ -1013,7 +1116,7 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() {
+ " data: {"
+ " failCommands: [\"commitTransaction\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -1039,13 +1142,18 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() {
public void shouldUseConnectTimeoutMsWhenEstablishingConnectionInBackground() {
assumeTrue(serverVersionAtLeast(4, 4));
+ // not scaled: deliberately uses a very short timeoutMS to force the background handshake command to time out.
+ int blockTimeMS = 500;
+ int timeoutMS = 10;
+ int sleepMS = 1000;
+
collectionHelper.runAdminCommand("{"
+ "configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
+ "mode: \"alwaysOn\","
+ " data: {"
+ " failCommands: [\"hello\", \"isMaster\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500 + ","
+ + " blockTimeMS: " + blockTimeMS + ","
// The appName is unique to prevent this failpoint from affecting ClusterFixture's ServerMonitor.
// Without the appName, ClusterFixture's heartbeats would be blocked, polluting RTT measurements with 500ms values,
// which would cause flakiness in other prose tests that use ClusterFixture.getPrimaryRTT() for timeout adjustments.
@@ -1057,11 +1165,11 @@ public void shouldUseConnectTimeoutMsWhenEstablishingConnectionInBackground() {
.applicationName("connectTimeoutBackgroundTest")
.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))) {
+ .timeout(timeoutMS, TimeUnit.MILLISECONDS))) {
InternalStreamConnection.setRecordEverything(true);
// Wait for the connection to start establishment in the background.
- sleep(1000);
+ sleep(sleepMS);
} finally {
InternalStreamConnection.setRecordEverything(false);
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java
index 8114d62e41a..8e1227803ba 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java
@@ -54,7 +54,11 @@ public static synchronized MongoClient getMongoClient() {
return;
}
if (defaultDatabase != null) {
- defaultDatabase.drop();
+ try {
+ defaultDatabase.drop();
+ } catch (Exception e) {
+ // ignore
+ }
}
mongoClient.close();
mongoClient = null;
diff --git a/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java
index 04303833bf5..f0e74b5c707 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java
@@ -57,6 +57,7 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import static com.mongodb.ClusterFixture.scaleForWindows;
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -94,12 +95,15 @@ public abstract class AbstractClientSideOperationsEncryptionTimeoutProseTest {
void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() {
assumeTrue(serverVersionAtLeast(4, 4));
+ int timeoutMS = scaleForWindows(100);
+ int blockTimeMS = scaleForWindows(100);
+
Map> kmsProviders = new HashMap<>();
Map localProviderMap = new HashMap<>();
localProviderMap.put("key", Base64.getDecoder().decode(MASTER_KEY));
kmsProviders.put("local", localProviderMap);
- try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(100))) {
+ try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(timeoutMS))) {
keyVaultCollectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
@@ -107,7 +111,7 @@ void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() {
+ " data: {"
+ " failCommands: [\"insert\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 100
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -126,7 +130,10 @@ void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() {
void shouldThrowOperationTimeoutExceptionWhenEncryptData() {
assumeTrue(serverVersionAtLeast(4, 4));
- try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(150))) {
+ int timeoutMS = scaleForWindows(150);
+ int blockTimeMS = scaleForWindows(150);
+
+ try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(timeoutMS))) {
clientEncryption.createDataKey("local");
@@ -136,7 +143,7 @@ void shouldThrowOperationTimeoutExceptionWhenEncryptData() {
+ " data: {"
+ " failCommands: [\"find\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 150
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -159,8 +166,11 @@ void shouldThrowOperationTimeoutExceptionWhenEncryptData() {
void shouldThrowOperationTimeoutExceptionWhenDecryptData() {
assumeTrue(serverVersionAtLeast(4, 4));
+ int timeoutMS = scaleForWindows(400);
+ int blockTimeMS = scaleForWindows(500);
+
BsonBinary encrypted;
- try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(400))) {
+ try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(timeoutMS))) {
clientEncryption.createDataKey("local");
BsonBinary dataKey = clientEncryption.createDataKey("local");
EncryptOptions encryptOptions = new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic");
@@ -168,14 +178,14 @@ void shouldThrowOperationTimeoutExceptionWhenDecryptData() {
encrypted = clientEncryption.encrypt(new BsonString("hello"), encryptOptions);
}
- try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(400))) {
+ try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(timeoutMS))) {
keyVaultCollectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
+ " mode: { times: 1 },"
+ " data: {"
+ " failCommands: [\"find\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 500
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
commandListener.reset();
@@ -194,7 +204,10 @@ void shouldThrowOperationTimeoutExceptionWhenDecryptData() {
@Test
void shouldDecreaseOperationTimeoutForSubsequentOperations() {
assumeTrue(serverVersionAtLeast(4, 4));
+
+ // not scaled: initialTimeoutMS is already ample relative to connection setup.
long initialTimeoutMS = 2500;
+ int blockTimeMS = 10;
keyVaultCollectionHelper.runAdminCommand("{"
+ " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\","
@@ -202,7 +215,7 @@ void shouldDecreaseOperationTimeoutForSubsequentOperations() {
+ " data: {"
+ " failCommands: [\"insert\", \"find\", \"listCollections\"],"
+ " blockConnection: true,"
- + " blockTimeMS: " + 10
+ + " blockTimeMS: " + blockTimeMS
+ " }"
+ "}");
@@ -268,7 +281,7 @@ void shouldDecreaseOperationTimeoutForSubsequentOperations() {
void shouldThrowTimeoutExceptionWhenCreateEncryptedCollection(final String commandToTimeout) {
assumeTrue(serverVersionAtLeast(7, 0));
//given
- long initialTimeoutMS = 200;
+ long initialTimeoutMS = scaleForWindows(200);
try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder()
.timeout(initialTimeoutMS, MILLISECONDS))) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
index e90941b6c09..76371e561c1 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java
@@ -34,6 +34,7 @@
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
import static com.mongodb.ClusterFixture.isSharded;
+import static com.mongodb.ClusterFixture.isWindows;
import static com.mongodb.ClusterFixture.serverVersionLessThan;
import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.assertTrue;
@@ -80,23 +81,6 @@ public static void applyCustomizations(final TestDef def) {
+ "unspecified on server 9.0+ (DRIVERS-3006)")
.when(() -> !serverVersionLessThan(9, 0))
.directory("client-side-operations-timeout");
- def.retry("Unified CSOT tests do not account for RTT which varies in TLS vs non-TLS runs")
- .whenFailureContains("timeout")
- .test("client-side-operations-timeout",
- "timeoutMS behaves correctly for non-tailable cursors",
- "timeoutMS is refreshed for getMore if timeoutMode is iteration - success");
-
- def.retry("Unified CSOT tests do not account for RTT which varies in TLS vs non-TLS runs")
- .whenFailureContains("timeout")
- .test("client-side-operations-timeout",
- "timeoutMS behaves correctly for tailable non-awaitData cursors",
- "timeoutMS is refreshed for getMore - success");
-
- def.retry("Unified CSOT tests do not account for RTT which varies in TLS vs non-TLS runs")
- .whenFailureContains("timeout")
- .test("client-side-operations-timeout",
- "timeoutMS behaves correctly for tailable non-awaitData cursors",
- "timeoutMS is refreshed for getMore - success");
//TODO-invistigate
/*
@@ -210,6 +194,116 @@ public static void applyCustomizations(final TestDef def) {
.test("client-side-operations-timeout", "timeoutMS can be configured on a MongoClient",
"timeoutMS can be set to 0 on a MongoClient - dropIndexes on collection");
+ def.retry("Unified CSOT tests do not account for RTT which varies in TLS vs non-TLS runs")
+ .whenFailureContains("timeout")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for non-tailable cursors",
+ "timeoutMS is refreshed for getMore if timeoutMode is iteration - success");
+ def.retry("Unified CSOT tests do not account for RTT which varies in TLS vs non-TLS runs")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for tailable non-awaitData cursors",
+ "timeoutMS is refreshed for getMore - success");
+
+ // Windows / slower-TLS-host CSOT timeout adjustments (JAVA-6057): these tests are calibrated for the fast
+ // Linux CI hosts; on Windows connection establishment consumes the tight budgets. See
+ // ClusterFixture.scaleForWindows and scaleForWindows(BsonArray, BsonDocument, int) below.
+
+ // "Whole operation" tests block two commands sharing one budget and expect the 2nd to time out. Scale
+ // timeoutMS and blockTimeMS by the same factor so the ordering is preserved with more setup headroom.
+ def.transform("JAVA-6057: whole-operation CSOT timeouts (cursor-lifetime, bulkWrite) are too tight for the "
+ + "slower Windows TLS hosts, where the budget is consumed by connection establishment before "
+ + "the command under test is sent",
+ (entitiesArray, definition) -> scaleForWindows(entitiesArray, definition, 10))
+ .when(ClusterFixture::isWindows)
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for non-tailable cursors",
+ "remaining timeoutMS applied to getMore if timeoutMode is cursor_lifetime")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for non-tailable cursors",
+ "remaining timeoutMS applied to getMore if timeoutMode is unset")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for bulkWrite operations",
+ "timeoutMS applied to entire bulkWrite, not individual commands");
+
+ // getMore-refresh "success" tests (timeoutMS 200/250, expect no error): on Windows setup eats the budget so
+ // they time out unexpectedly. Raise timeoutMS to a value-agnostic 1000ms floor (safe since they expect success).
+ def.transform("JAVA-6057: getMore-refresh success timeouts are too tight for the slower Windows TLS hosts, "
+ + "where connection setup consumes the budget before the command completes",
+ (entitiesArray, definition) ->
+ raiseIntToFloor(definition.getArray("operations"), "arguments.timeoutMS", 1000))
+ .when(ClusterFixture::isWindows)
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for change streams",
+ "timeoutMS is refreshed for getMore if maxAwaitTimeMS is set")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for non-tailable cursors",
+ "timeoutMS is refreshed for getMore if timeoutMode is iteration - success")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for tailable awaitData cursors",
+ "timeoutMS is refreshed for getMore if maxAwaitTimeMS is set")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for tailable awaitData cursors",
+ "timeoutMS is refreshed for getMore if maxAwaitTimeMS is not set")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for tailable non-awaitData cursors",
+ "timeoutMS is refreshed for getMore - success");
+
+ // Retryable tests use client timeoutMS=100 + minPoolSize=1; on Windows the background handshake exceeds 100ms
+ // so the pool never populates ("Error waiting for awaitMinPoolSizeMS"). Scale ×10 (whole-op block ordering kept).
+ def.transform("JAVA-6057: retryable CSOT tests cannot populate minPoolSize under the tight client-level "
+ + "timeoutMS on the slower Windows TLS hosts, where background connection establishment "
+ + "exceeds the timeout",
+ (entitiesArray, definition) -> scaleForWindows(entitiesArray, definition, 10))
+ .when(ClusterFixture::isWindows)
+ .file("client-side-operations-timeout", "timeoutMS behaves correctly for retryable operations");
+
+ def.transform("JAVA-5839: Bump blocking/timeout to avoid CI latency failures",
+ (entitiesArray, definition) -> {
+ // Two blocked finds (times:2); the 2nd must time out (2 events). On Windows 1000/600 keeps
+ // 2*600 > 1000 with setup headroom so the first find completes.
+ findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 75, isWindows() ? 1000 : 250);
+ findAndSetInt(definition.getArray("operations"),
+ "arguments.failPoint.data.blockTimeMS", 50, isWindows() ? 600 : 200);
+ })
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS download operations",
+ "timeoutMS applied to entire download, not individual parts");
+
+ // Single-command GridFS CSOT tests: on Windows the tight 75ms budget is spent on connection setup before the
+ // command is sent (expected:<1> but was:<0>). Bump timeoutMS/blockTimeMS so the timeout triggers on the command.
+ def.transform("JAVA-6057: GridFS CSOT timeouts are too tight for the slower Windows TLS hosts, where the "
+ + "timeout elapses during connection establishment before the command under test is sent",
+ (entitiesArray, definition) -> {
+ findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 75, 250);
+ findAndSetInt(definition.getArray("operations"),
+ "arguments.failPoint.data.blockTimeMS", 100, 400);
+ })
+ .when(ClusterFixture::isWindows)
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS download operations",
+ "timeoutMS applied to find to get files document")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS download operations",
+ "timeoutMS applied to find to get chunks")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS delete operations",
+ "timeoutMS applied to delete against the files collection")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS delete operations",
+ "timeoutMS applied to delete against the chunks collection")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for advanced GridFS API operations",
+ "timeoutMS applied to update during a rename")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for advanced GridFS API operations",
+ "timeoutMS applied to files collection drop")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for advanced GridFS API operations",
+ "timeoutMS applied to chunks collection drop")
+ .test("client-side-operations-timeout",
+ "timeoutMS behaves correctly for GridFS find operations",
+ "timeoutMS applied to find command");
+
// OpenTelemetry
def.skipNoncompliantReactive("withTransaction is not supported in the reactive driver unified test runner")
.file("open-telemetry/tests", "convenient transactions");
@@ -331,15 +425,6 @@ public static void applyCustomizations(final TestDef def) {
def.skipJira("https://jira.mongodb.org/browse/JAVA-5689")
.file("gridfs", "gridfs-deleteByName")
.file("gridfs", "gridfs-renameByName");
- def.transform("JAVA-5839: Bump blocking/timeout to avoid CI latency failures",
- (entitiesArray, definition) -> {
- findAndSetInt(entitiesArray, "client.uriOptions.timeoutMS", 75, 250);
- findAndSetInt(definition.getArray("operations"),
- "arguments.failPoint.data.blockTimeMS", 50, 200);
- })
- .test("client-side-operations-timeout",
- "timeoutMS behaves correctly for GridFS download operations",
- "timeoutMS applied to entire download, not individual parts");
// Skip all rawData based tests
def.skipJira("https://jira.mongodb.org/browse/JAVA-5830 rawData support only added to Go and Node")
@@ -554,6 +639,109 @@ public static void applyCustomizations(final TestDef def) {
*/
static void findAndSetInt(final BsonArray array, final String path,
final int expectedValue, final int newValue) {
+ findAndSetInt(array, path, expectedValue, newValue, true);
+ }
+
+ /**
+ * Raises the numeric leaf at {@code path} to {@code floor} wherever it is below it (leaving larger values
+ * untouched). Unlike {@link #findAndSetInt} it does not assert the current value, so one transform can cover
+ * sibling tests using different (but similarly tight) values. Fails if the path is never found.
+ */
+ static void raiseIntToFloor(final BsonArray array, final String path, final int floor) {
+ String[] segments = path.split("\\.");
+ int matches = 0;
+ for (BsonValue element : array) {
+ if (!element.isDocument()) {
+ continue;
+ }
+ BsonDocument current = element.asDocument();
+ boolean found = true;
+ for (int i = 0; i < segments.length - 1; i++) {
+ if (current.containsKey(segments[i]) && current.get(segments[i]).isDocument()) {
+ current = current.getDocument(segments[i]);
+ } else {
+ found = false;
+ break;
+ }
+ }
+ String leafKey = segments[segments.length - 1];
+ if (found && current.containsKey(leafKey) && current.get(leafKey).isNumber()) {
+ matches++;
+ int oldValue = current.get(leafKey).asNumber().intValue();
+ if (oldValue < floor) {
+ LOGGER.info(format(" %s: %d -> %d", path, oldValue, floor));
+ current.put(leafKey, new BsonInt32(floor));
+ }
+ }
+ }
+ if (matches == 0) {
+ throw new AssertionFailedError(format(
+ "raiseIntToFloor: no value found at path '%s'. Spec may have changed or path is incorrect.",
+ path));
+ }
+ }
+
+ /**
+ * Unified analogue of {@link ClusterFixture#scaleForWindows(long)}: multiplies a test's CSOT knobs by
+ * {@code factor} — {@code client.uriOptions.timeoutMS} and the operations' {@code arguments.timeoutMS} /
+ * {@code arguments.failPoint.data.blockTimeMS} — preserving their ordering so a test that expects a specific
+ * command to time out still does. Use for timeout-expecting tests; success-only tests use {@link #raiseIntToFloor}.
+ * Fails if nothing was scaled, to catch spec drift.
+ */
+ static void scaleForWindows(final BsonArray entitiesArray, final BsonDocument definition, final int factor) {
+ int changes = 0;
+ changes += multiplyInt(entitiesArray, "client.uriOptions.timeoutMS", factor);
+ BsonArray operations = definition.getArray("operations");
+ changes += multiplyInt(operations, "arguments.timeoutMS", factor);
+ changes += multiplyInt(operations, "arguments.failPoint.data.blockTimeMS", factor);
+ if (changes == 0) {
+ throw new AssertionFailedError(
+ "scaleForWindows: no timeoutMS/blockTimeMS found to scale. Spec may have changed.");
+ }
+ }
+
+ /**
+ * Walks {@code array} for the numeric leaf at the dot-separated {@code path} and multiplies each occurrence by
+ * {@code factor} (logging each change). Tolerates the path being absent. Returns the number of values changed.
+ */
+ private static int multiplyInt(final BsonArray array, final String path, final int factor) {
+ String[] segments = path.split("\\.");
+ int count = 0;
+ for (BsonValue element : array) {
+ if (!element.isDocument()) {
+ continue;
+ }
+ BsonDocument current = element.asDocument();
+ boolean found = true;
+ for (int i = 0; i < segments.length - 1; i++) {
+ if (current.containsKey(segments[i]) && current.get(segments[i]).isDocument()) {
+ current = current.getDocument(segments[i]);
+ } else {
+ found = false;
+ break;
+ }
+ }
+ String leafKey = segments[segments.length - 1];
+ if (found && current.containsKey(leafKey) && current.get(leafKey).isNumber()) {
+ int oldValue = current.get(leafKey).asNumber().intValue();
+ int newValue = oldValue * factor;
+ LOGGER.info(format(" %s: %d -> %d", path, oldValue, newValue));
+ current.put(leafKey, new BsonInt32(newValue));
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Variant of {@link #findAndSetInt(BsonArray, String, int, int)} that, when {@code required} is
+ * {@code false}, tolerates the path being absent (no replacement made) instead of failing. Useful when a
+ * single transform is applied across a file whose tests do not all contain the field being adjusted.
+ *
+ * @param required whether at least one replacement must be made
+ */
+ static void findAndSetInt(final BsonArray array, final String path,
+ final int expectedValue, final int newValue, final boolean required) {
String[] segments = path.split("\\.");
int replacements = 0;
for (BsonValue element : array) {
@@ -584,7 +772,7 @@ static void findAndSetInt(final BsonArray array, final String path,
replacements++;
}
}
- if (replacements == 0) {
+ if (replacements == 0 && required) {
throw new AssertionFailedError(format(
"findAndSetInt: no value found at path '%s'. Spec may have changed or path is incorrect.",
path));
diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts
index 208034beaad..5702a9c12d7 100644
--- a/mongodb-crypt/build.gradle.kts
+++ b/mongodb-crypt/build.gradle.kts
@@ -168,10 +168,51 @@ abstract class VerifyLibmongocryptTask : DefaultTask() {
return
}
+ // The gpg shipped on the Evergreen Windows hosts is a Cygwin build that only understands
+ // POSIX paths.
+ // Handed a native Windows path ("C:\dir") it treats the backslash path as relative,
+ // prepends its working
+ // directory, and fails with "no writable keyring found". Translate drive-letter paths to
+ // the Cygwin form
+ // ("C:\dir" -> "/cygdrive/c/dir") on Windows so every path argument (homedir, key,
+ // signatures, tarballs)
+ // is parsed correctly; other platforms pass paths through unchanged.
+ val isWindows = System.getProperty("os.name").startsWith("Windows", ignoreCase = true)
+ fun toGpgPath(path: String): String {
+ if (!isWindows) {
+ return path
+ }
+ val driveLetter = Regex("^([A-Za-z]):[\\\\/](.*)$").matchEntire(path) ?: return path.replace('\\', '/')
+ val (drive, rest) = driveLetter.destructured
+ return "/cygdrive/${drive.lowercase()}/${rest.replace('\\', '/')}"
+ }
+
+ // Run gpg capturing both streams; on non-zero exit throw with the captured output appended
+ // so the
+ // underlying gpg diagnostic is visible instead of Gradle's opaque "finished with non-zero
+ // exit value N".
+ fun runGpg(vararg args: String, onFailure: (String) -> String) {
+ val out = ByteArrayOutputStream()
+ val err = ByteArrayOutputStream()
+ val result =
+ execOps.exec {
+ commandLine(listOf("gpg") + args)
+ standardOutput = out
+ errorOutput = err
+ isIgnoreExitValue = true
+ }
+ val combined = (out.toString().trim() + "\n" + err.toString().trim()).trim()
+ logger.info("gpg ${args.joinToString(" ")} -> exit ${result.exitValue}\n$combined")
+ if (result.exitValue != 0) {
+ throw GradleException("${onFailure(combined)}\ngpg command: gpg ${args.joinToString(" ")}")
+ }
+ }
+
+ val versionOut = ByteArrayOutputStream()
try {
execOps.exec {
commandLine("gpg", "--version")
- standardOutput = ByteArrayOutputStream()
+ standardOutput = versionOut
}
} catch (e: Exception) {
throw GradleException(
@@ -180,6 +221,7 @@ abstract class VerifyLibmongocryptTask : DefaultTask() {
"or pass -PskipCryptVerify=true for offline development builds.",
e)
}
+ logger.lifecycle("Using gpg:\n${versionOut.toString().trim().lineSequence().firstOrNull() ?: ""}")
val home =
gnupgHome.get().asFile.apply {
@@ -193,40 +235,27 @@ abstract class VerifyLibmongocryptTask : DefaultTask() {
setExecutable(false, false)
setExecutable(true, true)
}
+ val homedir = toGpgPath(home.path)
+ logger.lifecycle(
+ "libmongocrypt verify: gnupgHome=${home.path} -> $homedir (exists=${home.exists()}, " +
+ "writable=${home.canWrite()}), publicKey=${publicKey.get().asFile.path} " +
+ "(exists=${publicKey.get().asFile.exists()})")
- execOps.exec {
- commandLine(
- "gpg",
- "--homedir",
- home.path,
- "--batch",
- "--quiet",
- "--no-autostart",
- "--import",
- publicKey.get().asFile.path)
- standardOutput = ByteArrayOutputStream()
- errorOutput = ByteArrayOutputStream()
+ runGpg("--homedir", homedir, "--batch", "--no-autostart", "--import", toGpgPath(publicKey.get().asFile.path)) {
+ output ->
+ "Failed to import libmongocrypt signing key into scratch keyring at ${home.path}.\n$output"
}
- try {
- execOps.exec {
- commandLine(
- "gpg",
- "--homedir",
- home.path,
- "--batch",
- "--no-autostart",
- "--with-colons",
- "--fingerprint",
- expectedFingerprint.get())
- standardOutput = ByteArrayOutputStream()
- errorOutput = ByteArrayOutputStream()
- }
- } catch (e: Exception) {
- throw GradleException(
- "Imported libmongocrypt signing key fingerprint does not match expected value " +
- "${expectedFingerprint.get()}. The downloaded public key may have been rotated.",
- e)
+ runGpg(
+ "--homedir",
+ homedir,
+ "--batch",
+ "--no-autostart",
+ "--with-colons",
+ "--fingerprint",
+ expectedFingerprint.get()) { output ->
+ "Imported libmongocrypt signing key fingerprint does not match expected value " +
+ "${expectedFingerprint.get()}. The downloaded public key may have been rotated.\n$output"
}
// Pair tarballs with signatures by basename; ConfigurableFileCollection.files is an
@@ -238,28 +267,16 @@ abstract class VerifyLibmongocryptTask : DefaultTask() {
signaturesByName[signatureName]
?: throw GradleException(
"Missing signature $signatureName for ${tarball.name}; expected it next to the tarball.")
- val verifyErr = ByteArrayOutputStream()
- try {
- execOps.exec {
- commandLine(
- "gpg",
- "--homedir",
- home.path,
- "--batch",
- "--quiet",
- "--no-autostart",
- "--trust-model",
- "always",
- "--verify",
- signature.path,
- tarball.path)
- standardOutput = ByteArrayOutputStream()
- errorOutput = verifyErr
- }
- } catch (e: Exception) {
- throw GradleException(
- "GPG signature verification failed for ${tarball.name}:\n${verifyErr.toString().trim()}", e)
- }
+ runGpg(
+ "--homedir",
+ homedir,
+ "--batch",
+ "--no-autostart",
+ "--trust-model",
+ "always",
+ "--verify",
+ toGpgPath(signature.path),
+ toGpgPath(tarball.path)) { output -> "GPG signature verification failed for ${tarball.name}:\n$output" }
}
verificationStamp