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 0cad654a73a..fb815d6a4a4 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java @@ -785,7 +785,7 @@ public void sendMessage(final List byteBuffers, final int lastRequestId stream.write(byteBuffers, operationContext); } catch (Exception e) { close(); - throwTranslatedWriteException(e, operationContext); + throw translateWriteException(e, operationContext); } } @@ -804,12 +804,8 @@ public void sendMessageAsync( }).thenRunTryCatchAsyncBlocks(c -> { stream.writeAsync(byteBuffers, operationContext, c.asHandler()); }, Exception.class, (e, c) -> { - try { - close(); - throwTranslatedWriteException(e, operationContext); - } catch (Throwable translatedException) { - c.completeExceptionally(translatedException); - } + close(); + c.completeExceptionally(translateWriteException(e, operationContext)); }).finish(errorHandlingCallback(callback, LOGGER)); } @@ -888,24 +884,34 @@ private void updateSessionContext(final SessionContext sessionContext, final Res } } - private void throwTranslatedWriteException(final Throwable e, final OperationContext operationContext) { - if (e instanceof MongoSocketWriteTimeoutException && operationContext.getTimeoutContext().hasTimeoutMS()) { - throw createMongoTimeoutException(e); + private MongoException translateWriteException(final Throwable e, final OperationContext operationContext) { + if (operationContext.getTimeoutContext().hasTimeoutMS()) { + if (e instanceof MongoSocketWriteTimeoutException) { + return 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. + return createMongoTimeoutException(createWriteTimeoutException((SocketTimeoutException) e)); + } } if (e instanceof MongoException) { - throw (MongoException) e; + return (MongoException) e; } Optional interruptedException = translateInterruptedException(e, "Interrupted while sending message"); if (interruptedException.isPresent()) { - throw interruptedException.get(); + return interruptedException.get(); } else if (e instanceof IOException) { - throw new MongoSocketWriteException("Exception sending message", getServerAddress(), e); + return new MongoSocketWriteException("Exception sending message", getServerAddress(), e); } else { - throw new MongoInternalException("Unexpected exception", e); + return new MongoInternalException("Unexpected exception", e); } } + 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..5ee655f8e3e 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 long scaleForWindows(final long 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..62fef59b116 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; @@ -223,12 +224,12 @@ public void testBlockingIterationMethodsTailableCursor() { + " data: {" + " failCommands: [\"getMore\"]," + " blockConnection: true," - + " blockTimeMS: " + 150 + + " blockTimeMS: " + scaleForWindows(150) + " }" + "}"); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() - .timeout(250, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(250), TimeUnit.MILLISECONDS))) { MongoCollection collection = client.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); @@ -262,12 +263,12 @@ public void testBlockingIterationMethodsChangeStream() { + " data: {" + " failCommands: [\"getMore\"]," + " blockConnection: true," - + " blockTimeMS: " + 150 + + " blockTimeMS: " + scaleForWindows(150) + " }" + "}"); try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder() - .timeout(250, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(250), TimeUnit.MILLISECONDS))) { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()).withReadPreference(ReadPreference.primary()); @@ -301,7 +302,7 @@ public void testGridFSUploadViaOpenUploadStreamTimeout() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 205 + + " blockTimeMS: " + scaleForWindows(205) + " }" + "}"); @@ -309,7 +310,7 @@ public void testGridFSUploadViaOpenUploadStreamTimeout() { filesCollectionHelper.create(); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() - .timeout(200, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(200), TimeUnit.MILLISECONDS))) { MongoDatabase database = client.getDatabase(namespace.getDatabaseName()); GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME); @@ -331,7 +332,7 @@ public void testAbortingGridFsUploadStreamTimeout() throws Throwable { + " data: {" + " failCommands: [\"delete\"]," + " blockConnection: true," - + " blockTimeMS: " + 320 + + " blockTimeMS: " + scaleForWindows(320) + " }" + "}"); @@ -339,7 +340,7 @@ public void testAbortingGridFsUploadStreamTimeout() throws Throwable { filesCollectionHelper.create(); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() - .timeout(300, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(300), TimeUnit.MILLISECONDS))) { MongoDatabase database = client.getDatabase(namespace.getDatabaseName()); GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME).withChunkSizeBytes(2); @@ -382,12 +383,12 @@ public void testGridFsDownloadStreamTimeout() { + " data: {" + " failCommands: [\"find\"]," + " blockConnection: true," - + " blockTimeMS: " + 500 + + " blockTimeMS: " + scaleForWindows(500) + " }" + "}"); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() - .timeout(300, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(300), TimeUnit.MILLISECONDS))) { MongoDatabase database = client.getDatabase(namespace.getDatabaseName()); GridFSBucket gridFsBucket = createGridFsBucket(database, GRID_FS_BUCKET_NAME).withChunkSizeBytes(2); @@ -551,7 +552,7 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithCommit() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 25 + + " blockTimeMS: " + scaleForWindows(25) + " }" + "}"); @@ -559,7 +560,7 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithCommit() { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); - int defaultTimeout = 300; + int defaultTimeout = (int) scaleForWindows(300); try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder() .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) { session.startTransaction(); @@ -583,7 +584,7 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithAbort() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 25 + + " blockTimeMS: " + scaleForWindows(25) + " }" + "}"); @@ -591,7 +592,7 @@ public void test9EndSessionCustomTesEachOperationHasItsOwnTimeoutWithAbort() { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); - int defaultTimeout = 300; + int defaultTimeout = (int) scaleForWindows(300); try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder() .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) { session.startTransaction(); @@ -616,12 +617,12 @@ public void test10ConvenientTransactions() { + " data: {" + " failCommands: [\"insert\", \"abortTransaction\"]," + " blockConnection: true," - + " blockTimeMS: " + 200 + + " blockTimeMS: " + scaleForWindows(200) + " }" + "}"); try (MongoClient mongoClient = createMongoClient(getMongoClientSettingsBuilder() - .timeout(150, TimeUnit.MILLISECONDS))) { + .timeout(scaleForWindows(150), TimeUnit.MILLISECONDS))) { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); @@ -651,7 +652,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 25 + + " blockTimeMS: " + scaleForWindows(25) + " }" + "}"); @@ -659,7 +660,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); - int defaultTimeout = 200; + int defaultTimeout = (int) scaleForWindows(200); try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder() .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) { assertThrows(MongoOperationTimeoutException.class, @@ -685,7 +686,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 25 + + " blockTimeMS: " + scaleForWindows(25) + " errorCode: " + 24 + " errorLabels: [\"TransientTransactionError\"]" + " }" @@ -695,7 +696,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() { MongoCollection collection = mongoClient.getDatabase(namespace.getDatabaseName()) .getCollection(namespace.getCollectionName()); - int defaultTimeout = 200; + int defaultTimeout = (int) scaleForWindows(200); try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder() .defaultTimeout(defaultTimeout, TimeUnit.MILLISECONDS).build())) { assertThrows(MongoOperationTimeoutException.class, @@ -999,7 +1000,7 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() { .getCollection(namespace.getCollectionName()); try (ClientSession session = mongoClient.startSession(ClientSessionOptions.builder() - .defaultTimeout(200, TimeUnit.MILLISECONDS) + .defaultTimeout(scaleForWindows(200), TimeUnit.MILLISECONDS) .build())) { session.startTransaction(TransactionOptions.builder().build()); collection.insertOne(session, new Document("x", 1)); @@ -1013,7 +1014,7 @@ public void shouldThrowTimeoutExceptionForSubsequentCommitTransaction() { + " data: {" + " failCommands: [\"commitTransaction\"]," + " blockConnection: true," - + " blockTimeMS: " + 500 + + " blockTimeMS: " + scaleForWindows(500) + " }" + "}"); 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..fb6c649d4e7 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; @@ -99,7 +100,7 @@ void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() { localProviderMap.put("key", Base64.getDecoder().decode(MASTER_KEY)); kmsProviders.put("local", localProviderMap); - try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(100))) { + try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(scaleForWindows(100)))) { keyVaultCollectionHelper.runAdminCommand("{" + " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\"," @@ -107,7 +108,7 @@ void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() { + " data: {" + " failCommands: [\"insert\"]," + " blockConnection: true," - + " blockTimeMS: " + 100 + + " blockTimeMS: " + scaleForWindows(100) + " }" + "}"); @@ -126,7 +127,7 @@ void shouldThrowOperationTimeoutExceptionWhenCreateDataKey() { void shouldThrowOperationTimeoutExceptionWhenEncryptData() { assumeTrue(serverVersionAtLeast(4, 4)); - try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(150))) { + try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(scaleForWindows(150)))) { clientEncryption.createDataKey("local"); @@ -136,7 +137,7 @@ void shouldThrowOperationTimeoutExceptionWhenEncryptData() { + " data: {" + " failCommands: [\"find\"]," + " blockConnection: true," - + " blockTimeMS: " + 150 + + " blockTimeMS: " + scaleForWindows(150) + " }" + "}"); @@ -160,7 +161,7 @@ void shouldThrowOperationTimeoutExceptionWhenDecryptData() { assumeTrue(serverVersionAtLeast(4, 4)); BsonBinary encrypted; - try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(400))) { + try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(scaleForWindows(400)))) { clientEncryption.createDataKey("local"); BsonBinary dataKey = clientEncryption.createDataKey("local"); EncryptOptions encryptOptions = new EncryptOptions("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"); @@ -168,14 +169,14 @@ void shouldThrowOperationTimeoutExceptionWhenDecryptData() { encrypted = clientEncryption.encrypt(new BsonString("hello"), encryptOptions); } - try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(400))) { + try (ClientEncryption clientEncryption = createClientEncryption(getClientEncryptionSettingsBuilder(scaleForWindows(400)))) { keyVaultCollectionHelper.runAdminCommand("{" + " configureFailPoint: \"" + FAIL_COMMAND_NAME + "\"," + " mode: { times: 1 }," + " data: {" + " failCommands: [\"find\"]," + " blockConnection: true," - + " blockTimeMS: " + 500 + + " blockTimeMS: " + scaleForWindows(500) + " }" + "}"); commandListener.reset(); @@ -268,7 +269,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