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());