Skip to content

Commit 03fcfb1

Browse files
authored
Merge pull request #1721 from ClickHouse/fix_stale_connection_issue
Add retry on NoHttpResponseException
2 parents 66f8bce + 845e76d commit 03fcfb1

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.clickhouse.client.AbstractSocketClient;
44
import com.clickhouse.client.ClickHouseClient;
55
import com.clickhouse.client.ClickHouseConfig;
6-
import com.clickhouse.client.ClickHouseException;
76
import com.clickhouse.client.ClickHouseNode;
87
import com.clickhouse.client.ClickHouseRequest;
98
import com.clickhouse.client.ClickHouseSocketFactory;
@@ -31,9 +30,11 @@
3130
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
3231
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
3332
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
33+
import org.apache.hc.core5.http.ConnectionClosedException;
3434
import org.apache.hc.core5.http.Header;
3535
import org.apache.hc.core5.http.HttpHost;
3636
import org.apache.hc.core5.http.HttpRequest;
37+
import org.apache.hc.core5.http.NoHttpResponseException;
3738
import org.apache.hc.core5.http.config.Registry;
3839
import org.apache.hc.core5.http.config.RegistryBuilder;
3940
import org.apache.hc.core5.http.io.SocketConfig;
@@ -45,7 +46,6 @@
4546

4647
import javax.net.ssl.SSLContext;
4748
import javax.net.ssl.SSLException;
48-
4949
import java.io.BufferedReader;
5050
import java.io.ByteArrayInputStream;
5151
import java.io.ByteArrayOutputStream;
@@ -58,7 +58,6 @@
5858
import java.net.HttpURLConnection;
5959
import java.net.InetSocketAddress;
6060
import java.net.Socket;
61-
import java.net.StandardSocketOptions;
6261
import java.nio.charset.StandardCharsets;
6362
import java.util.Collections;
6463
import java.util.List;
@@ -251,11 +250,35 @@ protected ClickHouseHttpResponse post(ClickHouseConfig config, String sql, Click
251250
ClickHouseHttpEntity postBody = new ClickHouseHttpEntity(config, contentType, contentEncoding, boundary,
252251
sql, data, tables);
253252
post.setEntity(postBody);
254-
CloseableHttpResponse response;
255-
try {
256-
response = client.execute(post);
257-
} catch (IOException e) {
258-
throw new ConnectException(ClickHouseUtils.format("HTTP request failed: %s", e.getMessage()));
253+
CloseableHttpResponse response = null;
254+
255+
int retryAttempts = config.getBoolOption(ClickHouseHttpOption.AHC_RETRY_ON_FAILURE) ? 2 : 1;
256+
for (int attempt = 0; attempt < retryAttempts; attempt++) {
257+
boolean isLastAttempt = attempt == retryAttempts - 1;
258+
log.debug("HTTP request attempt " + attempt);
259+
try {
260+
response = client.execute(post);
261+
262+
if (!isLastAttempt && (response.getCode() == HttpURLConnection.HTTP_UNAVAILABLE)) {
263+
log.debug("HTTP request failed with status code 503, retrying...");
264+
continue;
265+
}
266+
267+
break;
268+
} catch (NoHttpResponseException | ConnectionClosedException e) {
269+
if (isLastAttempt) {
270+
throw new ConnectException(e.getMessage());
271+
} else {
272+
continue;
273+
}
274+
} catch (IOException e) {
275+
log.error("HTTP request failed", e);
276+
throw new ConnectException(e.getMessage());
277+
}
278+
}
279+
if (response == null) {
280+
// Should not happen but needed for compiler
281+
throw new ConnectException("HTTP request failed");
259282
}
260283

261284
checkResponse(config, response);

clickhouse-http-client/src/main/java/com/clickhouse/client/http/config/ClickHouseHttpOption.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.clickhouse.data.ClickHouseChecker;
55

66
import java.io.Serializable;
7+
import java.net.UnknownHostException;
78

89
/**
910
* Http client options.
@@ -73,6 +74,20 @@ public enum ClickHouseHttpOption implements ClickHouseOption {
7374
*/
7475
AHC_VALIDATE_AFTER_INACTIVITY("ahc_validate_after_inactivity", 5000L,
7576
"The time in milliseconds after which the connection is validated after inactivity."),
77+
78+
/**
79+
* Whether to retry on failure with AsyncHttpClient. Failure includes some 'critical' IO exceptions:
80+
* <ul>
81+
* <li>{@code org.apache.hc.core5.http.ConnectionClosedException}</li>
82+
* <li>{@code org.apache.hc.core5.http.NoHttpResponseException}</li>
83+
* </ul>
84+
*
85+
* And next status codes:
86+
* <ul>
87+
* <li>{@code 503 Service Unavailable}</li>
88+
* </ul>
89+
*/
90+
AHC_RETRY_ON_FAILURE("ahc_retry_on_failure", false, "Whether to retry on failure with AsyncHttpClient.")
7691
;
7792

7893
private final String key;

clickhouse-http-client/src/test/java/com/clickhouse/client/http/ApacheHttpConnectionImplTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@
2525
import java.util.concurrent.atomic.AtomicBoolean;
2626

2727
import com.github.tomakehurst.wiremock.WireMockServer;
28+
import com.github.tomakehurst.wiremock.admin.model.ScenarioState;
2829
import com.github.tomakehurst.wiremock.client.WireMock;
2930
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
3031
import com.github.tomakehurst.wiremock.http.Fault;
3132
import com.github.tomakehurst.wiremock.stubbing.Scenario;
33+
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
34+
import com.github.tomakehurst.wiremock.stubbing.Scenario;
3235
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
3336
import org.apache.hc.core5.http.NoHttpResponseException;
37+
import org.apache.hc.core5.http.HttpStatus;
3438
import org.testng.Assert;
3539
import org.testng.annotations.DataProvider;
3640
import org.testng.annotations.Test;
@@ -144,12 +148,70 @@ public void testFailureWhileRequest() {
144148
httpClient.executeAndWait(request);
145149
} catch (ClickHouseException e) {
146150
Assert.assertEquals(e.getErrorCode(), ClickHouseException.ERROR_NETWORK);
151+
return;
147152
}
153+
154+
Assert.fail("Should throw exception");
148155
} finally {
149156
faultyServer.stop();
150157
}
151158
}
152159

160+
@Test(groups = {"unit"}, dataProvider = "retryOnFailureProvider")
161+
public void testRetryOnFailure(StubMapping failureStub) {
162+
faultyServer = new WireMockServer(9090);
163+
faultyServer.start();
164+
try {
165+
faultyServer.addStubMapping(failureStub);
166+
faultyServer.addStubMapping(WireMock.post(WireMock.anyUrl())
167+
.withRequestBody(WireMock.equalTo("SELECT 1"))
168+
.inScenario("Retry")
169+
.whenScenarioStateIs("Failed")
170+
.willReturn(WireMock.aResponse()
171+
.withHeader("X-ClickHouse-Summary",
172+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}"))
173+
.build());
174+
175+
ClickHouseHttpClient httpClient = new ClickHouseHttpClient();
176+
Map<ClickHouseOption, Serializable> options = new HashMap<>();
177+
options.put(ClickHouseHttpOption.AHC_RETRY_ON_FAILURE, true);
178+
ClickHouseConfig config = new ClickHouseConfig(options);
179+
httpClient.init(config);
180+
ClickHouseRequest request = httpClient.read("http://localhost:9090/").query("SELECT 1");
181+
182+
ClickHouseResponse response = null;
183+
try {
184+
response = httpClient.executeAndWait(request);
185+
} catch (ClickHouseException e) {
186+
Assert.fail("Should not throw exception", e);
187+
}
188+
Assert.assertEquals(response.getSummary().getReadBytes(), 10);
189+
Assert.assertEquals(response.getSummary().getReadRows(), 1);
190+
} finally {
191+
faultyServer.stop();
192+
}
193+
}
194+
195+
@DataProvider(name = "retryOnFailureProvider")
196+
private static StubMapping[] retryOnFailureProvider() {
197+
return new StubMapping[] {
198+
WireMock.post(WireMock.anyUrl())
199+
.withRequestBody(WireMock.equalTo("SELECT 1"))
200+
.inScenario("Retry")
201+
.whenScenarioStateIs(Scenario.STARTED)
202+
.willReturn(WireMock.aResponse().withFault(Fault.EMPTY_RESPONSE))
203+
.willSetStateTo("Failed")
204+
.build()
205+
,WireMock.post(WireMock.anyUrl())
206+
.withRequestBody(WireMock.equalTo("SELECT 1"))
207+
.inScenario("Retry")
208+
.whenScenarioStateIs(Scenario.STARTED)
209+
.willReturn(WireMock.aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))
210+
.willSetStateTo("Failed")
211+
.build()
212+
};
213+
}
214+
153215
@Test(groups = {"unit"}, dataProvider = "validationTimeoutProvider")
154216
public void testNoHttpResponseExceptionWithValidation(long validationTimeout) {
155217

0 commit comments

Comments
 (0)