Skip to content

Commit c7bc379

Browse files
committed
reduce request drops with smaller validation time and retries
1 parent 8747cb7 commit c7bc379

File tree

6 files changed

+96
-91
lines changed

6 files changed

+96
-91
lines changed

core-httpclient-impl/README.md

+14-14
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,24 @@ The number of workers determines the number of threads the thread pool uses.
106106
### Builder Methods
107107
The following builder methods can be used to custom configure the `AsyncEventHandler`.
108108

109-
|Method Name|Default Value| Description |
110-
|---|---|----------------------------------------------------|
111-
|`withQueueCapacity(int)`|10000| Queue size for pending logEvents |
112-
|`withNumWorkers(int)`|2| Number of worker threads |
113-
|`withMaxTotalConnections(int)`|200| Maximum number of connections |
114-
|`withMaxPerRoute(int)`|20| Maximum number of connections per route |
115-
|`withValidateAfterInactivity(int)`|5000| Time to maintain idle connections (in milliseconds) |
109+
|Method Name|Default Value|Description|
110+
|---|---|-----------------------------------------------|
111+
|`withQueueCapacity(int)`|10000|Queue size for pending logEvents|
112+
|`withNumWorkers(int)`|2|Number of worker threads|
113+
|`withMaxTotalConnections(int)`|200|Maximum number of connections|
114+
|`withMaxPerRoute(int)`|20|Maximum number of connections per route|
115+
|`withValidateAfterInactivity(int)`|5000|Time to maintain idle connections (in milliseconds)|
116116

117117
### Advanced configuration
118118
The following properties can be set to override the default configuration.
119119

120-
|Property Name|Default Value| Description |
121-
|---|---|----------------------------------------------------|
122-
|**async.event.handler.queue.capacity**|10000| Queue size for pending logEvents |
123-
|**async.event.handler.num.workers**|2| Number of worker threads |
124-
|**async.event.handler.max.connections**|200| Maximum number of connections |
125-
|**async.event.handler.event.max.per.route**|20| Maximum number of connections per route |
126-
|**async.event.handler.validate.after**|5000| Time to maintain idle connections (in milliseconds) |
120+
|Property Name|Default Value|Description|
121+
|---|---|-----------------------------------------------|
122+
|**async.event.handler.queue.capacity**|10000|Queue size for pending logEvents|
123+
|**async.event.handler.num.workers**|2|Number of worker threads|
124+
|**async.event.handler.max.connections**|200|Maximum number of connections|
125+
|**async.event.handler.event.max.per.route**|20|Maximum number of connections per route|
126+
|**async.event.handler.validate.after**|5000|Time to maintain idle connections (in milliseconds)|
127127

128128
## HttpProjectConfigManager
129129

core-httpclient-impl/build.gradle

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
dependencies {
2-
compile project(':core-api')
3-
2+
implementation project(':core-api')
43
compileOnly group: 'com.google.code.gson', name: 'gson', version: gsonVersion
4+
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: httpClientVersion
55

6-
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: httpClientVersion
7-
8-
testImplementation 'io.javalin:javalin:3.13.10'
9-
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
10-
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
11-
testImplementation 'io.rest-assured:rest-assured:4.4.0'
6+
testImplementation 'org.mock-server:mockserver-netty:5.15.0'
127
}
138

149
task exhaustiveTest {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public final class HttpClientUtils {
2626
public static final int CONNECTION_TIMEOUT_MS = 10000;
2727
public static final int CONNECTION_REQUEST_TIMEOUT_MS = 5000;
2828
public static final int SOCKET_TIMEOUT_MS = 10000;
29-
public static final int DEFAULT_VALIDATE_AFTER_INACTIVITY = 5000;
29+
public static final int DEFAULT_VALIDATE_AFTER_INACTIVITY = 1000;
3030
public static final int DEFAULT_MAX_CONNECTIONS = 200;
3131
public static final int DEFAULT_MAX_PER_ROUTE = 20;
3232
private static RequestConfig requestConfigWithTimeout;

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

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public static class Builder {
8181
// The maximum number of connections allowed for a route
8282
int maxPerRoute = HttpClientUtils.DEFAULT_MAX_PER_ROUTE;
8383
// Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer.
84+
// If this is too long, it's expected to see more requests dropped on staled connections (dropped by the server or networks).
85+
// We can configure retries (POST for AsyncEventDispatcher) to cover the staled connections.
8486
int validateAfterInactivity = HttpClientUtils.DEFAULT_VALIDATE_AFTER_INACTIVITY;
8587
// force-close the connection after this idle time (with 0, eviction is disabled by default)
8688
long evictConnectionIdleTimePeriod = 0;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public AsyncEventHandler(int queueCapacity,
143143
.withValidateAfterInactivity(validateAfter)
144144
// infrequent event discards observed. staled connections force-closed after a long idle time.
145145
.withEvictIdleConnections(1L, TimeUnit.MINUTES)
146-
// enable retry on POST
146+
// enable retry on event POST (default: retry on GET only)
147147
.withRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
148148
.build();
149149
}

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

+75-67
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,37 @@
1616
*/
1717
package com.optimizely.ab;
1818

19-
import io.javalin.Javalin;
20-
import org.apache.http.NoHttpResponseException;
19+
import org.apache.http.HttpException;
2120
import org.apache.http.client.HttpRequestRetryHandler;
2221
import org.apache.http.client.ResponseHandler;
22+
import org.apache.http.client.methods.CloseableHttpResponse;
2323
import org.apache.http.client.methods.HttpGet;
2424
import org.apache.http.client.methods.HttpUriRequest;
2525
import org.apache.http.client.methods.RequestBuilder;
2626
import org.apache.http.conn.HttpHostConnectException;
2727
import org.apache.http.impl.client.CloseableHttpClient;
2828
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
29+
import org.apache.http.protocol.HttpContext;
2930
import org.junit.*;
31+
import org.mockserver.integration.ClientAndServer;
32+
import org.mockserver.model.ConnectionOptions;
33+
import org.mockserver.model.HttpError;
34+
import org.mockserver.model.HttpRequest;
35+
import org.mockserver.model.HttpResponse;
3036

3137
import java.io.IOException;
38+
import java.util.concurrent.ExecutionException;
3239
import java.util.concurrent.TimeUnit;
33-
import java.util.concurrent.atomic.AtomicInteger;
3440

3541
import static com.optimizely.ab.OptimizelyHttpClient.builder;
3642
import static java.util.concurrent.TimeUnit.*;
3743
import static org.junit.Assert.*;
38-
import static org.mockito.Matchers.any;
3944
import static org.mockito.Mockito.*;
45+
import static org.mockserver.model.HttpForward.forward;
46+
import static org.mockserver.model.HttpRequest.request;
47+
import static org.mockserver.model.HttpResponse.*;
48+
import static org.mockserver.model.HttpResponse.response;
49+
import static org.mockserver.verify.VerificationTimes.exactly;
4050

4151
public class OptimizelyHttpClientTest {
4252
@Before
@@ -111,74 +121,72 @@ public void testExecute() throws IOException {
111121
}
112122

113123
@Test
114-
public void testRetries() throws IOException {
115-
// Javalin intercepts before proxy, so host and port should be set correct here
116-
String host = "http://localhost";
117-
int port = 8000;
118-
Javalin app = Javalin.create().start(port);
119-
int maxFailures = 2;
120-
121-
AtomicInteger callTimes = new AtomicInteger();
122-
app.get("/", ctx -> {
123-
callTimes.addAndGet(1);
124-
int count = callTimes.get();
125-
if (count < maxFailures) {
126-
throw new NoHttpResponseException("TESTING CONNECTION FAILURE");
127-
} else {
128-
ctx.status(200).result("Success");
129-
}
130-
});
124+
public void testRetriesWithCustomRetryHandler() throws IOException {
131125

132-
OptimizelyHttpClient optimizelyHttpClient = spy(OptimizelyHttpClient.builder().build());
133-
optimizelyHttpClient.execute(new HttpGet(host + ":" + String.valueOf(port)));
134-
assertEquals(3, callTimes.get());
135-
}
126+
// [NOTE] Request retries are all handled inside HttpClient. Not easy for unit test.
127+
// - "DefaultHttpRetryHandler" in HttpClient retries only with special types of Exceptions
128+
// like "NoHttpResponseException", etc.
129+
// Other exceptions (SocketTimeout, ProtocolException, etc.) all ignored.
130+
// - Not easy to force the specific exception type in the low-level.
131+
// - This test just validates custom retry handler injected ok by validating the number of retries.
136132

137-
@Test
138-
public void testRetriesWithCustom() throws IOException {
139-
// Javalin intercepts before proxy, so host and port should be set correct here
140-
String host = "http://localhost";
141-
int port = 8000;
142-
Javalin app = Javalin.create().start(port);
143-
int maxFailures = 2;
144-
145-
AtomicInteger callTimes = new AtomicInteger();
146-
app.get("/", ctx -> {
147-
callTimes.addAndGet(1);
148-
int count = callTimes.get();
149-
if (count < maxFailures) {
150-
// throw new NoHttpResponseException("TESTING CONNECTION FAILURE");
151-
throw new IOException("TESTING CONNECTION FAILURE");
152-
153-
// ctx.status(500).result("TESTING Server Error");
154-
} else {
155-
ctx.status(200).result("Success");
133+
class CustomRetryHandler implements HttpRequestRetryHandler {
134+
private final int maxRetries;
135+
136+
public CustomRetryHandler(int maxRetries) {
137+
this.maxRetries = maxRetries;
156138
}
157-
});
158139

159-
HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(3, true);
160-
OptimizelyHttpClient optimizelyHttpClient = spy(OptimizelyHttpClient.builder().withRetryHandler(retryHandler).build());
140+
@Override
141+
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
142+
// override to retry for any type of exceptions
143+
return executionCount < maxRetries;
144+
}
145+
}
161146

162-
optimizelyHttpClient.execute(new HttpGet(host + ":" + String.valueOf(port)));
163-
assertEquals(3, callTimes.get());
164-
}
147+
int port = 9999;
148+
ClientAndServer mockServer;
149+
int retryCount;
165150

166-
//
167-
// @Test
168-
// public void testRetriesWithCustom() throws IOException {
169-
// CloseableHttpClient httpClient = mock(CloseableHttpClient.class);
170-
// CloseableHttpResponse response = mock(CloseableHttpResponse.class);
171-
//
172-
// HttpRequestRetryHandler mockRetryHandler = spy(new DefaultHttpRequestRetryHandler(3, true));
173-
// when(mockRetryHandler.retryRequest(any(), any(), any())).thenReturn(true);
174-
//
175-
// OptimizelyHttpClient optimizelyHttpClient = OptimizelyHttpClient.builder().withRetryHandler(mockRetryHandler).build();
176-
// try {
177-
// optimizelyHttpClient.execute(new HttpGet("https://example.com"));
178-
// } catch(Exception e) {
179-
// assert(e instanceof IOException);
180-
// }
181-
// verify(mockRetryHandler, times(3)).retryRequest(any(), any(), any());
182-
// }
151+
// default httpclient (retries enabled by default, but no retry for timeout connection)
183152

153+
mockServer = ClientAndServer.startClientAndServer(port);
154+
mockServer
155+
.when(request().withMethod("GET").withPath("/"))
156+
.error(HttpError.error());
157+
158+
OptimizelyHttpClient clientDefault = OptimizelyHttpClient.builder()
159+
.setTimeoutMillis(100)
160+
.build();
161+
162+
try {
163+
clientDefault.execute(new HttpGet("http://localhost:" + port));
164+
fail();
165+
} catch (Exception e) {
166+
retryCount = mockServer.retrieveRecordedRequests(request()).length;
167+
assertEquals(1, retryCount);
168+
}
169+
mockServer.stop();
170+
171+
// httpclient with custom retry handler (5 times retries for any request)
172+
173+
mockServer = ClientAndServer.startClientAndServer(port);
174+
mockServer
175+
.when(request().withMethod("GET").withPath("/"))
176+
.error(HttpError.error());
177+
178+
OptimizelyHttpClient clientWithRetries = OptimizelyHttpClient.builder()
179+
.withRetryHandler(new CustomRetryHandler(5))
180+
.setTimeoutMillis(100)
181+
.build();
182+
183+
try {
184+
clientWithRetries.execute(new HttpGet("http://localhost:" + port));
185+
fail();
186+
} catch (Exception e) {
187+
retryCount = mockServer.retrieveRecordedRequests(request()).length;
188+
assertEquals(5, retryCount);
189+
}
190+
mockServer.stop();
191+
}
184192
}

0 commit comments

Comments
 (0)