Skip to content

Commit 81d5950

Browse files
committed
HADOOP-15609. Retry KMS calls when SSLHandshakeException occurs. Contributed by Kitti Nanasi.
1 parent 2686447 commit 81d5950

File tree

2 files changed

+92
-4
lines changed

2 files changed

+92
-4
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/LoadBalancingKMSClientProvider.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020

2121
import java.io.IOException;
2222
import java.io.InterruptedIOException;
23+
import java.net.ConnectException;
2324
import java.security.GeneralSecurityException;
2425
import java.security.NoSuchAlgorithmException;
2526
import java.util.Arrays;
2627
import java.util.Collections;
2728
import java.util.List;
2829
import java.util.concurrent.atomic.AtomicInteger;
2930

31+
import javax.net.ssl.SSLHandshakeException;
32+
3033
import org.apache.hadoop.conf.Configuration;
3134
import org.apache.hadoop.crypto.key.KeyProvider;
3235
import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.CryptoExtension;
@@ -115,7 +118,6 @@ private <T> T doOp(ProviderCallable<T> op, int currPos)
115118
if (providers.length == 0) {
116119
throw new IOException("No providers configured !");
117120
}
118-
IOException ex = null;
119121
int numFailovers = 0;
120122
for (int i = 0;; i++, numFailovers++) {
121123
KMSClientProvider provider = providers[(currPos + i) % providers.length];
@@ -130,8 +132,15 @@ private <T> T doOp(ProviderCallable<T> op, int currPos)
130132
} catch (IOException ioe) {
131133
LOG.warn("KMS provider at [{}] threw an IOException: ",
132134
provider.getKMSUrl(), ioe);
133-
ex = ioe;
134-
135+
// SSLHandshakeException can occur here because of lost connection
136+
// with the KMS server, creating a ConnectException from it,
137+
// so that the FailoverOnNetworkExceptionRetry policy will retry
138+
if (ioe instanceof SSLHandshakeException) {
139+
Exception cause = ioe;
140+
ioe = new ConnectException("SSLHandshakeException: "
141+
+ cause.getMessage());
142+
ioe.initCause(cause);
143+
}
135144
RetryAction action = null;
136145
try {
137146
action = retryPolicy.shouldRetry(ioe, 0, numFailovers, false);
@@ -153,7 +162,7 @@ private <T> T doOp(ProviderCallable<T> op, int currPos)
153162
CommonConfigurationKeysPublic.
154163
KMS_CLIENT_FAILOVER_MAX_RETRIES_KEY, providers.length),
155164
providers.length);
156-
throw ex;
165+
throw ioe;
157166
}
158167
if (((numFailovers + 1) % providers.length) == 0) {
159168
// Sleep only after we try all the providers for every cycle.

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/crypto/key/kms/TestLoadBalancingKMSClientProvider.java

+79
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.hadoop.crypto.key.kms;
1919

2020
import static org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.EncryptedKeyVersion;
21+
import static org.apache.hadoop.test.LambdaTestUtils.intercept;
2122
import static org.junit.Assert.assertEquals;
2223
import static org.junit.Assert.assertTrue;
2324
import static org.junit.Assert.fail;
@@ -26,12 +27,15 @@
2627
import static org.mockito.Mockito.verify;
2728

2829
import java.io.IOException;
30+
import java.net.ConnectException;
2931
import java.net.NoRouteToHostException;
3032
import java.net.URI;
3133
import java.net.UnknownHostException;
3234
import java.security.GeneralSecurityException;
3335
import java.security.NoSuchAlgorithmException;
3436

37+
import javax.net.ssl.SSLHandshakeException;
38+
3539
import org.apache.hadoop.conf.Configuration;
3640
import org.apache.hadoop.crypto.key.KeyProvider;
3741
import org.apache.hadoop.crypto.key.KeyProvider.Options;
@@ -44,13 +48,18 @@
4448
import org.apache.hadoop.security.authorize.AuthorizationException;
4549
import org.junit.After;
4650
import org.junit.BeforeClass;
51+
import org.junit.Rule;
4752
import org.junit.Test;
53+
import org.junit.rules.Timeout;
4854
import org.mockito.Mockito;
4955

5056
import com.google.common.collect.Sets;
5157

5258
public class TestLoadBalancingKMSClientProvider {
5359

60+
@Rule
61+
public Timeout testTimeout = new Timeout(30 * 1000);
62+
5463
@BeforeClass
5564
public static void setup() throws IOException {
5665
SecurityUtil.setTokenServiceUseIp(false);
@@ -638,4 +647,74 @@ public void testClientRetriesWithAuthenticationExceptionWrappedinIOException()
638647
verify(p2, Mockito.times(1)).createKey(Mockito.eq("test3"),
639648
Mockito.any(Options.class));
640649
}
650+
651+
/**
652+
* Tests the operation succeeds second time after SSLHandshakeException.
653+
* @throws Exception
654+
*/
655+
@Test
656+
public void testClientRetriesWithSSLHandshakeExceptionSucceedsSecondTime()
657+
throws Exception {
658+
Configuration conf = new Configuration();
659+
conf.setInt(
660+
CommonConfigurationKeysPublic.KMS_CLIENT_FAILOVER_MAX_RETRIES_KEY, 3);
661+
final String keyName = "test";
662+
KMSClientProvider p1 = mock(KMSClientProvider.class);
663+
when(p1.createKey(Mockito.anyString(), Mockito.any(Options.class)))
664+
.thenThrow(new SSLHandshakeException("p1"))
665+
.thenReturn(new KMSClientProvider.KMSKeyVersion(keyName, "v1",
666+
new byte[0]));
667+
KMSClientProvider p2 = mock(KMSClientProvider.class);
668+
when(p2.createKey(Mockito.anyString(), Mockito.any(Options.class)))
669+
.thenThrow(new ConnectException("p2"));
670+
671+
when(p1.getKMSUrl()).thenReturn("p1");
672+
when(p2.getKMSUrl()).thenReturn("p2");
673+
674+
LoadBalancingKMSClientProvider kp = new LoadBalancingKMSClientProvider(
675+
new KMSClientProvider[] {p1, p2}, 0, conf);
676+
677+
kp.createKey(keyName, new Options(conf));
678+
verify(p1, Mockito.times(2)).createKey(Mockito.eq(keyName),
679+
Mockito.any(Options.class));
680+
verify(p2, Mockito.times(1)).createKey(Mockito.eq(keyName),
681+
Mockito.any(Options.class));
682+
}
683+
684+
/**
685+
* Tests the operation fails at every attempt after SSLHandshakeException.
686+
* @throws Exception
687+
*/
688+
@Test
689+
public void testClientRetriesWithSSLHandshakeExceptionFailsAtEveryAttempt()
690+
throws Exception {
691+
Configuration conf = new Configuration();
692+
conf.setInt(
693+
CommonConfigurationKeysPublic.KMS_CLIENT_FAILOVER_MAX_RETRIES_KEY, 2);
694+
final String keyName = "test";
695+
final String exceptionMessage = "p1 exception message";
696+
KMSClientProvider p1 = mock(KMSClientProvider.class);
697+
Exception originalSslEx = new SSLHandshakeException(exceptionMessage);
698+
when(p1.createKey(Mockito.anyString(), Mockito.any(Options.class)))
699+
.thenThrow(originalSslEx);
700+
KMSClientProvider p2 = mock(KMSClientProvider.class);
701+
when(p2.createKey(Mockito.anyString(), Mockito.any(Options.class)))
702+
.thenThrow(new ConnectException("p2 exception message"));
703+
704+
when(p1.getKMSUrl()).thenReturn("p1");
705+
when(p2.getKMSUrl()).thenReturn("p2");
706+
707+
LoadBalancingKMSClientProvider kp = new LoadBalancingKMSClientProvider(
708+
new KMSClientProvider[] {p1, p2}, 0, conf);
709+
710+
Exception interceptedEx = intercept(ConnectException.class,
711+
"SSLHandshakeException: " + exceptionMessage,
712+
()-> kp.createKey(keyName, new Options(conf)));
713+
assertEquals(originalSslEx, interceptedEx.getCause());
714+
715+
verify(p1, Mockito.times(2)).createKey(Mockito.eq(keyName),
716+
Mockito.any(Options.class));
717+
verify(p2, Mockito.times(1)).createKey(Mockito.eq(keyName),
718+
Mockito.any(Options.class));
719+
}
641720
}

0 commit comments

Comments
 (0)