Problem
REST API的開發者,大都會非常頻繁的存取REST服務提供者;Connection Pool是其中一個增進存取效率的方式。然而,這陣子在使用RESTEasy + Apache Connection Pool後,發現ConnectionRequestTimeout並沒作用。本篇文章主要分享目前的解決方法。
Version Info
由於某些限制,我們目前還沒使用最新版本的RESTEasy,版本資訊如下:
resteasy-client: 3.0.14.Final httpcore: 4.3.3 httpclient: 4.3.6
Test RESTClient
我寫了一個測試去確認ConnectionRequestTimeout是否有作用,方法如下:
- 將Pool大小設定為1。
- 設定ConnectionRequestTimeout為2秒。
- 依序發起兩個HTTP Get,但第一個請求不關閉。
- 接著發起第二個HTTP Get,由於沒有有效的連線可以使用,會Block。
- 在2秒後,Client會收到Exception,Root Cause為ConnectionPoolTimeoutException。
首先是HttpEngine設定的部分,由於這個版本ResteasyClientBuilder內建的PoolManager有些問題,因此我是另外產生的,程式碼大致如下:
// 這是自己寫的Builder,產生的實體是PoolingHttpClientConnectionManager private HttpClientConnectionManager initConnectionManager(int poolSize, int perRoute) { HttpClientConnectionManagerBuilder builder = new HttpClientConnectionManagerBuilder(); return builder.withMaxTotal(poolSize).withDefaultMaxPerRoute(perRoute).build(); } // TestCase的timeout設定為20秒,避免有問題時會卡很久 @Test(timeout=20000) public void testRequestTimeoutWithCustomHttpEngine() throws Exception { int expectTimeout = 2*1000; connectionManager = initConnectionManager(20 , 1); RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(expectTimeout) .build(); ClientHttpEngine httpEngin = new ClientHttpEngineBuilder() .withRequestConfig(requestConfig) .withConnectionManager(connectionManager).build(); ResteasyClientBuilder clientBuilder = (ResteasyClientBuilder)ResteasyClientBuilder.newBuilder(); clientBuilder.httpEngine(httpEngin); // 以下略
接著是發出兩個請求,第一個請求沒執行close,接著執行第二個請求:
// 延續httpEngine的初始化 Client client = clientBuilder.build(); Response leak_repsonse = client.target(target).request().get(); Response second_response = null; long before = System.currentTimeMillis(); try { second_response = client.target(target).request().get(); fail("should be timeout"); } catch (Exception e) { assertNotNull(e.getCause()); assertTrue(e.getCause() instanceof ConnectionPoolTimeoutException); assertEquals(expectTimeout, System.currentTimeMillis() - before, 500); } finally { leak_repsonse.close(); if( second_response != null ) second_response.close(); } }
在執行以上測試後,會因為第二個請求block直到Test case 20秒timeout而錯誤;此測試並沒發生預期的ConnectionPoolTimeoutException。
Test HttpClient
由於前一個測試沒達到預期效果,因此繼續確認PoolingHttpClientConnectionManager是否有問題。測試流程與前一個測試大同小異,差別只在於是直接對HttpClient做操作:
@Test(timeout=20*1000) public void testRequestTimeout() throws Exception { int expect_timeout = 2*1000; connectionManager = new HttpClientConnectionManagerBuilder().build(); RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(expect_timeout) .build(); HttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build(); HttpUriRequest uriRequest = new HttpGet(target); HttpResponse response = httpClient.execute(uriRequest); assertEquals(200, response.getStatusLine().getStatusCode()); long before = System.currentTimeMillis(); try { uriRequest = new HttpGet(target); response = httpClient.execute(uriRequest); } catch( ConnectionPoolTimeoutException e ) { assertEquals("Timeout waiting for connection from pool", e.getMessage()); } long duration = System.currentTimeMillis() - before; assertEquals(expect_timeout, duration, 500); }
這個測試最後會因為第二個連線無法在2秒內取得而拋出ConnectionPoolTimeoutException。
How to fix?
根據這兩個測試,懷疑是RESTEasy與HttpClient整合起來的問題。測試過更新RESTEasy與HttpClient的libraries後,確認只要將HttpClient的library升級至4.5.3以上就可以解決問題:
resteasy-client: 3.0.14.Final httpcore: 4.3.3 httpclient: 4.5.3
留言
張貼留言