This repository has been archived by the owner on Jul 27, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 241
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix blocking query read timeout issue (#289)
* Decrease OkHTTP Read Timeout to 2 second Currently, the read timeout is the default value of okhttp i.e. 10 seconds. That is a very long duration when we consider testing. Decreasing it to 2 second should have limited to no impact. * Make ConsulCache AutoCloseable This is useful for test purposes. * Add tests for Key/Value cache The first test checks that the cache is notified at startup. The second test checks cache notifications for blocking queries of 1 and 10 seconds. Note that it is successful for 1 sec (<readTime) but failing for 10 sec. * Add timeout interceptor This "interceptor" is required to extract the wait query parameter from the url. Then, it adjusts the OkHttp read timeout accordingly. This feature requires OkHttp 3.9.0 which has been already added to retrofit2, but not yet release. * Activate read timeout auto adjustment feature * [pom] Set the version of OkHttp in properties
- Loading branch information
Showing
9 changed files
with
293 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
src/main/java/com/orbitz/consul/cache/TimeoutInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package com.orbitz.consul.cache; | ||
|
||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.base.Strings; | ||
import okhttp3.Interceptor; | ||
import okhttp3.Request; | ||
import okhttp3.Response; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.IOException; | ||
import java.time.Duration; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
public class TimeoutInterceptor implements Interceptor { | ||
|
||
private final static Logger LOGGER = LoggerFactory.getLogger(TimeoutInterceptor.class); | ||
|
||
private CacheConfig config; | ||
|
||
public TimeoutInterceptor() { | ||
this(CacheConfig.get()); | ||
} | ||
|
||
@VisibleForTesting | ||
TimeoutInterceptor(CacheConfig config) { | ||
this.config = config; | ||
} | ||
|
||
@Override | ||
public Response intercept(Chain chain) throws IOException { | ||
Request request = chain.request(); | ||
int readTimeout = chain.readTimeoutMillis(); | ||
|
||
if (config.isTimeoutAutoAdjustmentEnabled()) { | ||
String waitQuery = request.url().queryParameter("wait"); | ||
Duration waitDuration = parseWaitQuery(waitQuery); | ||
if (waitDuration != null) { | ||
int waitDurationMs = (int) waitDuration.toMillis(); | ||
int readTimeoutConfigMargin = (int) config.getTimeoutAutoAdjustmentMargin().toMillis(); | ||
|
||
// According to https://www.consul.io/api/index.html#blocking-queries | ||
// A small random amount of additional wait time is added to the supplied maximum wait time by consul | ||
// agent to spread out the wake up time of any concurrent requests. | ||
// This adds up to (wait / 16) additional time to the maximum duration. | ||
int readTimeoutRequiredMargin = (int) Math.ceil((double)(waitDurationMs) / 16); | ||
|
||
readTimeout = waitDurationMs + readTimeoutRequiredMargin + readTimeoutConfigMargin; | ||
} | ||
} | ||
|
||
return chain | ||
.withReadTimeout(readTimeout, TimeUnit.MILLISECONDS) | ||
.proceed(request); | ||
} | ||
|
||
private Duration parseWaitQuery(String query) { | ||
if (Strings.isNullOrEmpty(query)) { | ||
return null; | ||
} | ||
|
||
Duration wait = null; | ||
try { | ||
if (query.contains("m")) { | ||
wait = Duration.ofMinutes(Integer.valueOf(query.replace("m",""))); | ||
} else if (query.contains("s")) { | ||
wait = Duration.ofSeconds(Integer.valueOf(query.replace("s",""))); | ||
} | ||
} catch (Exception e) { | ||
LOGGER.warn(String.format("Error while extracting wait duration from query parameters: %s", query)); | ||
} | ||
return wait; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
com.orbitz.consul { | ||
cache.backOffDelay: 10 seconds | ||
cache.timeout.autoAdjustment { | ||
enable: true | ||
margin: 2 seconds | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
src/test/java/com/orbitz/consul/cache/TimeoutInterceptorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package com.orbitz.consul.cache; | ||
|
||
import junitparams.JUnitParamsRunner; | ||
import junitparams.Parameters; | ||
import junitparams.naming.TestCaseName; | ||
import okhttp3.Interceptor; | ||
import okhttp3.Request; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
import java.io.IOException; | ||
import java.time.Duration; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.mockito.Matchers.any; | ||
import static org.mockito.Matchers.anyInt; | ||
import static org.mockito.Mockito.*; | ||
|
||
@RunWith(JUnitParamsRunner.class) | ||
public class TimeoutInterceptorTest { | ||
|
||
@Test | ||
@Parameters(method = "getInterceptParameters") | ||
@TestCaseName("expected timeout of {4} ms for url {0} with timeout of {1} ms and margin of {3} ms (enabled: {2})") | ||
public void checkIntercept(String url, int defaultTimeout, boolean enabled, int margin, int expectedTimeoutMs) | ||
throws IOException { | ||
CacheConfig config = createConfigMock(enabled, margin); | ||
Interceptor.Chain chain = createChainMock(defaultTimeout, url); | ||
|
||
TimeoutInterceptor interceptor = new TimeoutInterceptor(config); | ||
interceptor.intercept(chain); | ||
verify(chain).withReadTimeout(eq(expectedTimeoutMs), eq(TimeUnit.MILLISECONDS)); | ||
} | ||
|
||
public Object getInterceptParameters() { | ||
return new Object[]{ | ||
// Auto Adjustment disabled | ||
new Object[]{"http://my_call", 1, false, 0, 1}, | ||
// Auto Adjustment disabled and valid "wait" query parameter | ||
new Object[]{"http://my_call?wait=1s", 1, false, 0, 1}, | ||
// Auto Adjustment enabled but not "wait" query parameter | ||
new Object[]{"http://my_call", 1, true, 0, 1}, | ||
new Object[]{"http://my_call", 1, true, 2, 1}, | ||
// Auto Adjustment enabled but invalid "wait" query parameter | ||
new Object[]{"http://my_call?wait=1", 1, true, 0, 1}, | ||
new Object[]{"http://my_call?wait=3h", 1, true, 2, 1}, | ||
// Auto Adjustment enabled and valid "wait" query parameter | ||
// Note: ceil(1/16*1000) = 63 and ceil(1/16*60000)=3750 | ||
new Object[]{"http://my_call?wait=1s", 1, true, 0, 1063}, | ||
new Object[]{"http://my_call?wait=1s", 0, true, 2, 1065}, | ||
new Object[]{"http://my_call?wait=1s", 1, true, 2, 1065}, | ||
new Object[]{"http://my_call?wait=1m", 1, true, 2, 63752}, | ||
}; | ||
} | ||
|
||
private CacheConfig createConfigMock(boolean autoAdjustEnabled, int autoAdjustMargin) { | ||
CacheConfig config = mock(CacheConfig.class); | ||
when(config.isTimeoutAutoAdjustmentEnabled()).thenReturn(autoAdjustEnabled); | ||
when(config.getTimeoutAutoAdjustmentMargin()).thenReturn(Duration.ofMillis(autoAdjustMargin)); | ||
return config; | ||
} | ||
|
||
private Interceptor.Chain createChainMock(int defaultTimeout, String url) throws IOException { | ||
Request request = new Request.Builder().url(url).build(); | ||
|
||
Interceptor.Chain chain = mock(Interceptor.Chain.class); | ||
when(chain.request()).thenReturn(request); | ||
when(chain.readTimeoutMillis()).thenReturn(defaultTimeout); | ||
doReturn(chain).when(chain).withReadTimeout(anyInt(), any(TimeUnit.class)); | ||
doReturn(null).when(chain).proceed(any(Request.class)); | ||
|
||
return chain; | ||
} | ||
} |