Skip to content

Commit 26da7d8

Browse files
authored
Merge pull request #760 from ryan-tu/validate_stale_conn
Validate stale connection to fix the bug: failed to respond
2 parents 8a4aee2 + a265d08 commit 26da7d8

File tree

4 files changed

+98
-12
lines changed

4 files changed

+98
-12
lines changed

clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/settings/ClickHouseConnectionSettings.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public enum ClickHouseConnectionSettings implements DriverPropertyCreator {
3131
/**
3232
* for ConnectionManager
3333
*/
34+
VALIDATE_AFTER_INACTIVITY_MILLIS("validateAfterInactivityMillis", 3 * 1000, "period of inactivity in milliseconds after which persistent connections must be re-validated, this check helps detect connections that have become stale (half-closed) while kept inactive in the pool. "),
3435
TIME_TO_LIVE_MILLIS("timeToLiveMillis", 60 * 1000, ""),
3536
DEFAULT_MAX_PER_ROUTE("defaultMaxPerRoute", 500, ""),
3637
MAX_TOTAL("maxTotal", 10000, ""),

clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/settings/ClickHouseProperties.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class ClickHouseProperties {
1717
private int connectionTimeout;
1818
private int dataTransferTimeout;
1919
private int timeToLiveMillis;
20+
private int validateAfterInactivityMillis;
2021
private int defaultMaxPerRoute;
2122
private int maxTotal;
2223
private int maxRetries;
@@ -112,6 +113,7 @@ public ClickHouseProperties(Properties info) {
112113
this.connectionTimeout = (Integer)getSetting(info, ClickHouseConnectionSettings.CONNECTION_TIMEOUT);
113114
this.dataTransferTimeout = (Integer)getSetting(info, ClickHouseConnectionSettings.DATA_TRANSFER_TIMEOUT);
114115
this.timeToLiveMillis = (Integer)getSetting(info, ClickHouseConnectionSettings.TIME_TO_LIVE_MILLIS);
116+
this.validateAfterInactivityMillis = (Integer)getSetting(info, ClickHouseConnectionSettings.VALIDATE_AFTER_INACTIVITY_MILLIS);
115117
this.defaultMaxPerRoute = (Integer)getSetting(info, ClickHouseConnectionSettings.DEFAULT_MAX_PER_ROUTE);
116118
this.maxTotal = (Integer)getSetting(info, ClickHouseConnectionSettings.MAX_TOTAL);
117119
this.maxRetries = (Integer)getSetting(info, ClickHouseConnectionSettings.MAX_RETRIES);
@@ -182,6 +184,7 @@ public Properties asProperties() {
182184
ret.put(ClickHouseConnectionSettings.CONNECTION_TIMEOUT.getKey(), String.valueOf(connectionTimeout));
183185
ret.put(ClickHouseConnectionSettings.DATA_TRANSFER_TIMEOUT.getKey(), String.valueOf(dataTransferTimeout));
184186
ret.put(ClickHouseConnectionSettings.TIME_TO_LIVE_MILLIS.getKey(), String.valueOf(timeToLiveMillis));
187+
ret.put(ClickHouseConnectionSettings.VALIDATE_AFTER_INACTIVITY_MILLIS.getKey(), String.valueOf(validateAfterInactivityMillis));
185188
ret.put(ClickHouseConnectionSettings.DEFAULT_MAX_PER_ROUTE.getKey(), String.valueOf(defaultMaxPerRoute));
186189
ret.put(ClickHouseConnectionSettings.MAX_TOTAL.getKey(), String.valueOf(maxTotal));
187190
ret.put(ClickHouseConnectionSettings.MAX_RETRIES.getKey(), String.valueOf(maxRetries));
@@ -254,6 +257,7 @@ public ClickHouseProperties(ClickHouseProperties properties) {
254257
setConnectionTimeout(properties.connectionTimeout);
255258
setDataTransferTimeout(properties.dataTransferTimeout);
256259
setTimeToLiveMillis(properties.timeToLiveMillis);
260+
setValidateAfterInactivityMillis(properties.validateAfterInactivityMillis);
257261
setDefaultMaxPerRoute(properties.defaultMaxPerRoute);
258262
setMaxTotal(properties.maxTotal);
259263
setMaxRetries(properties.maxRetries);
@@ -582,6 +586,14 @@ public void setTimeToLiveMillis(int timeToLiveMillis) {
582586
this.timeToLiveMillis = timeToLiveMillis;
583587
}
584588

589+
public int getValidateAfterInactivityMillis() {
590+
return validateAfterInactivityMillis;
591+
}
592+
593+
public void setValidateAfterInactivityMillis(int validateAfterInactivityMillis) {
594+
this.validateAfterInactivityMillis = validateAfterInactivityMillis;
595+
}
596+
585597
public int getDefaultMaxPerRoute() {
586598
return defaultMaxPerRoute;
587599
}

clickhouse-jdbc/src/main/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ private PoolingHttpClientConnectionManager getConnectionManager()
151151
TimeUnit.MILLISECONDS
152152
);
153153

154+
connectionManager.setValidateAfterInactivity(properties.getValidateAfterInactivityMillis());
154155
connectionManager.setDefaultMaxPerRoute(properties.getDefaultMaxPerRoute());
155156
connectionManager.setMaxTotal(properties.getMaxTotal());
156157
connectionManager.setDefaultConnectionConfig(getConnectionConfig());

clickhouse-jdbc/src/test/java/ru/yandex/clickhouse/util/ClickHouseHttpClientBuilderTest.java

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package ru.yandex.clickhouse.util;
22

33
import org.apache.http.HttpHost;
4+
import org.apache.http.HttpResponse;
45
import org.apache.http.NoHttpResponseException;
56
import org.apache.http.client.methods.HttpGet;
67
import org.apache.http.client.methods.HttpPost;
78
import org.apache.http.conn.HttpHostConnectException;
89
import org.apache.http.impl.client.CloseableHttpClient;
910
import org.apache.http.protocol.BasicHttpContext;
1011
import org.apache.http.protocol.HttpContext;
12+
import org.apache.http.util.EntityUtils;
1113
import org.testng.annotations.AfterClass;
1214
import org.testng.annotations.AfterMethod;
1315
import org.testng.annotations.BeforeClass;
@@ -176,18 +178,22 @@ private static Object[][] provideAuthUserPasswordTestData() {
176178
};
177179
}
178180

179-
private static WireMockServer newServer() {
181+
private static WireMockServer newServer(int delayMillis) {
180182
WireMockServer server = new WireMockServer(
181183
WireMockConfiguration.wireMockConfig().dynamicPort());
182184
server.start();
183185
server.stubFor(WireMock.post(WireMock.urlPathMatching("/*"))
184186
.willReturn(WireMock.aResponse().withStatus(200).withHeader("Connection", "Keep-Alive")
185187
.withHeader("Content-Type", "text/plain; charset=UTF-8")
186188
.withHeader("Transfer-Encoding", "chunked").withHeader("Keep-Alive", "timeout=3")
187-
.withBody("OK.........................").withFixedDelay(2)));
189+
.withBody("OK.........................").withFixedDelay(delayMillis)));
188190
return server;
189191
}
190192

193+
private static WireMockServer newServer() {
194+
return newServer(2);
195+
}
196+
191197
private static void shutDownServerWithDelay(final WireMockServer server, final long delayMs) {
192198
new Thread() {
193199
public void run() {
@@ -203,38 +209,104 @@ public void run() {
203209
}.start();
204210
}
205211

206-
// @Test(groups = "unit", dependsOnMethods = { "testWithRetry" }, expectedExceptions = { NoHttpResponseException.class })
207-
public void testWithoutRetry() throws Exception {
208-
final WireMockServer server = newServer();
212+
@Test(expectedExceptions = { NoHttpResponseException.class })
213+
public void testReproduceFailedToResponseProblem() throws Exception {
214+
final WireMockServer server = newServer(2);
209215

210216
ClickHouseProperties props = new ClickHouseProperties();
217+
// Disable retry when "failed to respond" occurs.
211218
props.setMaxRetries(0);
219+
// Disable validation to reproduce "failed to respond" problem
220+
props.setValidateAfterInactivityMillis(0);
221+
// Ensure there is exactly one TCP connection in connection pool and therefore be re-used between
222+
// multiple http requests.
223+
props.setMaxTotal(1);
224+
props.setDefaultMaxPerRoute(1);
225+
212226
ClickHouseHttpClientBuilder builder = new ClickHouseHttpClientBuilder(props);
213227
CloseableHttpClient client = builder.buildClient();
214228
HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%201");
215229

216-
shutDownServerWithDelay(server, 500);
230+
try {
231+
// Make the 1st http request to establish one tcp connection and keep it in the pool.
232+
{
233+
HttpResponse response = client.execute(post);
234+
EntityUtils.consume(response.getEntity());
235+
}
236+
237+
// Close the server, now the pooling tcp connection is half closed.
238+
server.shutdownServer();
239+
server.stop();
240+
241+
// The 2nd http request will re-use the pooling tcp connection which is stale
242+
// and "failed to respond" occurs.
243+
{
244+
HttpResponse response = client.execute(post);
245+
EntityUtils.consume(response.getEntity());
246+
}
247+
} finally {
248+
client.close();
249+
}
250+
}
251+
252+
@Test(expectedExceptions = { HttpHostConnectException.class })
253+
public void testEnableValidation() throws Exception {
254+
final WireMockServer server = newServer(2);
255+
256+
ClickHouseProperties props = new ClickHouseProperties();
257+
// Disable retry when "failed to respond" occurs.
258+
props.setMaxRetries(0);
259+
// Disable validation to reproduce "failed to respond" problem
260+
props.setValidateAfterInactivityMillis(1);
261+
// Ensure there is exactly one TCP connection in connection pool and therefore be re-used between
262+
// multiple http requests.
263+
props.setMaxTotal(1);
264+
props.setDefaultMaxPerRoute(1);
265+
266+
ClickHouseHttpClientBuilder builder = new ClickHouseHttpClientBuilder(props);
267+
CloseableHttpClient client = builder.buildClient();
268+
HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%201");
217269

218270
try {
219-
client.execute(post);
271+
// Make the 1st http request to establish one tcp connection and keep it in the pool.
272+
{
273+
HttpResponse response = client.execute(post);
274+
EntityUtils.consume(response.getEntity());
275+
}
276+
277+
// Sleep a while to wait for the validation reaches inactivity timeout.
278+
Thread.sleep(5);
279+
280+
// Close the server, now the pooling tcp connection is half closed.
281+
server.shutdownServer();
282+
server.stop();
283+
284+
// The 2nd http request re-uses the pooling tcp connection.
285+
// But the validation checks that the connection has been stale, thus a
286+
// new tcp connection is attempted to establish to the closed server
287+
// which leads to HttpHostConnectException.
288+
{
289+
HttpResponse response = client.execute(post);
290+
EntityUtils.consume(response.getEntity());
291+
}
220292
} finally {
221293
client.close();
222294
}
223295
}
224296

225-
// @Test(groups = "unit", expectedExceptions = { HttpHostConnectException.class })
297+
@Test(expectedExceptions = { HttpHostConnectException.class })
226298
public void testWithRetry() throws Exception {
227-
final WireMockServer server = newServer();
299+
final WireMockServer server = newServer(500);
228300

229301
ClickHouseProperties props = new ClickHouseProperties();
230-
// props.setMaxRetries(3);
302+
props.setMaxRetries(3);
231303
ClickHouseHttpClientBuilder builder = new ClickHouseHttpClientBuilder(props);
232304
CloseableHttpClient client = builder.buildClient();
233305
HttpContext context = new BasicHttpContext();
234306
context.setAttribute("is_idempotent", Boolean.TRUE);
235307
HttpPost post = new HttpPost("http://localhost:" + server.port() + "/?db=system&query=select%202");
236-
237-
shutDownServerWithDelay(server, 500);
308+
309+
shutDownServerWithDelay(server, 100);
238310

239311
try {
240312
client.execute(post, context);

0 commit comments

Comments
 (0)