Skip to content

Commit d774b3d

Browse files
authored
fix: add configurable evictIdleConnections (#431)
1 parent a09e13c commit d774b3d

File tree

6 files changed

+131
-7
lines changed

6 files changed

+131
-7
lines changed

core-httpclient-impl/src/main/java/com/optimizely/ab/OptimizelyFactory.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,28 @@ public static void setBlockingTimeout(long blockingDuration, TimeUnit blockingTi
123123
PropertyUtils.set(HttpProjectConfigManager.CONFIG_BLOCKING_UNIT, blockingTimeout.toString());
124124
}
125125

126+
/**
127+
* Convenience method for setting the evict idle connections.
128+
* {@link HttpProjectConfigManager.Builder#withEvictIdleConnections(long, TimeUnit)}
129+
*
130+
* @param maxIdleTime The connection idle time duration (0 to disable eviction)
131+
* @param maxIdleTimeUnit The connection idle time unit
132+
*/
133+
public static void setEvictIdleConnections(long maxIdleTime, TimeUnit maxIdleTimeUnit) {
134+
if (maxIdleTimeUnit == null) {
135+
logger.warn("TimeUnit cannot be null. Reverting to default configuration.");
136+
return;
137+
}
138+
139+
if (maxIdleTime < 0) {
140+
logger.warn("Timeout cannot be < 0. Reverting to default configuration.");
141+
return;
142+
}
143+
144+
PropertyUtils.set(HttpProjectConfigManager.CONFIG_EVICT_DURATION, Long.toString(maxIdleTime));
145+
PropertyUtils.set(HttpProjectConfigManager.CONFIG_EVICT_UNIT, maxIdleTimeUnit.toString());
146+
}
147+
126148
/**
127149
* Convenience method for setting the polling interval on System properties.
128150
* {@link HttpProjectConfigManager.Builder#withPollingInterval(Long, TimeUnit)}

core-httpclient-impl/src/main/java/com/optimizely/ab/OptimizelyHttpClient.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
import org.apache.http.client.methods.CloseableHttpResponse;
2323
import org.apache.http.client.methods.HttpUriRequest;
2424
import org.apache.http.impl.client.CloseableHttpClient;
25+
import org.apache.http.impl.client.HttpClientBuilder;
2526
import org.apache.http.impl.client.HttpClients;
2627
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
2728

2829
import java.io.Closeable;
2930
import java.io.IOException;
31+
import java.util.concurrent.TimeUnit;
3032

3133
/**
3234
* Basic HttpClient wrapper to be utilized for fetching the datafile
@@ -73,6 +75,9 @@ public static class Builder {
7375
private int maxPerRoute = 20;
7476
// Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer.
7577
private int validateAfterInactivity = 5000;
78+
// force-close the connection after this idle time (with 0, eviction is disabled by default)
79+
long evictConnectionIdleTimePeriod = 0;
80+
TimeUnit evictConnectionIdleTimeUnit = TimeUnit.MILLISECONDS;
7681

7782
private Builder() {
7883

@@ -93,18 +98,29 @@ public Builder withValidateAfterInactivity(int validateAfterInactivity) {
9398
return this;
9499
}
95100

101+
public Builder withEvictIdleConnections(long maxIdleTime, TimeUnit maxIdleTimeUnit) {
102+
this.evictConnectionIdleTimePeriod = maxIdleTime;
103+
this.evictConnectionIdleTimeUnit = maxIdleTimeUnit;
104+
return this;
105+
}
106+
96107
public OptimizelyHttpClient build() {
97108
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
98109
poolingHttpClientConnectionManager.setMaxTotal(maxTotalConnections);
99110
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
100111
poolingHttpClientConnectionManager.setValidateAfterInactivity(validateAfterInactivity);
101112

102-
CloseableHttpClient closableHttpClient = HttpClients.custom()
113+
HttpClientBuilder builder = HttpClients.custom()
103114
.setDefaultRequestConfig(HttpClientUtils.DEFAULT_REQUEST_CONFIG)
104115
.setConnectionManager(poolingHttpClientConnectionManager)
105116
.disableCookieManagement()
106-
.useSystemProperties()
107-
.build();
117+
.useSystemProperties();
118+
119+
if (evictConnectionIdleTimePeriod > 0) {
120+
builder.evictIdleConnections(evictConnectionIdleTimePeriod, evictConnectionIdleTimeUnit);
121+
}
122+
123+
CloseableHttpClient closableHttpClient = builder.build();
108124

109125
return new OptimizelyHttpClient(closableHttpClient);
110126
}

core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.http.client.ClientProtocolException;
2828
import org.apache.http.client.methods.CloseableHttpResponse;
2929
import org.apache.http.client.methods.HttpGet;
30+
import org.apache.http.impl.client.CloseableHttpClient;
3031
import org.apache.http.util.EntityUtils;
3132
import org.slf4j.Logger;
3233
import org.slf4j.LoggerFactory;
@@ -46,13 +47,17 @@ public class HttpProjectConfigManager extends PollingProjectConfigManager {
4647
public static final String CONFIG_POLLING_UNIT = "http.project.config.manager.polling.unit";
4748
public static final String CONFIG_BLOCKING_DURATION = "http.project.config.manager.blocking.duration";
4849
public static final String CONFIG_BLOCKING_UNIT = "http.project.config.manager.blocking.unit";
50+
public static final String CONFIG_EVICT_DURATION = "http.project.config.manager.evict.duration";
51+
public static final String CONFIG_EVICT_UNIT = "http.project.config.manager.evict.unit";
4952
public static final String CONFIG_SDK_KEY = "http.project.config.manager.sdk.key";
5053
public static final String CONFIG_DATAFILE_AUTH_TOKEN = "http.project.config.manager.datafile.auth.token";
5154

5255
public static final long DEFAULT_POLLING_DURATION = 5;
5356
public static final TimeUnit DEFAULT_POLLING_UNIT = TimeUnit.MINUTES;
5457
public static final long DEFAULT_BLOCKING_DURATION = 10;
5558
public static final TimeUnit DEFAULT_BLOCKING_UNIT = TimeUnit.SECONDS;
59+
public static final long DEFAULT_EVICT_DURATION = 1;
60+
public static final TimeUnit DEFAULT_EVICT_UNIT = TimeUnit.MINUTES;
5661

5762
private static final Logger logger = LoggerFactory.getLogger(HttpProjectConfigManager.class);
5863

@@ -178,6 +183,10 @@ public static class Builder {
178183
long blockingTimeoutPeriod = PropertyUtils.getLong(CONFIG_BLOCKING_DURATION, DEFAULT_BLOCKING_DURATION);
179184
TimeUnit blockingTimeoutUnit = PropertyUtils.getEnum(CONFIG_BLOCKING_UNIT, TimeUnit.class, DEFAULT_BLOCKING_UNIT);
180185

186+
// force-close the persistent connection after this idle time
187+
long evictConnectionIdleTimePeriod = PropertyUtils.getLong(CONFIG_EVICT_DURATION, DEFAULT_EVICT_DURATION);
188+
TimeUnit evictConnectionIdleTimeUnit = PropertyUtils.getEnum(CONFIG_EVICT_UNIT, TimeUnit.class, DEFAULT_EVICT_UNIT);
189+
181190
public Builder withDatafile(String datafile) {
182191
this.datafile = datafile;
183192
return this;
@@ -208,6 +217,25 @@ public Builder withOptimizelyHttpClient(OptimizelyHttpClient httpClient) {
208217
return this;
209218
}
210219

220+
/**
221+
* Makes HttpClient proactively evict idle connections from theœ
222+
* connection pool using a background thread.
223+
*
224+
* @see org.apache.http.impl.client.HttpClientBuilder#evictIdleConnections(long, TimeUnit)
225+
*
226+
* @param maxIdleTime maximum time persistent connections can stay idle while kept alive
227+
* in the connection pool. Connections whose inactivity period exceeds this value will
228+
* get closed and evicted from the pool. Set to 0 to disable eviction.
229+
* @param maxIdleTimeUnit time unit for the above parameter.
230+
*
231+
* @return A HttpProjectConfigManager builder
232+
*/
233+
public Builder withEvictIdleConnections(long maxIdleTime, TimeUnit maxIdleTimeUnit) {
234+
this.evictConnectionIdleTimePeriod = maxIdleTime;
235+
this.evictConnectionIdleTimeUnit = maxIdleTimeUnit;
236+
return this;
237+
}
238+
211239
/**
212240
* Configure time to block before Completing the future. This timeout is used on the first call
213241
* to {@link PollingProjectConfigManager#getConfig()}. If the timeout is exceeded then the
@@ -300,7 +328,9 @@ public HttpProjectConfigManager build(boolean defer) {
300328
}
301329

302330
if (httpClient == null) {
303-
httpClient = HttpClientUtils.getDefaultHttpClient();
331+
httpClient = OptimizelyHttpClient.builder()
332+
.withEvictIdleConnections(evictConnectionIdleTimePeriod, evictConnectionIdleTimeUnit)
333+
.build();
304334
}
305335

306336
if (url == null) {

core-httpclient-impl/src/test/java/com/optimizely/ab/OptimizelyFactoryTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public void setUp() {
4545
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_POLLING_UNIT);
4646
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_BLOCKING_DURATION);
4747
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_BLOCKING_UNIT);
48+
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_EVICT_DURATION);
49+
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_EVICT_UNIT);
4850
PropertyUtils.clear(HttpProjectConfigManager.CONFIG_SDK_KEY);
4951
}
5052

@@ -152,6 +154,27 @@ public void setInvalidBlockingTimeout() {
152154
assertNull(PropertyUtils.getEnum(HttpProjectConfigManager.CONFIG_POLLING_UNIT, TimeUnit.class));
153155
}
154156

157+
@Test
158+
public void setEvictIdleConnections() {
159+
Long duration = 2000L;
160+
TimeUnit timeUnit = TimeUnit.SECONDS;
161+
OptimizelyFactory.setEvictIdleConnections(duration, timeUnit);
162+
163+
assertEquals(duration, PropertyUtils.getLong(HttpProjectConfigManager.CONFIG_EVICT_DURATION));
164+
assertEquals(timeUnit, PropertyUtils.getEnum(HttpProjectConfigManager.CONFIG_EVICT_UNIT, TimeUnit.class));
165+
}
166+
167+
@Test
168+
public void setInvalidEvictIdleConnections() {
169+
OptimizelyFactory.setEvictIdleConnections(-1, TimeUnit.MICROSECONDS);
170+
assertNull(PropertyUtils.getLong(HttpProjectConfigManager.CONFIG_EVICT_DURATION));
171+
assertNull(PropertyUtils.getEnum(HttpProjectConfigManager.CONFIG_EVICT_UNIT, TimeUnit.class));
172+
173+
OptimizelyFactory.setEvictIdleConnections(10, null);
174+
assertNull(PropertyUtils.getLong(HttpProjectConfigManager.CONFIG_EVICT_DURATION));
175+
assertNull(PropertyUtils.getEnum(HttpProjectConfigManager.CONFIG_EVICT_UNIT, TimeUnit.class));
176+
}
177+
155178
@Test
156179
public void setSdkKey() {
157180
String expected = "sdk-key";

core-httpclient-impl/src/test/java/com/optimizely/ab/OptimizelyHttpClientTest.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
import org.junit.Test;
2828

2929
import java.io.IOException;
30+
import java.util.concurrent.TimeUnit;
3031

32+
import static com.optimizely.ab.OptimizelyHttpClient.builder;
33+
import static java.util.concurrent.TimeUnit.*;
3134
import static org.junit.Assert.*;
3235
import static org.mockito.Mockito.mock;
3336
import static org.mockito.Mockito.when;
@@ -46,24 +49,39 @@ public void tearDown() {
4649

4750
@Test
4851
public void testDefaultConfiguration() {
49-
OptimizelyHttpClient optimizelyHttpClient = OptimizelyHttpClient.builder().build();
52+
OptimizelyHttpClient optimizelyHttpClient = builder().build();
5053
assertTrue(optimizelyHttpClient.getHttpClient() instanceof CloseableHttpClient);
5154
}
5255

5356
@Test
5457
public void testNonDefaultConfiguration() {
55-
OptimizelyHttpClient optimizelyHttpClient = OptimizelyHttpClient.builder()
58+
OptimizelyHttpClient optimizelyHttpClient = builder()
5659
.withValidateAfterInactivity(1)
5760
.withMaxPerRoute(2)
5861
.withMaxTotalConnections(3)
62+
.withEvictIdleConnections(5, MINUTES)
5963
.build();
6064

6165
assertTrue(optimizelyHttpClient.getHttpClient() instanceof CloseableHttpClient);
6266
}
6367

68+
@Test
69+
public void testEvictTime() {
70+
OptimizelyHttpClient.Builder builder = builder();
71+
long expectedPeriod = builder.evictConnectionIdleTimePeriod;
72+
TimeUnit expectedTimeUnit = builder.evictConnectionIdleTimeUnit;
73+
74+
assertEquals(expectedPeriod, 0L);
75+
assertEquals(expectedTimeUnit, MILLISECONDS);
76+
77+
builder.withEvictIdleConnections(10L, SECONDS);
78+
assertEquals(10, builder.evictConnectionIdleTimePeriod);
79+
assertEquals(SECONDS, builder.evictConnectionIdleTimeUnit);
80+
}
81+
6482
@Test(expected = HttpHostConnectException.class)
6583
public void testProxySettings() throws IOException {
66-
OptimizelyHttpClient optimizelyHttpClient = OptimizelyHttpClient.builder().build();
84+
OptimizelyHttpClient optimizelyHttpClient = builder().build();
6785

6886
// If this request succeeds then the proxy config was not picked up.
6987
HttpGet get = new HttpGet("https://www.optimizely.com");

core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
import static com.optimizely.ab.config.HttpProjectConfigManager.*;
4444
import static java.util.concurrent.TimeUnit.SECONDS;
45+
import static java.util.concurrent.TimeUnit.MINUTES;
4546
import static org.junit.Assert.*;
4647
import static org.mockito.Matchers.any;
4748
import static org.mockito.Mockito.*;
@@ -254,6 +255,20 @@ public void testInvalidBlockingTimeout() {
254255
assertEquals(SECONDS, builder.blockingTimeoutUnit);
255256
}
256257

258+
@Test
259+
public void testEvictTime() {
260+
Builder builder = builder();
261+
long expectedPeriod = builder.evictConnectionIdleTimePeriod;
262+
TimeUnit expectedTimeUnit = builder.evictConnectionIdleTimeUnit;
263+
264+
assertEquals(expectedPeriod, 1L);
265+
assertEquals(expectedTimeUnit, MINUTES);
266+
267+
builder.withEvictIdleConnections(10L, SECONDS);
268+
assertEquals(10, builder.evictConnectionIdleTimePeriod);
269+
assertEquals(SECONDS, builder.evictConnectionIdleTimeUnit);
270+
}
271+
257272
@Test
258273
@Ignore
259274
public void testGetDatafileHttpResponse2XX() throws Exception {

0 commit comments

Comments
 (0)