diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
index 1e0e2ed809..ccfe9679d4 100644
--- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
+++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
@@ -198,6 +198,13 @@ public interface AsyncHttpClientConfig {
*/
CookieStore getCookieStore();
+ /**
+ * Return the delay in milliseconds to evict expired cookies from {@linkplain CookieStore}
+ *
+ * @return the delay in milliseconds to evict expired cookies from {@linkplain CookieStore}
+ */
+ int expiredCookieEvictionDelay();
+
/**
* Return the number of time the library will retry when an {@link java.io.IOException} is throw by the remote server
*
diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
index 18425801fc..0e97fcef79 100644
--- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
+++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
@@ -22,6 +22,7 @@
import io.netty.util.Timer;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.asynchttpclient.channel.ChannelPool;
+import org.asynchttpclient.cookie.CookieEvictionTask;
import org.asynchttpclient.filter.FilterContext;
import org.asynchttpclient.filter.FilterException;
import org.asynchttpclient.filter.RequestFilter;
@@ -90,6 +91,22 @@ public DefaultAsyncHttpClient(AsyncHttpClientConfig config) {
channelManager = new ChannelManager(config, nettyTimer);
requestSender = new NettyRequestSender(config, channelManager, nettyTimer, new AsyncHttpClientState(closed));
channelManager.configureBootstraps(requestSender);
+ boolean scheduleCookieEviction = false;
+
+ final int cookieStoreCount = config.getCookieStore().incrementAndGet();
+ if (!allowStopNettyTimer) {
+ if (cookieStoreCount == 1) {
+ // If this is the first AHC instance for the shared (user-provided) netty timer.
+ scheduleCookieEviction = true;
+ }
+ } else {
+ // If Timer is not shared.
+ scheduleCookieEviction = true;
+ }
+ if (scheduleCookieEviction) {
+ nettyTimer.newTimeout(new CookieEvictionTask(config.expiredCookieEvictionDelay(), config.getCookieStore()),
+ config.expiredCookieEvictionDelay(), TimeUnit.MILLISECONDS);
+ }
}
private Timer newNettyTimer(AsyncHttpClientConfig config) {
diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
index daf043356a..0f4e62c560 100644
--- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
+++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
@@ -109,6 +109,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
// cookie store
private final CookieStore cookieStore;
+ private final int expiredCookieEvictionDelay;
// internals
private final String threadPoolName;
@@ -192,6 +193,7 @@ private DefaultAsyncHttpClientConfig(// http
// cookie store
CookieStore cookieStore,
+ int expiredCookieEvictionDelay,
// tuning
boolean tcpNoDelay,
@@ -283,6 +285,7 @@ private DefaultAsyncHttpClientConfig(// http
// cookie store
this.cookieStore = cookieStore;
+ this.expiredCookieEvictionDelay = expiredCookieEvictionDelay;
// tuning
this.tcpNoDelay = tcpNoDelay;
@@ -558,6 +561,11 @@ public CookieStore getCookieStore() {
return cookieStore;
}
+ @Override
+ public int expiredCookieEvictionDelay() {
+ return expiredCookieEvictionDelay;
+ }
+
// tuning
@Override
public boolean isTcpNoDelay() {
@@ -746,6 +754,7 @@ public static class Builder {
// cookie store
private CookieStore cookieStore = new ThreadSafeCookieStore();
+ private int expiredCookieEvictionDelay = defaultExpiredCookieEvictionDelay();
// tuning
private boolean tcpNoDelay = defaultTcpNoDelay();
@@ -1146,6 +1155,11 @@ public Builder setCookieStore(CookieStore cookieStore) {
return this;
}
+ public Builder setExpiredCookieEvictionDelay(int expiredCookieEvictionDelay) {
+ this.expiredCookieEvictionDelay = expiredCookieEvictionDelay;
+ return this;
+ }
+
// tuning
public Builder setTcpNoDelay(boolean tcpNoDelay) {
this.tcpNoDelay = tcpNoDelay;
@@ -1330,6 +1344,7 @@ public DefaultAsyncHttpClientConfig build() {
responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters),
ioExceptionFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ioExceptionFilters),
cookieStore,
+ expiredCookieEvictionDelay,
tcpNoDelay,
soReuseAddress,
soKeepAlive,
diff --git a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java
index 641c37d538..14dcec3bfd 100644
--- a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java
+++ b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java
@@ -73,6 +73,7 @@ public final class AsyncHttpClientConfigDefaults {
public static final String IO_THREADS_COUNT_CONFIG = "ioThreadsCount";
public static final String HASHED_WHEEL_TIMER_TICK_DURATION = "hashedWheelTimerTickDuration";
public static final String HASHED_WHEEL_TIMER_SIZE = "hashedWheelTimerSize";
+ public static final String EXPIRED_COOKIE_EVICTION_DELAY = "expiredCookieEvictionDelay";
public static final String AHC_VERSION;
@@ -304,4 +305,8 @@ public static int defaultHashedWheelTimerTickDuration() {
public static int defaultHashedWheelTimerSize() {
return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HASHED_WHEEL_TIMER_SIZE);
}
+
+ public static int defaultExpiredCookieEvictionDelay() {
+ return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + EXPIRED_COOKIE_EVICTION_DELAY);
+ }
}
diff --git a/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java b/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java
new file mode 100644
index 0000000000..b5ce4aed0a
--- /dev/null
+++ b/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java
@@ -0,0 +1,30 @@
+package org.asynchttpclient.cookie;
+
+import java.util.concurrent.TimeUnit;
+
+import org.asynchttpclient.AsyncHttpClientConfig;
+
+import io.netty.util.Timeout;
+import io.netty.util.TimerTask;
+
+/**
+ * Evicts expired cookies from the {@linkplain CookieStore} periodically.
+ * The default delay is 30 seconds. You may override the default using
+ * {@linkplain AsyncHttpClientConfig#expiredCookieEvictionDelay()}.
+ */
+public class CookieEvictionTask implements TimerTask {
+
+ private final long evictDelayInMs;
+ private final CookieStore cookieStore;
+
+ public CookieEvictionTask(long evictDelayInMs, CookieStore cookieStore) {
+ this.evictDelayInMs = evictDelayInMs;
+ this.cookieStore = cookieStore;
+ }
+
+ @Override
+ public void run(Timeout timeout) throws Exception {
+ cookieStore.evictExpired();
+ timeout.timer().newTimeout(this, evictDelayInMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java
index 0c5ad544ed..6cd540226c 100644
--- a/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java
+++ b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java
@@ -16,6 +16,7 @@
import io.netty.handler.codec.http.cookie.Cookie;
import org.asynchttpclient.uri.Uri;
+import org.asynchttpclient.util.Counted;
import java.net.CookieManager;
import java.util.List;
@@ -31,10 +32,10 @@
*
* @since 2.1
*/
-public interface CookieStore {
+public interface CookieStore extends Counted {
/**
* Adds one {@link Cookie} to the store. This is called for every incoming HTTP response.
- * If the given cookie has already expired it will not be added, but existing values will still be removed.
+ * If the given cookie has already expired it will not be added.
*
*
A cookie to store may or may not be associated with an URI. If it
* is not associated with an URI, the cookie's domain and path attribute
@@ -82,4 +83,9 @@ public interface CookieStore {
* @return true if any cookies were purged.
*/
boolean clear();
+
+ /**
+ * Evicts all the cookies that expired as of the time this method is run.
+ */
+ void evictExpired();
}
diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java
index 277db387ce..8cdc29f45e 100644
--- a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java
+++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java
@@ -21,12 +21,14 @@
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class ThreadSafeCookieStore implements CookieStore {
- private Map cookieJar = new ConcurrentHashMap<>();
+ private final Map> cookieJar = new ConcurrentHashMap<>();
+ private final AtomicInteger counter = new AtomicInteger();
@Override
public void add(Uri uri, Cookie cookie) {
@@ -43,28 +45,29 @@ public List get(Uri uri) {
@Override
public List getAll() {
- final boolean[] removeExpired = {false};
List result = cookieJar
- .entrySet()
+ .values()
.stream()
- .filter(pair -> {
- boolean hasCookieExpired = hasCookieExpired(pair.getValue().cookie, pair.getValue().createdAt);
- if (hasCookieExpired && !removeExpired[0])
- removeExpired[0] = true;
- return !hasCookieExpired;
- })
- .map(pair -> pair.getValue().cookie)
+ .flatMap(map -> map.values().stream())
+ .filter(pair -> !hasCookieExpired(pair.cookie, pair.createdAt))
+ .map(pair -> pair.cookie)
.collect(Collectors.toList());
- if (removeExpired[0])
- removeExpired();
-
return result;
}
@Override
public boolean remove(Predicate predicate) {
- return cookieJar.entrySet().removeIf(v -> predicate.test(v.getValue().cookie));
+ final boolean[] removed = {false};
+ cookieJar.forEach((key, value) -> {
+ if (!removed[0]) {
+ removed[0] = value.entrySet().removeIf(v -> predicate.test(v.getValue().cookie));
+ }
+ });
+ if (removed[0]) {
+ cookieJar.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty());
+ }
+ return removed[0];
}
@Override
@@ -74,8 +77,33 @@ public boolean clear() {
return result;
}
+ @Override
+ public void evictExpired() {
+ removeExpired();
+ }
+
+
+ @Override
+ public int incrementAndGet() {
+ return counter.incrementAndGet();
+ }
+
+ @Override
+ public int decrementAndGet() {
+ return counter.decrementAndGet();
+ }
+
+ @Override
+ public int count() {
+ return counter.get();
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ public Map> getUnderlying() {
+ return new HashMap<>(cookieJar);
+ }
+
private String requestDomain(Uri requestUri) {
return requestUri.getHost().toLowerCase();
}
@@ -126,13 +154,6 @@ private boolean hasCookieExpired(Cookie cookie, long whenCreated) {
return false;
}
- // rfc6265#section-5.1.3
- // check "The string is a host name (i.e., not an IP address)" ignored
- private boolean domainsMatch(String cookieDomain, String requestDomain, boolean hostOnly) {
- return (hostOnly && Objects.equals(requestDomain, cookieDomain)) ||
- (Objects.equals(requestDomain, cookieDomain) || requestDomain.endsWith("." + cookieDomain));
- }
-
// rfc6265#section-5.1.4
private boolean pathsMatch(String cookiePath, String requestPath) {
return Objects.equals(cookiePath, requestPath) ||
@@ -140,50 +161,73 @@ private boolean pathsMatch(String cookiePath, String requestPath) {
}
private void add(String requestDomain, String requestPath, Cookie cookie) {
-
AbstractMap.SimpleEntry pair = cookieDomain(cookie.domain(), requestDomain);
String keyDomain = pair.getKey();
boolean hostOnly = pair.getValue();
String keyPath = cookiePath(cookie.path(), requestPath);
- CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyDomain, keyPath);
+ CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath);
if (hasCookieExpired(cookie, 0))
- cookieJar.remove(key);
- else
- cookieJar.put(key, new StoredCookie(cookie, hostOnly, cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE));
+ cookieJar.getOrDefault(keyDomain, Collections.emptyMap()).remove(key);
+ else {
+ final Map innerMap = cookieJar.computeIfAbsent(keyDomain, domain -> new ConcurrentHashMap<>());
+ innerMap.put(key, new StoredCookie(cookie, hostOnly, cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE));
+ }
}
private List get(String domain, String path, boolean secure) {
+ boolean exactDomainMatch = true;
+ String subDomain = domain;
+ List results = null;
+
+ while (MiscUtils.isNonEmpty(subDomain)) {
+ final List storedCookies = getStoredCookies(subDomain, path, secure, exactDomainMatch);
+ subDomain = DomainUtils.getSubDomain(subDomain);
+ exactDomainMatch = false;
+ if (storedCookies.isEmpty()) {
+ continue;
+ }
+ if (results == null) {
+ results = new ArrayList<>(4);
+ }
+ results.addAll(storedCookies);
+ }
- final boolean[] removeExpired = {false};
+ return results == null ? Collections.emptyList() : results;
+ }
- List result = cookieJar.entrySet().stream().filter(pair -> {
+ private List getStoredCookies(String domain, String path, boolean secure, boolean isExactMatch) {
+ final Map innerMap = cookieJar.get(domain);
+ if (innerMap == null) {
+ return Collections.emptyList();
+ }
+
+ return innerMap.entrySet().stream().filter(pair -> {
CookieKey key = pair.getKey();
StoredCookie storedCookie = pair.getValue();
boolean hasCookieExpired = hasCookieExpired(storedCookie.cookie, storedCookie.createdAt);
- if (hasCookieExpired && !removeExpired[0])
- removeExpired[0] = true;
- return !hasCookieExpired && domainsMatch(key.domain, domain, storedCookie.hostOnly) && pathsMatch(key.path, path) && (secure || !storedCookie.cookie.isSecure());
+ return !hasCookieExpired &&
+ (isExactMatch || !storedCookie.hostOnly) &&
+ pathsMatch(key.path, path) &&
+ (secure || !storedCookie.cookie.isSecure());
}).map(v -> v.getValue().cookie).collect(Collectors.toList());
-
- if (removeExpired[0])
- removeExpired();
-
- return result;
}
private void removeExpired() {
- cookieJar.entrySet().removeIf(v -> hasCookieExpired(v.getValue().cookie, v.getValue().createdAt));
+ final boolean[] removed = {false};
+ cookieJar.values().forEach(cookieMap -> removed[0] |= cookieMap.entrySet().removeIf(
+ v -> hasCookieExpired(v.getValue().cookie, v.getValue().createdAt)));
+ if (removed[0]) {
+ cookieJar.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty());
+ }
}
private static class CookieKey implements Comparable {
final String name;
- final String domain;
final String path;
- CookieKey(String name, String domain, String path) {
+ CookieKey(String name, String path) {
this.name = name;
- this.domain = domain;
this.path = path;
}
@@ -192,7 +236,6 @@ public int compareTo(CookieKey o) {
Assertions.assertNotNull(o, "Parameter can't be null");
int result;
if ((result = this.name.compareTo(o.name)) == 0)
- if ((result = this.domain.compareTo(o.domain)) == 0)
result = this.path.compareTo(o.path);
return result;
@@ -207,14 +250,13 @@ public boolean equals(Object obj) {
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
- result = 31 * result + domain.hashCode();
result = 31 * result + path.hashCode();
return result;
}
@Override
public String toString() {
- return String.format("%s: %s; %s", name, domain, path);
+ return String.format("%s: %s", name, path);
}
}
@@ -235,4 +277,20 @@ public String toString() {
return String.format("%s; hostOnly %s; persistent %s", cookie.toString(), hostOnly, persistent);
}
}
+
+ public static final class DomainUtils {
+ private static final char DOT = '.';
+ public static String getSubDomain(String domain) {
+ if (domain == null || domain.isEmpty()) {
+ return null;
+ }
+ final int indexOfDot = domain.indexOf(DOT);
+ if (indexOfDot == -1) {
+ return null;
+ }
+ return domain.substring(indexOfDot + 1);
+ }
+
+ private DomainUtils() {}
+ }
}
diff --git a/client/src/main/java/org/asynchttpclient/util/Counted.java b/client/src/main/java/org/asynchttpclient/util/Counted.java
new file mode 100644
index 0000000000..b8791e2fea
--- /dev/null
+++ b/client/src/main/java/org/asynchttpclient/util/Counted.java
@@ -0,0 +1,23 @@
+package org.asynchttpclient.util;
+
+/**
+ * An interface that defines useful methods to check how many {@linkplain org.asynchttpclient.AsyncHttpClient}
+ * instances this particular implementation is shared with.
+ */
+public interface Counted {
+
+ /**
+ * Increment counter and return the incremented value
+ */
+ int incrementAndGet();
+
+ /**
+ * Decrement counter and return the decremented value
+ */
+ int decrementAndGet();
+
+ /**
+ * Return the current counter
+ */
+ int count();
+}
diff --git a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties
index 4c78250445..62bc177726 100644
--- a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties
+++ b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties
@@ -52,3 +52,4 @@ org.asynchttpclient.useNativeTransport=false
org.asynchttpclient.ioThreadsCount=0
org.asynchttpclient.hashedWheelTimerTickDuration=100
org.asynchttpclient.hashedWheelTimerSize=512
+org.asynchttpclient.expiredCookieEvictionDelay=30000
diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java
index e16a477c25..e248e9a0c4 100644
--- a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java
+++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java
@@ -17,6 +17,8 @@
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
+import io.netty.handler.codec.http.cookie.DefaultCookie;
+
import org.asynchttpclient.cookie.CookieStore;
import org.asynchttpclient.cookie.ThreadSafeCookieStore;
import org.asynchttpclient.uri.Uri;
@@ -26,10 +28,14 @@
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import java.util.Collection;
import java.util.List;
+import java.util.stream.Collectors;
import static org.testng.Assert.assertTrue;
+import com.google.common.collect.Sets;
+
public class CookieStoreTest {
private final Logger logger = LoggerFactory.getLogger(getClass());
@@ -46,7 +52,7 @@ public void tearDownGlobal() {
}
@Test
- public void runAllSequentiallyBecauseNotThreadSafe() {
+ public void runAllSequentiallyBecauseNotThreadSafe() throws Exception {
addCookieWithEmptyPath();
dontReturnCookieForAnotherDomain();
returnCookieWhenItWasSetOnSamePath();
@@ -77,6 +83,7 @@ public void runAllSequentiallyBecauseNotThreadSafe() {
shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme();
shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme();
shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme();
+ shouldCleanExpiredCookieFromUnderlyingDataStructure();
}
private void addCookieWithEmptyPath() {
@@ -284,8 +291,9 @@ private void returnMultipleCookiesEvenIfTheyHaveSameName() {
assertTrue(cookies1.size() == 2);
assertTrue(cookies1.stream().filter(c -> c.value().equals("FOO") || c.value().equals("BAR")).count() == 2);
- String result = ClientCookieEncoder.LAX.encode(cookies1.get(0), cookies1.get(1));
- assertTrue(result.equals("JSESSIONID=FOO; JSESSIONID=BAR"));
+ List encodedCookieStrings = cookies1.stream().map(ClientCookieEncoder.LAX::encode).collect(Collectors.toList());
+ assertTrue(encodedCookieStrings.contains("JSESSIONID=FOO"));
+ assertTrue(encodedCookieStrings.contains("JSESSIONID=BAR"));
}
// rfc6265#section-1 Cookies for a given host are shared across all the ports on that host
@@ -337,4 +345,26 @@ private void shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme() {
assertTrue(store.get(uri).get(0).value().equals("VALUE3"));
assertTrue(store.get(uri).get(0).isSecure());
}
+
+ private void shouldCleanExpiredCookieFromUnderlyingDataStructure() throws Exception {
+ ThreadSafeCookieStore store = new ThreadSafeCookieStore();
+ store.add(Uri.create("https://foo.org/moodle/"), getCookie("JSESSIONID", "FOO", 1));
+ store.add(Uri.create("https://bar.org/moodle/"), getCookie("JSESSIONID", "BAR", 1));
+ store.add(Uri.create("https://bar.org/moodle/"), new DefaultCookie("UNEXPIRED_BAR", "BAR"));
+ store.add(Uri.create("https://foobar.org/moodle/"), new DefaultCookie("UNEXPIRED_FOOBAR", "FOOBAR"));
+
+
+ assertTrue(store.getAll().size() == 4);
+ Thread.sleep(2000);
+ store.evictExpired();
+ assertTrue(store.getUnderlying().size() == 2);
+ Collection unexpiredCookieNames = store.getAll().stream().map(Cookie::name).collect(Collectors.toList());
+ assertTrue(unexpiredCookieNames.containsAll(Sets.newHashSet("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR")));
+ }
+
+ private static Cookie getCookie(String key, String value, int maxAge) {
+ DefaultCookie cookie = new DefaultCookie(key, value);
+ cookie.setMaxAge(maxAge);
+ return cookie;
+ }
}
diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java
new file mode 100644
index 0000000000..eadd41226a
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java
@@ -0,0 +1,81 @@
+package org.asynchttpclient;
+
+import io.netty.util.Timer;
+import org.asynchttpclient.DefaultAsyncHttpClientConfig.Builder;
+import org.asynchttpclient.cookie.CookieEvictionTask;
+import org.asynchttpclient.cookie.CookieStore;
+import org.asynchttpclient.cookie.ThreadSafeCookieStore;
+import org.testng.annotations.Test;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.assertEquals;
+
+public class DefaultAsyncHttpClientTest {
+
+ @Test
+ public void testWithSharedNettyTimerShouldScheduleCookieEvictionOnlyOnce() throws IOException {
+ final Timer nettyTimerMock = mock(Timer.class);
+ final CookieStore cookieStore = new ThreadSafeCookieStore();
+ final DefaultAsyncHttpClientConfig config = new Builder().setNettyTimer(nettyTimerMock).setCookieStore(cookieStore).build();
+ final AsyncHttpClient client1 = new DefaultAsyncHttpClient(config);
+ final AsyncHttpClient client2 = new DefaultAsyncHttpClient(config);
+
+ assertEquals(cookieStore.count(), 2);
+ verify(nettyTimerMock, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class));
+
+ closeSilently(client1);
+ closeSilently(client2);
+ }
+
+ @Test
+ public void testWitDefaultConfigShouldScheduleCookieEvictionForEachAHC() throws IOException {
+ final AsyncHttpClientConfig config1 = new DefaultAsyncHttpClientConfig.Builder().build();
+ final DefaultAsyncHttpClient client1 = new DefaultAsyncHttpClient(config1);
+
+ final AsyncHttpClientConfig config2 = new DefaultAsyncHttpClientConfig.Builder().build();
+ final DefaultAsyncHttpClient client2 = new DefaultAsyncHttpClient(config2);
+
+ assertEquals(config1.getCookieStore().count(), 1);
+ assertEquals(config2.getCookieStore().count(), 1);
+
+ closeSilently(client1);
+ closeSilently(client2);
+ }
+
+ @Test
+ public void testWithSharedCookieStoreButNonSharedTimerShouldScheduleCookieEvictionForFirstAHC() throws IOException {
+ final CookieStore cookieStore = new ThreadSafeCookieStore();
+ final Timer nettyTimerMock1 = mock(Timer.class);
+ final AsyncHttpClientConfig config1 = new DefaultAsyncHttpClientConfig.Builder()
+ .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock1).build();
+ final DefaultAsyncHttpClient client1 = new DefaultAsyncHttpClient(config1);
+
+ final Timer nettyTimerMock2 = mock(Timer.class);
+ final AsyncHttpClientConfig config2 = new DefaultAsyncHttpClientConfig.Builder()
+ .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock2).build();
+ final DefaultAsyncHttpClient client2 = new DefaultAsyncHttpClient(config2);
+
+ assertEquals(config1.getCookieStore().count(), 2);
+ verify(nettyTimerMock1, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class));
+ verify(nettyTimerMock2, never()).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class));
+
+ closeSilently(client1);
+ closeSilently(client2);
+ }
+
+ private static void closeSilently(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // swallow
+ }
+ }
+ }
+}
diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
index 38cd870289..fa5d87bcf3 100644
--- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
+++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java
@@ -164,6 +164,11 @@ public CookieStore getCookieStore() {
return new ThreadSafeCookieStore();
}
+ @Override
+ public int expiredCookieEvictionDelay() {
+ return getIntegerOpt(EXPIRED_COOKIE_EVICTION_DELAY).orElse(defaultExpiredCookieEvictionDelay());
+ }
+
@Override
public int getMaxRequestRetry() {
return getIntegerOpt(MAX_REQUEST_RETRY_CONFIG).orElse(defaultMaxRequestRetry());