From d42d8e1df647f509210aabfc0ae67e56f8190f01 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 09:40:38 +0100 Subject: [PATCH 1/6] JAVA-6057: fix verifyCryptLibs gpg path handling on Windows The gpg on the Evergreen Windows hosts is a Cygwin build that only understands POSIX paths. Handed native Windows paths (C:\dir) it treats them as relative, mangles the keyring location, and fails to find a writable keyring. Translate drive-letter paths to the Cygwin form (C:\dir -> /cygdrive/c/dir) on Windows for every gpg path argument (homedir, public key, signatures, tarballs); other platforms are unchanged. Also surface gpg stdout/stderr and the gnupgHome/publicKey state on failure so future gpg issues are diagnosable instead of showing only Gradle's opaque non-zero exit value. --- mongodb-crypt/build.gradle.kts | 125 +++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 208034beaa..5702a9c12d 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 From 41f2f1e513078ce234584514f9e5cefc680e5764 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 09:43:47 +0100 Subject: [PATCH 2/6] Fix server version checking for nested MQL asString. --- .../client/model/mql/TypeMqlValuesFunctionalTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 228dc3ede7..16c0c2f856 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()); From 3be153f9dd9ed8fac2dae7816b6fcdd80f702ab2 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 10:01:20 +0100 Subject: [PATCH 3/6] Set a default OS in setup-env.bash Should fix publishing of snapshots. JAVA-6057 --- .evergreen/setup-env.bash | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.evergreen/setup-env.bash b/.evergreen/setup-env.bash index a8a7840292..c8a96b0300 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" From 470aee3e5886c2d22e8e67f90ddf489c4cd1eb9b Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 9 Jul 2026 10:21:40 +0100 Subject: [PATCH 4/6] Ignore drop errors in the shutdown hook. Brings it inline with the reactive streams shutdown hook. Ensures tests don't fail on shutdown with a DatabaseDropPending exception --- .../src/test/functional/com/mongodb/client/Fixture.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 8114d62e41..8e1227803b 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; From b44c965830fdce4e8b972bf1887ea2575b0bbaab Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 22 Jul 2026 17:19:22 +0100 Subject: [PATCH 5/6] JAVA-6057: scale CSOT test timeouts for the slower Windows TLS hosts CSOT prose and unified tests are calibrated for fast Linux CI hosts; on Windows + TLS a fresh client's connection establishment consumes the tight timeout budgets, so a command under test times out during setup (or the wrong command times out). Add ClusterFixture.isWindows()/scaleForWindows() plus a unified scaleForWindows(entities, definition, factor) transform, and apply Windows-only timeout scaling (and a value-agnostic floor for the success-family getMore-refresh tests) to the affected CSOT tests. The non-Windows RTT retries are retained for other TLS runs. All Windows CSOT adjustments are grouped at the end of the CSOT section. --- .../com/mongodb/ClusterFixture.java | 16 ++ ...tClientSideOperationsTimeoutProseTest.java | 244 +++++++++++++----- ...eOperationsEncryptionTimeoutProseTest.java | 31 ++- .../unified/UnifiedTestModifications.java | 242 +++++++++++++++-- 4 files changed, 429 insertions(+), 104 deletions(-) diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java index 80c09a5cf0..868dfac452 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-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java index 7828ecde68..63bacfc759 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/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java index 04303833bf..f0e74b5c70 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 e90941b6c0..76371e561c 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)); From b75082f2f281f5f36712466c0e60c6dc625bc8bd Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 22 Jul 2026 17:24:37 +0100 Subject: [PATCH 6/6] JAVA-6254: translate a write-path SocketTimeoutException to MongoOperationTimeoutException With timeoutMS set, socket timeouts should surface as MongoOperationTimeoutException. The read path translates a bare java.net.SocketTimeoutException, but the write path only handled MongoSocketWriteTimeoutException, so a bare SocketTimeoutException during a send fell through to MongoSocketWriteException. On Windows + TLS a blocking SSLSocket write surfaces the socket read-timeout as a bare SocketTimeoutException, reproducing the inconsistency. In throwTranslatedWriteException, when timeoutMS is set, wrap a bare SocketTimeoutException in a MongoSocketWriteTimeoutException and then MongoOperationTimeoutException, mirroring translateReadException. Confined to the timeoutMS path. --- .../connection/InternalStreamConnection.java | 14 +++++++++++-- ...ternalStreamConnectionSpecification.groovy | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) 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 99694a9649..27de484063 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/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy index 3cdabf31da..c9d0525ad1 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) }