Skip to content

Commit 11a7b73

Browse files
nhachichastIncMale
authored andcommitted
JAVA-5949 preserve connection pool on backpressure errors when establishing connections (#1900)
1 parent 7cbf5e7 commit 11a7b73

12 files changed

Lines changed: 584 additions & 15 deletions
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.internal.connection;
18+
19+
import com.mongodb.MongoException;
20+
import com.mongodb.MongoSocketException;
21+
22+
import javax.net.ssl.SSLHandshakeException;
23+
import javax.net.ssl.SSLPeerUnverifiedException;
24+
import javax.net.ssl.SSLProtocolException;
25+
import java.net.UnknownHostException;
26+
import java.security.cert.CertPathBuilderException;
27+
import java.security.cert.CertPathValidatorException;
28+
import java.security.cert.CertificateException;
29+
import java.util.Arrays;
30+
import java.util.Collections;
31+
import java.util.HashSet;
32+
import java.util.Locale;
33+
import java.util.Set;
34+
35+
/**
36+
* Attaches {@link MongoException#SYSTEM_OVERLOADED_ERROR_LABEL} and
37+
* {@link MongoException#RETRYABLE_ERROR_LABEL} to network errors encountered during connection
38+
* establishment or the hello message, per the CMAP specification.
39+
*
40+
* <p>This is topology-agnostic: it must be invoked from the connection-establishment path so that
41+
* both default SDAM and load-balanced modes are covered.
42+
*/
43+
final class BackpressureErrorLabeler {
44+
45+
/**
46+
* BouncyCastle TLS fatal-alert exception type names.
47+
*/
48+
private static final Set<String> BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES = Collections.unmodifiableSet(
49+
new HashSet<>(Arrays.asList(
50+
"org.bouncycastle.tls.TlsFatalAlert",
51+
"org.bouncycastle.tls.TlsFatalAlertReceived",
52+
"org.bouncycastle.tls.crypto.TlsCryptoException")));
53+
54+
/**
55+
* RFC 5246 / RFC 8446 alert descriptions that surface in BouncyCastle TLS exception messages.
56+
* See <a href="https://github.com/bcgit/bc-java/blob/main/tls/src/main/java/org/bouncycastle/tls/AlertDescription.java">AlertDescription.java</a>.
57+
* See <a href="https://datatracker.ietf.org/doc/html/rfc5246#section-7.2">(TLS) Protocol Version 1.2 - Alert Protocol</a>.
58+
*/
59+
private static final Set<String> BOUNCY_CASTLE_TLS_ALERT_DESCRIPTIONS = Collections.unmodifiableSet(
60+
new HashSet<>(Arrays.asList(
61+
"close_notify", "unexpected_message", "bad_record_mac", "decryption_failed",
62+
"record_overflow", "decompression_failure", "handshake_failure", "no_certificate",
63+
"bad_certificate", "unsupported_certificate", "certificate_revoked", "certificate_expired",
64+
"certificate_unknown", "illegal_parameter", "unknown_ca", "access_denied",
65+
"decode_error", "decrypt_error", "export_restriction", "protocol_version",
66+
"insufficient_security", "internal_error", "no_renegotiation", "unsupported_extension",
67+
"certificate_unobtainable", "unrecognized_name", "bad_certificate_status_response",
68+
"bad_certificate_hash_value", "unknown_psk_identity", "no_application_protocol",
69+
"inappropriate_fallback", "missing_extension", "certificate_required")));
70+
71+
private BackpressureErrorLabeler() {
72+
}
73+
74+
static void applyLabelsIfEligible(final Throwable t) {
75+
if (!(t instanceof MongoSocketException)) {
76+
return;
77+
}
78+
MongoSocketException socketException = (MongoSocketException) t;
79+
if (isDnsLookupFailure(socketException)) {
80+
return;
81+
}
82+
if (isTlsConfigurationError(socketException)) {
83+
return;
84+
}
85+
// TODO-BACKPRESSURE Nabil - Add SOCKS5 check once JAVA-6194 is introduced
86+
// async proxy error surfaces can be handled together — likely via a dedicated internal
87+
// exception thrown from the proxy code path.
88+
socketException.addLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
89+
socketException.addLabel(MongoException.RETRYABLE_ERROR_LABEL);
90+
}
91+
92+
private static boolean isDnsLookupFailure(final MongoSocketException t) {
93+
Throwable cause = t.getCause();
94+
while (cause != null) {
95+
if (cause instanceof UnknownHostException) {
96+
return true;
97+
}
98+
cause = cause.getCause();
99+
}
100+
return false;
101+
}
102+
103+
private static boolean isTlsConfigurationError(final MongoSocketException t) {
104+
Throwable cause = t.getCause();
105+
while (cause != null) {
106+
if (cause instanceof CertificateException
107+
|| cause instanceof CertPathBuilderException
108+
|| cause instanceof CertPathValidatorException
109+
|| cause instanceof SSLPeerUnverifiedException
110+
|| cause instanceof SSLProtocolException) {
111+
return true;
112+
}
113+
if (cause instanceof SSLHandshakeException) {
114+
String message = cause.getMessage();
115+
if (message != null) {
116+
String lowerMessage = message.toLowerCase(Locale.ROOT);
117+
if (lowerMessage.contains("verify")
118+
|| lowerMessage.contains("protocol")
119+
|| lowerMessage.contains("cipher")
120+
|| lowerMessage.contains("received fatal alert")) {
121+
return true;
122+
}
123+
}
124+
}
125+
if (isBouncyCastleTlsError(cause)) {
126+
return true;
127+
}
128+
129+
cause = cause.getCause();
130+
}
131+
return false;
132+
}
133+
134+
private static boolean isBouncyCastleTlsError(final Throwable cause) {
135+
if (!isBouncyCastleTlsFatalType(cause.getClass())) {
136+
return false;
137+
}
138+
String message = cause.getMessage();
139+
if (message == null) {
140+
return false;
141+
}
142+
String description = message.toLowerCase(Locale.ROOT);
143+
for (String alertName : BOUNCY_CASTLE_TLS_ALERT_DESCRIPTIONS) {
144+
if (description.contains(alertName)) {
145+
return true;
146+
}
147+
}
148+
return false;
149+
}
150+
151+
/**
152+
* Walks the class hierarchy comparing fully qualified names so a subclass of a known BC type
153+
* still matches.
154+
*/
155+
private static boolean isBouncyCastleTlsFatalType(final Class<?> exceptionClass) {
156+
Class<?> cls = exceptionClass;
157+
while (cls != null) {
158+
if (BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES.contains(cls.getName())) {
159+
return true;
160+
}
161+
cls = cls.getSuperclass();
162+
}
163+
return false;
164+
}
165+
}

driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ private void handleException(final SdamIssue sdamIssue, final boolean beforeHand
137137
serverMonitor.connect();
138138
} else if (sdamIssue.relatedToNetworkNotTimeout()
139139
|| (beforeHandshake && (sdamIssue.relatedToNetworkTimeout() || sdamIssue.relatedToAuth()))) {
140+
if (sdamIssue.hasSystemOverloadedLabel()) {
141+
return;
142+
}
140143
updateDescription(sdamIssue.serverDescription());
141144
connectionPool.invalidate(sdamIssue.exception().orElse(null));
142145
serverMonitor.cancelCurrentCheck();

driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,11 @@ public void open(final OperationContext originalOperationContext) {
229229
isTrue("Open already called", stream == null);
230230
stream = streamFactory.create(serverId.getAddress());
231231
OperationContext operationContext = originalOperationContext;
232+
boolean beforeHandshake = true;
232233
try {
233234
stream.open(operationContext);
234235
InternalConnectionInitializationDescription initializationDescription = connectionInitializer.startHandshake(this, operationContext);
236+
beforeHandshake = false;
235237

236238
operationContext = operationContext.withOverride(TimeoutContext::withNewlyStartedMaintenanceTimeout);
237239
initAfterHandshakeStart(initializationDescription);
@@ -240,6 +242,9 @@ public void open(final OperationContext originalOperationContext) {
240242
initAfterHandshakeFinish(initializationDescription);
241243
} catch (Throwable t) {
242244
close();
245+
if (beforeHandshake) {
246+
BackpressureErrorLabeler.applyLabelsIfEligible(t);
247+
}
243248
if (t instanceof MongoException) {
244249
throw (MongoException) t;
245250
} else {
@@ -262,6 +267,7 @@ public void completed(@Nullable final Void aVoid) {
262267
(initialResult, initialException) -> {
263268
if (initialException != null) {
264269
close();
270+
BackpressureErrorLabeler.applyLabelsIfEligible(initialException);
265271
callback.onResult(null, initialException);
266272
} else {
267273
assertNotNull(initialResult);
@@ -277,11 +283,13 @@ public void completed(@Nullable final Void aVoid) {
277283
@Override
278284
public void failed(final Throwable t) {
279285
close();
286+
BackpressureErrorLabeler.applyLabelsIfEligible(t);
280287
callback.onResult(null, t);
281288
}
282289
});
283290
} catch (Throwable t) {
284291
close();
292+
BackpressureErrorLabeler.applyLabelsIfEligible(t);
285293
callback.onResult(null, t);
286294
}
287295
}

driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.mongodb.internal.connection;
1818

1919
import com.mongodb.MongoCommandException;
20+
import com.mongodb.MongoException;
2021
import com.mongodb.MongoNodeIsRecoveringException;
2122
import com.mongodb.MongoNotPrimaryException;
2223
import com.mongodb.MongoSecurityException;
@@ -162,6 +163,11 @@ boolean relatedToWriteConcern() {
162163
return exception instanceof MongoWriteConcernWithResponseException;
163164
}
164165

166+
boolean hasSystemOverloadedLabel() {
167+
return exception instanceof MongoException
168+
&& ((MongoException) exception).hasErrorLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
169+
}
170+
165171
private static boolean stale(@Nullable final Throwable t, final ServerDescription currentServerDescription) {
166172
return TopologyVersionHelper.topologyVersion(t)
167173
.map(candidateTopologyVersion -> TopologyVersionHelper.newerOrEqual(

driver-core/src/test/unit/com/mongodb/internal/connection/AbstractServerDiscoveryAndMonitoringTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ protected void applyApplicationError(final BsonDocument applicationError) {
113113

114114
switch (when) {
115115
case "beforeHandshakeCompletes":
116+
BackpressureErrorLabeler.applyLabelsIfEligible(exception);
116117
server.sdamServerDescriptionManager().handleExceptionBeforeHandshake(
117118
SdamIssue.of(exception, new SdamIssue.Context(server.serverId(), errorGeneration, maxWireVersion)));
118119
break;

0 commit comments

Comments
 (0)