11package ru .yandex .clickhouse .util ;
22
33import org .apache .http .HttpHost ;
4+ import org .apache .http .HttpResponse ;
45import org .apache .http .NoHttpResponseException ;
56import org .apache .http .client .methods .HttpGet ;
67import org .apache .http .client .methods .HttpPost ;
78import org .apache .http .conn .HttpHostConnectException ;
89import org .apache .http .impl .client .CloseableHttpClient ;
910import org .apache .http .protocol .BasicHttpContext ;
1011import org .apache .http .protocol .HttpContext ;
12+ import org .apache .http .util .EntityUtils ;
1113import org .testng .annotations .AfterClass ;
1214import org .testng .annotations .AfterMethod ;
1315import 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