From ce748bb747f4e7116e565143694ddf6c3226f9e1 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 11 Dec 2025 15:35:23 +0000 Subject: [PATCH] Fixes JAVA-5949 prevent connection churn on backpressure errors when establishing connections --- .../src/main/com/mongodb/MongoException.java | 16 ++++++ .../DefaultSdamServerDescriptionManager.java | 26 ++++++++-- .../InternalStreamConnectionInitializer.java | 2 + .../SdamServerDescriptionManager.java | 51 +++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/driver-core/src/main/com/mongodb/MongoException.java b/driver-core/src/main/com/mongodb/MongoException.java index a668dd344b7..62610a62feb 100644 --- a/driver-core/src/main/com/mongodb/MongoException.java +++ b/driver-core/src/main/com/mongodb/MongoException.java @@ -50,6 +50,22 @@ public class MongoException extends RuntimeException { */ public static final String UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL = "UnknownTransactionCommitResult"; + /** + * An error label indicating that the server is overloaded. + * + * @see #hasErrorLabel(String) + * @since 5.7 + */ + public static final String SYSTEM_OVERLOADED_ERROR_LABEL = "SystemOverloadedError"; + + /** + * An error label indicating that the operation is safely retryable. + * + * @see #hasErrorLabel(String) + * @since 5.7 + */ + public static final String RETRYABLE_ERROR_LABEL = "RetryableError"; + private static final long serialVersionUID = -4415279469780082174L; private final int code; diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java index af4acd8c031..b6154a00cf2 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java @@ -16,6 +16,7 @@ package com.mongodb.internal.connection; +import com.mongodb.MongoException; import com.mongodb.annotations.ThreadSafe; import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ServerDescription; @@ -137,9 +138,28 @@ private void handleException(final SdamIssue sdamIssue, final boolean beforeHand serverMonitor.connect(); } else if (sdamIssue.relatedToNetworkNotTimeout() || (beforeHandshake && (sdamIssue.relatedToNetworkTimeout() || sdamIssue.relatedToAuth()))) { - updateDescription(sdamIssue.serverDescription()); - connectionPool.invalidate(sdamIssue.exception().orElse(null)); - serverMonitor.cancelCurrentCheck(); + // Backpressure spec: Don't clear pool or mark server unknown for connection establishment failures + // (network errors or timeouts during handshake). Authentication errors after handshake should still + // clear the pool as they're not related to overload. + // TLS configuration errors (certificate validation, protocol mismatches) should also clear the pool + // as they indicate configuration issues, not server overload. + if (beforeHandshake && (sdamIssue.relatedToNetworkNotTimeout() || sdamIssue.relatedToNetworkTimeout()) + && !sdamIssue.relatedToAuth() && !sdamIssue.relatedToTlsConfigurationError()) { + // Don't update server description to Unknown + // Don't invalidate the connection pool + // Apply error labels for backpressure + sdamIssue.exception().ifPresent(exception -> { + if (exception instanceof MongoException) { + MongoException mongoException = (MongoException) exception; + mongoException.addLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL); + mongoException.addLabel(MongoException.RETRYABLE_ERROR_LABEL); + } + }); + } else { + updateDescription(sdamIssue.serverDescription()); + connectionPool.invalidate(sdamIssue.exception().orElse(null)); + serverMonitor.cancelCurrentCheck(); + } } else if (sdamIssue.relatedToWriteConcern() || sdamIssue.relatedToStalePrimary()) { updateDescription(sdamIssue.serverDescription()); serverMonitor.connect(); diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java index 574a85669d0..6463a101dfd 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java +++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java @@ -198,6 +198,8 @@ private BsonDocument createHelloCommand(final Authenticator authenticator, final helloCommandDocument.append("speculativeAuthenticate", speculativeAuthenticateDocument); } } + // Add backpressure support indication + helloCommandDocument.append("backpressure", BsonBoolean.TRUE); return helloCommandDocument; } diff --git a/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java b/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java index 7f014d7ede6..a2135085535 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java +++ b/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java @@ -30,6 +30,10 @@ import com.mongodb.connection.TopologyVersion; import com.mongodb.lang.Nullable; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; import java.util.Optional; import static com.mongodb.assertions.Assertions.assertNotNull; @@ -162,6 +166,53 @@ boolean relatedToWriteConcern() { return exception instanceof MongoWriteConcernWithResponseException; } + /** + * Checks if the exception is related to TLS configuration errors that are NOT due to server overload. + * These include certificate validation failures, protocol mismatches, etc. + * + * @return true if this is a TLS configuration error (not network-related) + */ + boolean relatedToTlsConfigurationError() { + if (!(exception instanceof MongoSocketException)) { + return false; + } + Throwable cause = exception.getCause(); + while (cause != null) { + // Check for various certificate validation and TLS configuration errors + if (cause instanceof CertificateException + || cause instanceof CertPathValidatorException + || cause instanceof SSLPeerUnverifiedException) { + return true; // Certificate/peer validation failure + } + + // Check for SunCertPathBuilderException by class name to avoid compile-time dependency on internal classes + String className = cause.getClass().getName(); + if (className.equals("sun.security.provider.certpath.SunCertPathBuilderException")) { + return true; // Certificate path building failure + } + + // SSLHandshakeException can be either network or config, so we check the message + if (cause instanceof SSLHandshakeException) { + String message = cause.getMessage(); + if (message != null) { + String lowerMessage = message.toLowerCase(); + // These indicate configuration issues, not network issues + if (lowerMessage.contains("certificate") + || lowerMessage.contains("verify") + || lowerMessage.contains("trust") + || lowerMessage.contains("hostname") + || lowerMessage.contains("protocol") + || lowerMessage.contains("cipher") + || lowerMessage.contains("handshake_failure")) { + return true; + } + } + } + cause = cause.getCause(); + } + return false; + } + private static boolean stale(@Nullable final Throwable t, final ServerDescription currentServerDescription) { return TopologyVersionHelper.topologyVersion(t) .map(candidateTopologyVersion -> TopologyVersionHelper.newerOrEqual(