Skip to content

Commit dea68bc

Browse files
committed
[KYUUBI #2881] [SUB-TASK][KPIP-4] Rest client supports retry request if catch net exception
### _Why are the changes needed?_ close #2880 The main changes: - Add a interface IRestClient for the underlying rest client - Add KyuubiRetryableException to make call side aware whether attempts - Add RetryableRestClient which delegates the request to the real RestClient and provide a functionality of request attempts - Make KyuubiRestClient supports multi-hostUrls ### _How was this patch tested?_ - [x] Add some test cases that check the changes thoroughly including negative and positive cases if possible - [ ] Add screenshots for manual tests if appropriate - [ ] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request Closes #2881 from ulysses-you/rest-retry. Closes #2881 edd3934 [ulysses-you] comment df94e71 [ulysses-you] fix 5e16343 [ulysses-you] Rest client supports retry request if catch net exception Authored-by: ulysses-you <ulyssesyou18@gmail.com> Signed-off-by: ulysses-you <ulyssesyou@apache.org>
1 parent bdceaaf commit dea68bc

File tree

9 files changed

+361
-56
lines changed

9 files changed

+361
-56
lines changed

kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/BatchRestApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public CloseBatchResponse deleteBatch(String batchId, String hs2ProxyUser) {
8989
return this.getClient().delete(path, params, CloseBatchResponse.class, client.getAuthHeader());
9090
}
9191

92-
private RestClient getClient() {
92+
private IRestClient getClient() {
9393
return this.client.getHttpClient();
9494
}
9595
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.kyuubi.client;
19+
20+
import javax.net.ssl.SSLContext;
21+
import org.apache.http.client.config.RequestConfig;
22+
import org.apache.http.conn.ssl.NoopHostnameVerifier;
23+
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
24+
import org.apache.http.conn.ssl.TrustStrategy;
25+
import org.apache.http.impl.client.CloseableHttpClient;
26+
import org.apache.http.impl.client.HttpClientBuilder;
27+
import org.apache.http.ssl.SSLContexts;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
public class HttpClientFactory {
32+
private static final Logger LOG = LoggerFactory.getLogger(HttpClientFactory.class);
33+
34+
public static CloseableHttpClient createHttpClient(RestClientConf conf) {
35+
RequestConfig requestConfig =
36+
RequestConfig.custom()
37+
.setSocketTimeout(conf.getSocketTimeout())
38+
.setConnectTimeout(conf.getConnectTimeout())
39+
.build();
40+
SSLConnectionSocketFactory sslSocketFactory;
41+
try {
42+
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
43+
SSLContext sslContext =
44+
SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
45+
sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
46+
} catch (Exception e) {
47+
LOG.error("Error: ", e);
48+
throw new RuntimeException(e);
49+
}
50+
51+
return HttpClientBuilder.create()
52+
.setDefaultRequestConfig(requestConfig)
53+
.setSSLSocketFactory(sslSocketFactory)
54+
.build();
55+
}
56+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.kyuubi.client;
19+
20+
import java.util.Map;
21+
22+
/** A underlying http client interface for common rest request. */
23+
public interface IRestClient extends AutoCloseable {
24+
public <T> T get(String path, Map<String, Object> params, Class<T> type, String authHeader);
25+
26+
public String get(String path, Map<String, Object> params, String authHeader);
27+
28+
public <T> T post(String path, String body, Class<T> type, String authHeader);
29+
30+
public String post(String path, String body, String authHeader);
31+
32+
public <T> T delete(String path, Map<String, Object> params, Class<T> type, String authHeader);
33+
34+
public String delete(String path, Map<String, Object> params, String authHeader);
35+
}

kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/KyuubiRestClient.java

Lines changed: 61 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,15 @@
1818
package org.apache.kyuubi.client;
1919

2020
import java.net.URI;
21-
import javax.net.ssl.SSLContext;
21+
import java.util.Arrays;
22+
import java.util.LinkedList;
23+
import java.util.List;
2224
import org.apache.commons.lang3.StringUtils;
23-
import org.apache.http.client.config.RequestConfig;
24-
import org.apache.http.conn.ssl.NoopHostnameVerifier;
25-
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
26-
import org.apache.http.conn.ssl.TrustStrategy;
27-
import org.apache.http.impl.client.CloseableHttpClient;
28-
import org.apache.http.impl.client.HttpClientBuilder;
29-
import org.apache.http.ssl.SSLContexts;
3025
import org.apache.kyuubi.client.auth.*;
31-
import org.slf4j.Logger;
32-
import org.slf4j.LoggerFactory;
3326

3427
public class KyuubiRestClient implements AutoCloseable {
3528

36-
private static final Logger LOG = LoggerFactory.getLogger(KyuubiRestClient.class);
37-
38-
private RestClient httpClient;
29+
private IRestClient httpClient;
3930

4031
private AuthHeaderGenerator authHeaderGenerator;
4132

@@ -64,13 +55,21 @@ public void close() throws Exception {
6455
private KyuubiRestClient() {}
6556

6657
private KyuubiRestClient(Builder builder) {
67-
// Remove the trailing "/" from the hostUrl if present
68-
String hostUrl = builder.hostUrl.replaceAll("/$", "");
69-
String baseUrl = String.format("%s/%s", hostUrl, builder.version.getApiNamespace());
58+
List<String> baseUrls = new LinkedList<>();
59+
for (String hostUrl : builder.hostUrls) {
60+
// Remove the trailing "/" from the hostUrl if present
61+
String baseUrl =
62+
String.format("%s/%s", hostUrl.replaceAll("/$", ""), builder.version.getApiNamespace());
63+
baseUrls.add(baseUrl);
64+
}
7065

71-
CloseableHttpClient httpclient = initHttpClient(builder);
66+
RestClientConf conf = new RestClientConf();
67+
conf.setConnectTimeout(builder.connectTimeout);
68+
conf.setSocketTimeout(builder.socketTimeout);
69+
conf.setMaxAttempts(builder.maxAttempts);
70+
conf.setAttemptWaitTime(builder.attemptWaitTime);
7271

73-
this.httpClient = new RestClient(baseUrl, httpclient);
72+
this.httpClient = RetryableRestClient.getRestClient(baseUrls, conf);
7473

7574
switch (builder.authHeaderMethod) {
7675
case BASIC:
@@ -94,42 +93,25 @@ public String getAuthHeader() {
9493
return authHeaderGenerator.generateAuthHeader();
9594
}
9695

97-
public RestClient getHttpClient() {
96+
public IRestClient getHttpClient() {
9897
return httpClient;
9998
}
10099

101-
private CloseableHttpClient initHttpClient(Builder builder) {
102-
RequestConfig requestConfig =
103-
RequestConfig.custom()
104-
.setSocketTimeout(builder.socketTimeout)
105-
.setConnectTimeout(builder.connectTimeout)
106-
.build();
107-
SSLConnectionSocketFactory sslSocketFactory;
108-
try {
109-
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
110-
SSLContext sslContext =
111-
SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
112-
sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
113-
} catch (Exception e) {
114-
LOG.error("Error: ", e);
115-
throw new RuntimeException(e);
116-
}
117-
118-
CloseableHttpClient httpclient =
119-
HttpClientBuilder.create()
120-
.setDefaultRequestConfig(requestConfig)
121-
.setSSLSocketFactory(sslSocketFactory)
122-
.build();
123-
return httpclient;
124-
}
125-
126100
public static Builder builder(String hostUrl) {
127101
return new Builder(hostUrl);
128102
}
129103

104+
public static Builder builder(String... hostUrls) {
105+
return new Builder(Arrays.asList(hostUrls));
106+
}
107+
108+
public static Builder builder(List<String> hostUrls) {
109+
return new Builder(hostUrls);
110+
}
111+
130112
public static class Builder {
131113

132-
private String hostUrl;
114+
private List<String> hostUrls;
133115

134116
private String spnegoHost;
135117

@@ -147,8 +129,23 @@ public static class Builder {
147129

148130
private int connectTimeout = 3000;
149131

132+
private int maxAttempts = 3;
133+
134+
private int attemptWaitTime = 3000;
135+
150136
public Builder(String hostUrl) {
151-
this.hostUrl = hostUrl;
137+
if (StringUtils.isBlank(hostUrl)) {
138+
throw new IllegalArgumentException("hostUrl cannot be blank.");
139+
}
140+
this.hostUrls = new LinkedList<>();
141+
this.hostUrls.add(hostUrl);
142+
}
143+
144+
public Builder(List<String> hostUrls) {
145+
if (hostUrls.isEmpty()) {
146+
throw new IllegalArgumentException("hostUrls cannot be blank.");
147+
}
148+
this.hostUrls = hostUrls;
152149
}
153150

154151
public Builder spnegoHost(String host) {
@@ -193,16 +190,27 @@ public Builder connectionTimeout(int connectTimeout) {
193190
return this;
194191
}
195192

196-
public KyuubiRestClient build() {
197-
if (StringUtils.isBlank(hostUrl)) {
198-
throw new IllegalArgumentException("hostUrl cannot be blank.");
199-
}
193+
public Builder maxAttempts(int maxAttempts) {
194+
this.maxAttempts = maxAttempts;
195+
return this;
196+
}
197+
198+
public Builder attemptWaitTime(int attemptWaitTime) {
199+
this.attemptWaitTime = attemptWaitTime;
200+
return this;
201+
}
200202

203+
public KyuubiRestClient build() {
201204
if (authHeaderMethod == AuthHeaderMethod.SPNEGO && StringUtils.isBlank(spnegoHost)) {
202-
try {
203-
this.spnegoHost = new URI(hostUrl).getHost();
204-
} catch (Exception e) {
205+
if (hostUrls.size() > 1) {
205206
throw new IllegalArgumentException("spnegoHost is invalid.");
207+
} else {
208+
// follow the behavior of curl, use host url by default
209+
try {
210+
this.spnegoHost = new URI(hostUrls.get(0)).getHost();
211+
} catch (Exception e) {
212+
throw new IllegalArgumentException("spnegoHost is invalid.", e);
213+
}
206214
}
207215
}
208216
return new KyuubiRestClient(this);

kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/RestClient.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.kyuubi.client;
1919

2020
import java.io.IOException;
21+
import java.net.ConnectException;
2122
import java.net.URI;
2223
import java.net.URISyntaxException;
2324
import java.nio.charset.StandardCharsets;
@@ -32,15 +33,17 @@
3233
import org.apache.http.client.methods.HttpUriRequest;
3334
import org.apache.http.client.methods.RequestBuilder;
3435
import org.apache.http.client.utils.URIBuilder;
36+
import org.apache.http.conn.ConnectTimeoutException;
3537
import org.apache.http.entity.StringEntity;
3638
import org.apache.http.impl.client.CloseableHttpClient;
3739
import org.apache.http.util.EntityUtils;
3840
import org.apache.kyuubi.client.exception.KyuubiRestException;
41+
import org.apache.kyuubi.client.exception.KyuubiRetryableException;
3942
import org.apache.kyuubi.client.util.JsonUtil;
4043
import org.slf4j.Logger;
4144
import org.slf4j.LoggerFactory;
4245

43-
public class RestClient implements AutoCloseable {
46+
public class RestClient implements IRestClient {
4447

4548
private static final Logger LOG = LoggerFactory.getLogger(RestClient.class);
4649

@@ -60,31 +63,37 @@ public void close() throws Exception {
6063
}
6164
}
6265

66+
@Override
6367
public <T> T get(String path, Map<String, Object> params, Class<T> type, String authHeader) {
6468
String responseJson = get(path, params, authHeader);
6569
return JsonUtil.toObject(responseJson, type);
6670
}
6771

72+
@Override
6873
public String get(String path, Map<String, Object> params, String authHeader) {
6974
return doRequest(buildURI(path, params), authHeader, RequestBuilder.get());
7075
}
7176

77+
@Override
7278
public <T> T post(String path, String body, Class<T> type, String authHeader) {
7379
String responseJson = post(path, body, authHeader);
7480
return JsonUtil.toObject(responseJson, type);
7581
}
7682

83+
@Override
7784
public String post(String path, String body, String authHeader) {
7885
RequestBuilder postRequestBuilder =
7986
RequestBuilder.post().setEntity(new StringEntity(body, StandardCharsets.UTF_8));
8087
return doRequest(buildURI(path), authHeader, postRequestBuilder);
8188
}
8289

90+
@Override
8391
public <T> T delete(String path, Map<String, Object> params, Class<T> type, String authHeader) {
8492
String responseJson = delete(path, params, authHeader);
8593
return JsonUtil.toObject(responseJson, type);
8694
}
8795

96+
@Override
8897
public String delete(String path, Map<String, Object> params, String authHeader) {
8998
return doRequest(buildURI(path, params), authHeader, RequestBuilder.delete());
9099
}
@@ -118,6 +127,9 @@ private String doRequest(URI uri, String authHeader, RequestBuilder requestBuild
118127

119128
response = httpclient.execute(httpRequest, responseHandler);
120129
LOG.info("Response: {}", response);
130+
} catch (ConnectException | ConnectTimeoutException e) {
131+
// net exception can be retried by connecting to other Kyuubi server
132+
throw new KyuubiRetryableException("Api request failed for " + uri.toString(), e);
121133
} catch (Exception e) {
122134
LOG.error("Error: ", e);
123135
throw new KyuubiRestException("Api request failed for " + uri.toString(), e);

0 commit comments

Comments
 (0)