You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2018/01/07 11:54:02 UTC

[1/3] httpcomponents-client git commit: Decoupled SchedulingStrategy and its implementations from HTTP cache specific logic

Repository: httpcomponents-client
Updated Branches:
  refs/heads/master 1c7328098 -> 7cf4240b3


Decoupled SchedulingStrategy and its implementations from HTTP cache specific logic


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/0561bacc
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/0561bacc
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/0561bacc

Branch: refs/heads/master
Commit: 0561bacc6602f001d97eac6c67d009d67e9ede62
Parents: 1c73280
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Fri Jan 5 10:18:17 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Fri Jan 5 12:29:14 2018 +0100

----------------------------------------------------------------------
 .../http/impl/cache/AsynchronousValidator.java  |  44 +++--
 .../ExponentialBackOffSchedulingStrategy.java   | 166 ------------------
 .../impl/cache/ImmediateSchedulingStrategy.java |  89 ----------
 .../http/impl/cache/SchedulingStrategy.java     |  45 -----
 .../ExponentialBackOffSchedulingStrategy.java   | 118 +++++++++++++
 .../schedule/ImmediateSchedulingStrategy.java   |  49 ++++++
 .../http/schedule/SchedulingStrategy.java       |  43 +++++
 .../impl/cache/TestAsynchronousValidator.java   | 121 ++++++-------
 ...ExponentialBackingOffSchedulingStrategy.java | 175 -------------------
 .../cache/TestImmediateSchedulingStrategy.java  |  55 ------
 ...ExponentialBackingOffSchedulingStrategy.java |  59 +++++++
 .../TestImmediateSchedulingStrategy.java        |  51 ++++++
 12 files changed, 399 insertions(+), 616 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
index c17928a..c635747 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
@@ -32,11 +32,18 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 
 import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.classic.ExecChain;
+import org.apache.hc.client5.http.impl.schedule.ImmediateSchedulingStrategy;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -45,6 +52,8 @@ import org.apache.logging.log4j.Logger;
  * while-revalidate" directive is present
  */
 class AsynchronousValidator implements Closeable {
+
+    private final ScheduledExecutorService executorService;
     private final SchedulingStrategy schedulingStrategy;
     private final Set<String> queued;
     private final CacheKeyGenerator cacheKeyGenerator;
@@ -63,7 +72,7 @@ class AsynchronousValidator implements Closeable {
      * and {@link CacheConfig#getRevalidationQueueSize()}.
      */
     public AsynchronousValidator(final CacheConfig config) {
-        this(new ImmediateSchedulingStrategy(config));
+        this(new ScheduledThreadPoolExecutor(config.getAsynchronousWorkersCore()), new ImmediateSchedulingStrategy());
     }
 
     /**
@@ -73,18 +82,14 @@ class AsynchronousValidator implements Closeable {
      * @param schedulingStrategy used to maintain a pool of worker threads and
      *                           schedules when requests are executed
      */
-    AsynchronousValidator(final SchedulingStrategy schedulingStrategy) {
+    AsynchronousValidator(final ScheduledExecutorService executorService, final SchedulingStrategy schedulingStrategy) {
+        this.executorService = executorService;
         this.schedulingStrategy = schedulingStrategy;
         this.queued = new HashSet<>();
         this.cacheKeyGenerator = CacheKeyGenerator.INSTANCE;
         this.failureCache = new DefaultFailureCache();
     }
 
-    @Override
-    public void close() throws IOException {
-        schedulingStrategy.close();
-    }
-
     /**
      * Schedules an asynchronous revalidation
      */
@@ -96,23 +101,34 @@ class AsynchronousValidator implements Closeable {
             final ExecChain chain,
             final HttpCacheEntry entry) {
         // getVariantURI will fall back on getURI if no variants exist
-        final String uri = cacheKeyGenerator.generateVariantURI(target, request, entry);
+        final String cacheKey = cacheKeyGenerator.generateVariantURI(target, request, entry);
 
-        if (!queued.contains(uri)) {
-            final int consecutiveFailedAttempts = failureCache.getErrorCount(uri);
+        if (!queued.contains(cacheKey)) {
+            final int consecutiveFailedAttempts = failureCache.getErrorCount(cacheKey);
             final AsynchronousValidationRequest revalidationRequest =
                 new AsynchronousValidationRequest(
-                        this, cachingExec, target, request, scope, chain, entry, uri, consecutiveFailedAttempts);
+                        this, cachingExec, target, request, scope, chain, entry, cacheKey, consecutiveFailedAttempts);
 
+            final TimeValue executionTime = schedulingStrategy.schedule(consecutiveFailedAttempts);
             try {
-                schedulingStrategy.schedule(revalidationRequest);
-                queued.add(uri);
+                executorService.schedule(revalidationRequest, executionTime.getDuration(), executionTime.getTimeUnit());
+                queued.add(cacheKey);
             } catch (final RejectedExecutionException ree) {
-                log.debug("Revalidation for [" + uri + "] not scheduled: " + ree);
+                log.debug("Revalidation for [" + cacheKey + "] not scheduled: " + ree);
             }
         }
     }
 
+    @Override
+    public void close() throws IOException {
+        executorService.shutdown();
+    }
+
+    public void awaitTermination(final Timeout timeout) throws InterruptedException {
+        Args.notNull(timeout, "Timeout");
+        executorService.awaitTermination(timeout.getDuration(), timeout.getTimeUnit());
+    }
+
     /**
      * Removes an identifier from the internal list of revalidation jobs in
      * progress.  This is meant to be called by

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ExponentialBackOffSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ExponentialBackOffSchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ExponentialBackOffSchedulingStrategy.java
deleted file mode 100644
index 921d49e..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ExponentialBackOffSchedulingStrategy.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.util.Args;
-
-/**
- * An implementation that backs off exponentially based on the number of
- * consecutive failed attempts stored in the
- * {@link AsynchronousValidationRequest}. It uses the following defaults:
- * <pre>
- *         no delay in case it was never tried or didn't fail so far
- *     6 secs delay for one failed attempt (= {@link #getInitialExpiryInMillis()})
- *    60 secs delay for two failed attempts
- *    10 mins delay for three failed attempts
- *   100 mins delay for four failed attempts
- *  ~16 hours delay for five failed attempts
- *   24 hours delay for six or more failed attempts (= {@link #getMaxExpiryInMillis()})
- * </pre>
- *
- * The following equation is used to calculate the delay for a specific revalidation request:
- * <pre>
- *     delay = {@link #getInitialExpiryInMillis()} * Math.pow({@link #getBackOffRate()},
- *     {@link AsynchronousValidationRequest#getConsecutiveFailedAttempts()} - 1))
- * </pre>
- * The resulting delay won't exceed {@link #getMaxExpiryInMillis()}.
- *
- * @since 4.3
- */
-@Contract(threading = ThreadingBehavior.SAFE)
-public class ExponentialBackOffSchedulingStrategy implements SchedulingStrategy {
-
-    public static final long DEFAULT_BACK_OFF_RATE = 10;
-    public static final long DEFAULT_INITIAL_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(6);
-    public static final long DEFAULT_MAX_EXPIRY_IN_MILLIS = TimeUnit.SECONDS.toMillis(86400);
-
-    private final long backOffRate;
-    private final long initialExpiryInMillis;
-    private final long maxExpiryInMillis;
-
-    private final ScheduledExecutorService executor;
-
-    /**
-     * Create a new scheduling strategy using a fixed pool of worker threads.
-     * @param cacheConfig the thread pool configuration to be used; not {@code null}
-     * @see CacheConfig#getAsynchronousWorkersMax()
-     * @see #DEFAULT_BACK_OFF_RATE
-     * @see #DEFAULT_INITIAL_EXPIRY_IN_MILLIS
-     * @see #DEFAULT_MAX_EXPIRY_IN_MILLIS
-     */
-    public ExponentialBackOffSchedulingStrategy(final CacheConfig cacheConfig) {
-        this(cacheConfig,
-                DEFAULT_BACK_OFF_RATE,
-                DEFAULT_INITIAL_EXPIRY_IN_MILLIS,
-                DEFAULT_MAX_EXPIRY_IN_MILLIS);
-    }
-
-    /**
-     * Create a new scheduling strategy by using a fixed pool of worker threads and the
-     * given parameters to calculated the delay.
-     *
-     * @param cacheConfig the thread pool configuration to be used; not {@code null}
-     * @param backOffRate the back off rate to be used; not negative
-     * @param initialExpiryInMillis the initial expiry in milli seconds; not negative
-     * @param maxExpiryInMillis the upper limit of the delay in milli seconds; not negative
-     * @see CacheConfig#getAsynchronousWorkersMax()
-     * @see ExponentialBackOffSchedulingStrategy
-     */
-    public ExponentialBackOffSchedulingStrategy(
-            final CacheConfig cacheConfig,
-            final long backOffRate,
-            final long initialExpiryInMillis,
-            final long maxExpiryInMillis) {
-        this(createThreadPoolFromCacheConfig(cacheConfig),
-                backOffRate,
-                initialExpiryInMillis,
-                maxExpiryInMillis);
-    }
-
-    private static ScheduledThreadPoolExecutor createThreadPoolFromCacheConfig(
-            final CacheConfig cacheConfig) {
-        final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(
-                cacheConfig.getAsynchronousWorkersMax());
-        scheduledThreadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
-        return scheduledThreadPoolExecutor;
-    }
-
-    ExponentialBackOffSchedulingStrategy(
-            final ScheduledExecutorService executor,
-            final long backOffRate,
-            final long initialExpiryInMillis,
-            final long maxExpiryInMillis) {
-        this.executor = Args.notNull(executor, "Executor");
-        this.backOffRate = Args.notNegative(backOffRate, "BackOffRate");
-        this.initialExpiryInMillis = Args.notNegative(initialExpiryInMillis, "InitialExpiryInMillis");
-        this.maxExpiryInMillis = Args.notNegative(maxExpiryInMillis, "MaxExpiryInMillis");
-    }
-
-    @Override
-    public void schedule(
-            final AsynchronousValidationRequest revalidationRequest) {
-        Args.notNull(revalidationRequest, "RevalidationRequest");
-        final int consecutiveFailedAttempts = revalidationRequest.getConsecutiveFailedAttempts();
-        final long delayInMillis = calculateDelayInMillis(consecutiveFailedAttempts);
-        executor.schedule(revalidationRequest, delayInMillis, TimeUnit.MILLISECONDS);
-    }
-
-    @Override
-    public void close() {
-        executor.shutdown();
-    }
-
-    public long getBackOffRate() {
-        return backOffRate;
-    }
-
-    public long getInitialExpiryInMillis() {
-        return initialExpiryInMillis;
-    }
-
-    public long getMaxExpiryInMillis() {
-        return maxExpiryInMillis;
-    }
-
-    protected long calculateDelayInMillis(final int consecutiveFailedAttempts) {
-        if (consecutiveFailedAttempts > 0) {
-            final long delayInSeconds = (long) (initialExpiryInMillis *
-                    Math.pow(backOffRate, consecutiveFailedAttempts - 1));
-            return Math.min(delayInSeconds, maxExpiryInMillis);
-        }
-        else {
-            return 0;
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ImmediateSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ImmediateSchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ImmediateSchedulingStrategy.java
deleted file mode 100644
index f38b061..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ImmediateSchedulingStrategy.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.util.Args;
-
-/**
- * Immediately schedules any incoming validation request. Relies on
- * {@link CacheConfig} to configure the used {@link java.util.concurrent.ThreadPoolExecutor}.
- *
- * @since 4.3
- */
-@Contract(threading = ThreadingBehavior.SAFE)
-public class ImmediateSchedulingStrategy implements SchedulingStrategy {
-
-    private final ExecutorService executor;
-
-    /**
-     * Uses a {@link java.util.concurrent.ThreadPoolExecutor} which is configured according to the
-     * given {@link CacheConfig}.
-     * @param cacheConfig specifies thread pool settings. See
-     * {@link CacheConfig#getAsynchronousWorkersMax()},
-     * {@link CacheConfig#getAsynchronousWorkersCore()},
-     * {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
-     * and {@link CacheConfig#getRevalidationQueueSize()}.
-     */
-    public ImmediateSchedulingStrategy(final CacheConfig cacheConfig) {
-        this(new ThreadPoolExecutor(
-                cacheConfig.getAsynchronousWorkersCore(),
-                cacheConfig.getAsynchronousWorkersMax(),
-                cacheConfig.getAsynchronousWorkerIdleLifetimeSecs(),
-                TimeUnit.SECONDS,
-                new ArrayBlockingQueue<Runnable>(cacheConfig.getRevalidationQueueSize()))
-        );
-    }
-
-    ImmediateSchedulingStrategy(final ExecutorService executor) {
-        this.executor = executor;
-    }
-
-    @Override
-    public void schedule(final AsynchronousValidationRequest revalidationRequest) {
-        Args.notNull(revalidationRequest, "AsynchronousValidationRequest");
-        executor.execute(revalidationRequest);
-    }
-
-    @Override
-    public void close() {
-        executor.shutdown();
-    }
-
-    /**
-     * Visible for testing.
-     */
-    void awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException {
-        executor.awaitTermination(timeout, unit);
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/SchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/SchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/SchedulingStrategy.java
deleted file mode 100644
index d9cbd55..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/SchedulingStrategy.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.io.Closeable;
-
-/**
- * Specifies when revalidation requests are scheduled.
- *
- * @since 4.3
- */
-public interface SchedulingStrategy extends Closeable
-{
-    /**
-     * Schedule an {@link AsynchronousValidationRequest} to be executed.
-     *
-     * @param revalidationRequest the request to be executed; not {@code null}
-     * @throws java.util.concurrent.RejectedExecutionException if the request could not be scheduled for execution
-     */
-    void schedule(AsynchronousValidationRequest revalidationRequest);
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ExponentialBackOffSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ExponentialBackOffSchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ExponentialBackOffSchedulingStrategy.java
new file mode 100644
index 0000000..2bccc9c
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ExponentialBackOffSchedulingStrategy.java
@@ -0,0 +1,118 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.schedule;
+
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+
+/**
+ * An implementation that backs off exponentially based on the number of
+ * consecutive failed attempts. It uses the following defaults:
+ * <pre>
+ *         no delay in case it was never tried or didn't fail so far
+ *     6 secs delay for one failed attempt (= {@link #getInitialExpiry()})
+ *    60 secs delay for two failed attempts
+ *    10 mins delay for three failed attempts
+ *   100 mins delay for four failed attempts
+ *  ~16 hours delay for five failed attempts
+ *   24 hours delay for six or more failed attempts (= {@link #getMaxExpiry()})
+ * </pre>
+ *
+ * The following equation is used to calculate the delay for a specific pending operation:
+ * <pre>
+ *     delay = {@link #getInitialExpiry()} * Math.pow({@link #getBackOffRate()},
+ *     {@code consecutiveFailedAttempts} - 1))
+ * </pre>
+ * The resulting delay won't exceed {@link #getMaxExpiry()}.
+ *
+ * @since 4.3
+ */
+@Contract(threading = ThreadingBehavior.SAFE)
+public class ExponentialBackOffSchedulingStrategy implements SchedulingStrategy {
+
+    public static final long DEFAULT_BACK_OFF_RATE = 10;
+    public static final TimeValue DEFAULT_INITIAL_EXPIRY = TimeValue.ofSeconds(6);
+    public static final TimeValue DEFAULT_MAX_EXPIRY = TimeValue.ofSeconds(86400);
+
+    private static final ExponentialBackOffSchedulingStrategy INSTANCE = new ExponentialBackOffSchedulingStrategy(
+            DEFAULT_BACK_OFF_RATE, DEFAULT_INITIAL_EXPIRY, DEFAULT_MAX_EXPIRY);
+
+    private final long backOffRate;
+    private final TimeValue initialExpiry;
+    private final TimeValue maxExpiry;
+
+    public ExponentialBackOffSchedulingStrategy(
+            final long backOffRate,
+            final TimeValue initialExpiry,
+            final TimeValue maxExpiry) {
+        this.backOffRate = Args.notNegative(backOffRate, "BackOff rate");
+        this.initialExpiry = Args.notNull(initialExpiry, "Initial expiry");
+        this.maxExpiry = Args.notNull(maxExpiry, "Max expiry");
+    }
+
+    public ExponentialBackOffSchedulingStrategy(final long backOffRate, final TimeValue initialExpiry) {
+        this(backOffRate, initialExpiry, DEFAULT_MAX_EXPIRY);
+    }
+
+    public ExponentialBackOffSchedulingStrategy(final long backOffRate) {
+        this(backOffRate, DEFAULT_INITIAL_EXPIRY, DEFAULT_MAX_EXPIRY);
+    }
+
+    public ExponentialBackOffSchedulingStrategy() {
+        this(DEFAULT_BACK_OFF_RATE, DEFAULT_INITIAL_EXPIRY, DEFAULT_MAX_EXPIRY);
+    }
+
+    @Override
+    public TimeValue schedule(final int attemptNumber) {
+        return TimeValue.ofMillis(calculateDelayInMillis(attemptNumber));
+    }
+
+    public long getBackOffRate() {
+        return backOffRate;
+    }
+
+    public TimeValue getInitialExpiry() {
+        return initialExpiry;
+    }
+
+    public TimeValue getMaxExpiry() {
+        return maxExpiry;
+    }
+
+    protected long calculateDelayInMillis(final int consecutiveFailedAttempts) {
+        if (consecutiveFailedAttempts > 0) {
+            final long delay = (long) (initialExpiry.toMillis() * Math.pow(backOffRate, consecutiveFailedAttempts - 1));
+            return Math.min(delay, maxExpiry.toMillis());
+        } else {
+            return 0;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ImmediateSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ImmediateSchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ImmediateSchedulingStrategy.java
new file mode 100644
index 0000000..bb498f6
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/schedule/ImmediateSchedulingStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.schedule;
+
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.TimeValue;
+
+/**
+ * Immediately schedules any operation.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class ImmediateSchedulingStrategy implements SchedulingStrategy {
+
+    private final static ImmediateSchedulingStrategy INSTANCE = new ImmediateSchedulingStrategy();
+
+    @Override
+    public TimeValue schedule(final int attemptNumber) {
+        return TimeValue.ZERO_MILLISECONDS;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/SchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/SchedulingStrategy.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/SchedulingStrategy.java
new file mode 100644
index 0000000..bffed1d
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/SchedulingStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.schedule;
+
+import org.apache.hc.core5.util.TimeValue;
+
+/**
+ * Strategy to determine an execution time (schedule) for an operation.
+ *
+ * @since 5.0
+ */
+public interface SchedulingStrategy {
+
+    /**
+     * Schedules execution time for an operation.
+     */
+    TimeValue schedule(int attemptNumber);
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
index 06c42ac..8450fa3 100644
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
@@ -26,15 +26,12 @@
  */
 package org.apache.hc.client5.http.impl.cache;
 
-import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import java.io.IOException;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.hc.client5.http.HttpRoute;
@@ -44,114 +41,121 @@ import org.apache.hc.client5.http.classic.ExecChain;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.Header;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.message.BasicHeaderIterator;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
 
-@SuppressWarnings({"boxing","static-access"}) // test code
+@RunWith(MockitoJUnitRunner.class)
 public class TestAsynchronousValidator {
 
-    private AsynchronousValidator impl;
-
+    @Mock
     private CachingExec mockClient;
+    @Mock
+    private ExecChain mockExecChain;
+    @Mock
+    private ExecRuntime mockEndpoint;
+    @Mock
+    private HttpCacheEntry mockCacheEntry;
+    @Mock
+    private SchedulingStrategy mockSchedulingStrategy;
+    @Mock
+    ScheduledExecutorService mockExecutorService;
+
     private HttpHost host;
     private HttpRoute route;
     private ClassicHttpRequest request;
     private HttpClientContext context;
     private ExecChain.Scope scope;
-    private ExecChain mockExecChain;
-    private ExecRuntime mockEndpoint;
-    private HttpCacheEntry mockCacheEntry;
+    private AsynchronousValidator impl;
 
-    private SchedulingStrategy mockSchedulingStrategy;
 
     @Before
     public void setUp() {
-        mockClient = mock(CachingExec.class);
         host = new HttpHost("foo.example.com", 80);
         route = new HttpRoute(host);
         request = new HttpGet("/");
         context = HttpClientContext.create();
-        mockExecChain = mock(ExecChain.class);
-        mockEndpoint = mock(ExecRuntime.class);
-        mockCacheEntry = mock(HttpCacheEntry.class);
-        mockSchedulingStrategy = mock(SchedulingStrategy.class);
         scope = new ExecChain.Scope("test", route, request, mockEndpoint, context);
+        impl = new AsynchronousValidator(mockExecutorService, mockSchedulingStrategy);
     }
 
     @Test
     public void testRevalidateCacheEntrySchedulesExecutionAndPopulatesIdentifier() {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
         when(mockCacheEntry.hasVariants()).thenReturn(false);
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(1));
 
         impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
 
         verify(mockCacheEntry).hasVariants();
-        verify(mockSchedulingStrategy).schedule(isA(AsynchronousValidationRequest.class));
+        verify(mockSchedulingStrategy).schedule(0);
+        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(1L), Mockito.eq(TimeUnit.SECONDS));
 
         Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
     }
 
     @Test
     public void testMarkCompleteRemovesIdentifier() {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
         when(mockCacheEntry.hasVariants()).thenReturn(false);
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(3));
 
         impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
 
-        final ArgumentCaptor<AsynchronousValidationRequest> cap = ArgumentCaptor.forClass(AsynchronousValidationRequest.class);
         verify(mockCacheEntry).hasVariants();
-        verify(mockSchedulingStrategy).schedule(cap.capture());
+        verify(mockSchedulingStrategy).schedule(0);
+        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(3L), Mockito.eq(TimeUnit.SECONDS));
 
         Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
+        final String cacheKey = CacheKeyGenerator.INSTANCE.generateVariantURI(host, request, mockCacheEntry);
+        Assert.assertTrue(impl.getScheduledIdentifiers().contains(cacheKey));
 
-        impl.markComplete(cap.getValue().getIdentifier());
+        impl.markComplete(cacheKey);
 
         Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
     }
 
     @Test
     public void testRevalidateCacheEntryDoesNotPopulateIdentifierOnRejectedExecutionException() {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
         when(mockCacheEntry.hasVariants()).thenReturn(false);
-        doThrow(new RejectedExecutionException()).when(mockSchedulingStrategy).schedule(isA(AsynchronousValidationRequest.class));
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
+        doThrow(new RejectedExecutionException()).when(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.anyLong(), Mockito.<TimeUnit>any());
 
         impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
 
         verify(mockCacheEntry).hasVariants();
 
         Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
-        verify(mockSchedulingStrategy).schedule(isA(AsynchronousValidationRequest.class));
+        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
     }
 
     @Test
     public void testRevalidateCacheEntryProperlyCollapsesRequest() {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
         when(mockCacheEntry.hasVariants()).thenReturn(false);
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
 
         impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
         impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
 
-        verify(mockCacheEntry, times(2)).hasVariants();
-        verify(mockSchedulingStrategy).schedule(isA(AsynchronousValidationRequest.class));
+        verify(mockCacheEntry, Mockito.times(2)).hasVariants();
+        verify(mockSchedulingStrategy).schedule(Mockito.anyInt());
+        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
 
         Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
     }
 
     @Test
     public void testVariantsBothRevalidated() {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
         final ClassicHttpRequest req1 = new HttpGet("/");
         req1.addHeader(new BasicHeader("Accept-Encoding", "identity"));
 
@@ -165,55 +169,28 @@ public class TestAsynchronousValidator {
         when(mockCacheEntry.hasVariants()).thenReturn(true);
         when(mockCacheEntry.headerIterator(HeaderConstants.VARY)).thenReturn(
                 new BasicHeaderIterator(variantHeaders, HeaderConstants.VARY));
-        mockSchedulingStrategy.schedule(isA(AsynchronousValidationRequest.class));
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
 
         impl.revalidateCacheEntry(mockClient, host, req1, new ExecChain.Scope("test", route, req1, mockEndpoint, context),
                 mockExecChain, mockCacheEntry);
         impl.revalidateCacheEntry(mockClient, host, req2, new ExecChain.Scope("test", route, req2, mockEndpoint, context),
                 mockExecChain, mockCacheEntry);
 
-        verify(mockCacheEntry, times(2)).hasVariants();
-        verify(mockCacheEntry, times(2)).headerIterator(HeaderConstants.VARY);
-        verify(mockSchedulingStrategy, times(2)).schedule(isA(AsynchronousValidationRequest.class));
+        verify(mockCacheEntry, Mockito.times(2)).hasVariants();
+        verify(mockCacheEntry, Mockito.times(2)).headerIterator(HeaderConstants.VARY);
+        verify(mockSchedulingStrategy, Mockito.times(2)).schedule(Mockito.anyInt());
+        verify(mockExecutorService, Mockito.times(2)).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
 
         Assert.assertEquals(2, impl.getScheduledIdentifiers().size());
     }
 
     @Test
-    public void testRevalidateCacheEntryEndToEnd() throws Exception {
-        final CacheConfig config = CacheConfig.custom()
-            .setAsynchronousWorkersMax(1)
-            .setAsynchronousWorkersCore(1)
-            .build();
-        final ImmediateSchedulingStrategy schedulingStrategy = new ImmediateSchedulingStrategy(config);
-        impl = new AsynchronousValidator(schedulingStrategy);
-
-        when(mockCacheEntry.hasVariants()).thenReturn(false);
-        when(mockClient.revalidateCacheEntry(
-                host, request, scope, mockExecChain, mockCacheEntry)).thenReturn(null);
-
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-
-        try {
-            // shut down backend executor and make sure all finishes properly, 1 second should be sufficient
-            schedulingStrategy.close();
-            schedulingStrategy.awaitTermination(1, TimeUnit.SECONDS);
-        } catch (final InterruptedException ie) {
-
-        } finally {
-            verify(mockCacheEntry).hasVariants();
-            verify(mockClient).revalidateCacheEntry(host, request, scope, mockExecChain, mockCacheEntry);
-
-            Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
-        }
-    }
-
-    @Test
-    public void testSchedulingStrategyShutdownOnClose() throws IOException {
-        impl = new AsynchronousValidator(mockSchedulingStrategy);
-
+    public void testShutdown() throws Exception {
         impl.close();
+        impl.awaitTermination(Timeout.ofMinutes(2));
 
-        verify(mockSchedulingStrategy).close();
+        verify(mockExecutorService).shutdown();
+        verify(mockExecutorService).awaitTermination(2L, TimeUnit.MINUTES);
     }
+
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestExponentialBackingOffSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestExponentialBackingOffSchedulingStrategy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestExponentialBackingOffSchedulingStrategy.java
deleted file mode 100644
index 277e82f..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestExponentialBackingOffSchedulingStrategy.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.client5.http.classic.ExecRuntime;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
-import org.junit.Before;
-import org.junit.Test;
-
-public class TestExponentialBackingOffSchedulingStrategy {
-
-    private ScheduledExecutorService mockExecutor;
-    private ExecRuntime mockEndpoint;
-    private ExponentialBackOffSchedulingStrategy impl;
-
-    @Before
-    public void setUp() {
-        mockExecutor = mock(ScheduledExecutorService.class);
-
-        impl = new ExponentialBackOffSchedulingStrategy(
-                mockExecutor,
-                ExponentialBackOffSchedulingStrategy.DEFAULT_BACK_OFF_RATE,
-                ExponentialBackOffSchedulingStrategy.DEFAULT_INITIAL_EXPIRY_IN_MILLIS,
-                ExponentialBackOffSchedulingStrategy.DEFAULT_MAX_EXPIRY_IN_MILLIS
-        );
-    }
-
-    @Test
-    public void testScheduleWithoutPreviousError() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(0));
-
-        expectRequestScheduledWithoutDelay(request);
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 0, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithOneFailedAttempt() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(1));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(6));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 6000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithTwoFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(2));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(60));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 60000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithThreeFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(3));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(600));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 600000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithFourFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(4));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(6000));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 6000000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithFiveFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(5));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(60000));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 60000000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithSixFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(6));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(86400));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 86400000, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void testScheduleWithMaxNumberOfFailedAttempts() {
-        final AsynchronousValidationRequest request = createAsynchronousValidationRequest(withErrorCount(Integer.MAX_VALUE));
-
-        expectRequestScheduledWithDelay(request, TimeUnit.SECONDS.toMillis(86400));
-
-        impl.schedule(request);
-
-        verify(mockExecutor).schedule(request, 86400000, TimeUnit.MILLISECONDS);
-    }
-
-    private void expectRequestScheduledWithoutDelay(final AsynchronousValidationRequest request) {
-        expectRequestScheduledWithDelay(request, 0);
-    }
-
-    private void expectRequestScheduledWithDelay(final AsynchronousValidationRequest request, final long delayInMillis) {
-        when(mockExecutor.schedule(request, delayInMillis, TimeUnit.MILLISECONDS)).thenReturn(null);
-    }
-
-    private AsynchronousValidationRequest createAsynchronousValidationRequest(final int errorCount) {
-        final CachingExec cachingHttpClient = new CachingExec();
-        final AsynchronousValidator mockValidator = new AsynchronousValidator(impl);
-        final HttpHost host = new HttpHost("foo.example.com", 80);
-        final HttpRoute route = new HttpRoute(host);
-        final ClassicHttpRequest request = new BasicClassicHttpRequest("GET", "/");
-        final HttpClientContext context = new HttpClientContext();
-        final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, mock(ExecRuntime.class), context);
-        return new AsynchronousValidationRequest(mockValidator, cachingHttpClient, host, request,
-                scope, mock(ExecChain.class), null, "identifier", errorCount);
-    }
-
-    private static int withErrorCount(final int errorCount) {
-        return errorCount;
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestImmediateSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestImmediateSchedulingStrategy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestImmediateSchedulingStrategy.java
deleted file mode 100644
index 645dbe0..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestImmediateSchedulingStrategy.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import static org.mockito.Mockito.mock;
-
-import java.util.concurrent.ExecutorService;
-
-import org.junit.Before;
-import org.junit.Test;
-
-public class TestImmediateSchedulingStrategy {
-
-    private ExecutorService mockExecutor;
-    private AsynchronousValidationRequest mockRevalidationRequest;
-    private SchedulingStrategy schedulingStrategy;
-
-    @Before
-    public void setUp() {
-        mockExecutor = mock(ExecutorService.class);
-        mockRevalidationRequest = mock(AsynchronousValidationRequest.class);
-        schedulingStrategy = new ImmediateSchedulingStrategy(mockExecutor);
-    }
-
-    @Test
-    public void testRequestScheduledImmediately() {
-        mockExecutor.execute(mockRevalidationRequest);
-
-        schedulingStrategy.schedule(mockRevalidationRequest);
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestExponentialBackingOffSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestExponentialBackingOffSchedulingStrategy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestExponentialBackingOffSchedulingStrategy.java
new file mode 100644
index 0000000..65181c9
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestExponentialBackingOffSchedulingStrategy.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.schedule;
+
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestExponentialBackingOffSchedulingStrategy {
+
+    private ExponentialBackOffSchedulingStrategy impl;
+
+    @Before
+    public void setUp() {
+        impl = new ExponentialBackOffSchedulingStrategy(
+                ExponentialBackOffSchedulingStrategy.DEFAULT_BACK_OFF_RATE,
+                ExponentialBackOffSchedulingStrategy.DEFAULT_INITIAL_EXPIRY,
+                ExponentialBackOffSchedulingStrategy.DEFAULT_MAX_EXPIRY
+        );
+    }
+
+    @Test
+    public void testSchedule() {
+        Assert.assertEquals(TimeValue.ofMillis(0), impl.schedule(0));
+        Assert.assertEquals(TimeValue.ofMillis(6000), impl.schedule(1));
+        Assert.assertEquals(TimeValue.ofMillis(60000), impl.schedule(2));
+        Assert.assertEquals(TimeValue.ofMillis(600000), impl.schedule(3));
+        Assert.assertEquals(TimeValue.ofMillis(6000000), impl.schedule(4));
+        Assert.assertEquals(TimeValue.ofMillis(60000000), impl.schedule(5));
+        Assert.assertEquals(TimeValue.ofMillis(86400000), impl.schedule(6));
+        Assert.assertEquals(TimeValue.ofMillis(86400000), impl.schedule(Integer.MAX_VALUE));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/0561bacc/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestImmediateSchedulingStrategy.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestImmediateSchedulingStrategy.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestImmediateSchedulingStrategy.java
new file mode 100644
index 0000000..97f1ac9
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/schedule/TestImmediateSchedulingStrategy.java
@@ -0,0 +1,51 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.schedule;
+
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestImmediateSchedulingStrategy {
+
+    private SchedulingStrategy impl;
+
+    @Before
+    public void setUp() {
+        impl = new ImmediateSchedulingStrategy();
+    }
+
+    @Test
+    public void testSchedule() {
+        Assert.assertEquals(TimeValue.ZERO_MILLISECONDS, impl.schedule(0));
+        Assert.assertEquals(TimeValue.ZERO_MILLISECONDS, impl.schedule(1));
+        Assert.assertEquals(TimeValue.ZERO_MILLISECONDS, impl.schedule(Integer.MAX_VALUE));
+    }
+
+}


[2/3] httpcomponents-client git commit: Replaced FailureCache by generic ConcurrentCountMap

Posted by ol...@apache.org.
Replaced FailureCache by generic ConcurrentCountMap


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/79b76030
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/79b76030
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/79b76030

Branch: refs/heads/master
Commit: 79b76030fd0844f5cd3f71f67d9fc93d5e2b6e57
Parents: 0561bac
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Fri Jan 5 15:09:05 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Fri Jan 5 15:09:05 2018 +0100

----------------------------------------------------------------------
 .../http/impl/cache/AsynchronousValidator.java  |  12 +-
 .../http/impl/cache/DefaultFailureCache.java    | 147 -------------------
 .../client5/http/impl/cache/FailureCache.java   |  57 -------
 .../http/impl/cache/FailureCacheValue.java      |  68 ---------
 .../http/schedule/ConcurrentCountMap.java       |  75 ++++++++++
 .../impl/cache/TestDefaultFailureCache.java     |  69 ---------
 .../http/schedule/TestConcurrentCountMap.java   |  50 +++++++
 7 files changed, 132 insertions(+), 346 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
index c635747..b5c90ca 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
@@ -38,6 +38,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
 import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.classic.ExecChain;
 import org.apache.hc.client5.http.impl.schedule.ImmediateSchedulingStrategy;
+import org.apache.hc.client5.http.schedule.ConcurrentCountMap;
 import org.apache.hc.client5.http.schedule.SchedulingStrategy;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.HttpHost;
@@ -57,7 +58,7 @@ class AsynchronousValidator implements Closeable {
     private final SchedulingStrategy schedulingStrategy;
     private final Set<String> queued;
     private final CacheKeyGenerator cacheKeyGenerator;
-    private final FailureCache failureCache;
+    private final ConcurrentCountMap<String> failureCache;
 
     private final Logger log = LogManager.getLogger(getClass());
 
@@ -87,7 +88,7 @@ class AsynchronousValidator implements Closeable {
         this.schedulingStrategy = schedulingStrategy;
         this.queued = new HashSet<>();
         this.cacheKeyGenerator = CacheKeyGenerator.INSTANCE;
-        this.failureCache = new DefaultFailureCache();
+        this.failureCache = new ConcurrentCountMap<>();
     }
 
     /**
@@ -104,7 +105,7 @@ class AsynchronousValidator implements Closeable {
         final String cacheKey = cacheKeyGenerator.generateVariantURI(target, request, entry);
 
         if (!queued.contains(cacheKey)) {
-            final int consecutiveFailedAttempts = failureCache.getErrorCount(cacheKey);
+            final int consecutiveFailedAttempts = failureCache.getCount(cacheKey);
             final AsynchronousValidationRequest revalidationRequest =
                 new AsynchronousValidationRequest(
                         this, cachingExec, target, request, scope, chain, entry, cacheKey, consecutiveFailedAttempts);
@@ -147,7 +148,7 @@ class AsynchronousValidator implements Closeable {
      * @param identifier the revalidation job's unique identifier
      */
     void jobSuccessful(final String identifier) {
-        failureCache.resetErrorCount(identifier);
+        failureCache.resetCount(identifier);
     }
 
     /**
@@ -157,10 +158,11 @@ class AsynchronousValidator implements Closeable {
      * @param identifier the revalidation job's unique identifier
      */
     void jobFailed(final String identifier) {
-        failureCache.increaseErrorCount(identifier);
+        failureCache.increaseCount(identifier);
     }
 
     Set<String> getScheduledIdentifiers() {
         return Collections.unmodifiableSet(queued);
     }
+
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultFailureCache.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultFailureCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultFailureCache.java
deleted file mode 100644
index 7170d99..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultFailureCache.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-
-/**
- * Implements a bounded failure cache. The oldest entries are discarded when
- * the maximum size is exceeded.
- *
- * @since 4.3
- */
-@Contract(threading = ThreadingBehavior.SAFE)
-public class DefaultFailureCache implements FailureCache {
-
-    static final int DEFAULT_MAX_SIZE = 1000;
-    static final int MAX_UPDATE_TRIES = 10;
-
-    private final int maxSize;
-    private final ConcurrentMap<String, FailureCacheValue> storage;
-
-    /**
-     * Create a new failure cache with the maximum size of
-     * {@link #DEFAULT_MAX_SIZE}.
-     */
-    public DefaultFailureCache() {
-        this(DEFAULT_MAX_SIZE);
-    }
-
-    /**
-     * Creates a new failure cache with the specified maximum size.
-     * @param maxSize the maximum number of entries the cache should store
-     */
-    public DefaultFailureCache(final int maxSize) {
-        this.maxSize = maxSize;
-        this.storage = new ConcurrentHashMap<>();
-    }
-
-    @Override
-    public int getErrorCount(final String identifier) {
-        if (identifier == null) {
-            throw new IllegalArgumentException("identifier may not be null");
-        }
-        final FailureCacheValue storedErrorCode = storage.get(identifier);
-        return storedErrorCode != null ? storedErrorCode.getErrorCount() : 0;
-    }
-
-    @Override
-    public void resetErrorCount(final String identifier) {
-        if (identifier == null) {
-            throw new IllegalArgumentException("identifier may not be null");
-        }
-        storage.remove(identifier);
-    }
-
-    @Override
-    public void increaseErrorCount(final String identifier) {
-        if (identifier == null) {
-            throw new IllegalArgumentException("identifier may not be null");
-        }
-        updateValue(identifier);
-        removeOldestEntryIfMapSizeExceeded();
-    }
-
-    private void updateValue(final String identifier) {
-        /**
-         * Due to concurrency it is possible that someone else is modifying an
-         * entry before we could write back our updated value. So we keep
-         * trying until it is our turn.
-         *
-         * In case there is a lot of contention on that identifier, a thread
-         * might starve. Thus it gives up after a certain number of failed
-         * processChallenge tries.
-         */
-        for (int i = 0; i < MAX_UPDATE_TRIES; i++) {
-            final FailureCacheValue oldValue = storage.get(identifier);
-            if (oldValue == null) {
-                final FailureCacheValue newValue = new FailureCacheValue(identifier, 1);
-                if (storage.putIfAbsent(identifier, newValue) == null) {
-                    return;
-                }
-            }
-            else {
-                final int errorCount = oldValue.getErrorCount();
-                if (errorCount == Integer.MAX_VALUE) {
-                    return;
-                }
-                final FailureCacheValue newValue = new FailureCacheValue(identifier, errorCount + 1);
-                if (storage.replace(identifier, oldValue, newValue)) {
-                    return;
-                }
-            }
-        }
-    }
-
-    private void removeOldestEntryIfMapSizeExceeded() {
-        if (storage.size() > maxSize) {
-            final FailureCacheValue valueWithOldestTimestamp = findValueWithOldestTimestamp();
-            if (valueWithOldestTimestamp != null) {
-                storage.remove(valueWithOldestTimestamp.getKey(), valueWithOldestTimestamp);
-            }
-        }
-    }
-
-    private FailureCacheValue findValueWithOldestTimestamp() {
-        long oldestTimestamp = Long.MAX_VALUE;
-        FailureCacheValue oldestValue = null;
-        for (final Map.Entry<String, FailureCacheValue> storageEntry : storage.entrySet()) {
-            final FailureCacheValue value = storageEntry.getValue();
-            final long creationTimeInNanos = value.getCreationTimeInNanos();
-            if (creationTimeInNanos < oldestTimestamp) {
-                oldestTimestamp = creationTimeInNanos;
-                oldestValue = storageEntry.getValue();
-            }
-        }
-        return oldestValue;
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCache.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCache.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCache.java
deleted file mode 100644
index 19c941d..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCache.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-/**
- * Increase and reset the number of errors associated with a specific
- * identifier.
- *
- * @since 4.3
- */
-public interface FailureCache {
-
-    /**
-     * Get the current error count.
-     * @param identifier the identifier for which the error count is requested
-     * @return the currently known error count or zero if there is no record
-     */
-    int getErrorCount(String identifier);
-
-    /**
-     * Reset the error count back to zero.
-     * @param identifier the identifier for which the error count should be
-     *                   reset
-     */
-    void resetErrorCount(String identifier);
-
-    /**
-     * Increases the error count by one.
-     * @param identifier the identifier for which the error count should be
-     *                   increased
-     */
-    void increaseErrorCount(String identifier);
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCacheValue.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCacheValue.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCacheValue.java
deleted file mode 100644
index 32f7702..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/FailureCacheValue.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-
-/**
- * The error count with a creation timestamp and its associated key.
- *
- * @since 4.3
- */
-@Contract(threading = ThreadingBehavior.IMMUTABLE)
-public class FailureCacheValue {
-
-    private final long creationTimeInNanos;
-    private final String key;
-    private final int errorCount;
-
-    public FailureCacheValue(final String key, final int errorCount) {
-        this.creationTimeInNanos = System.nanoTime();
-        this.key = key;
-        this.errorCount = errorCount;
-    }
-
-    public long getCreationTimeInNanos() {
-        return creationTimeInNanos;
-    }
-
-    public String getKey()
-    {
-        return key;
-    }
-
-    public int getErrorCount() {
-        return errorCount;
-    }
-
-    @Override
-    public String toString() {
-        return "[entry creationTimeInNanos=" + creationTimeInNanos + "; " +
-                "key=" + key + "; errorCount=" + errorCount + ']';
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/ConcurrentCountMap.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/ConcurrentCountMap.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/ConcurrentCountMap.java
new file mode 100644
index 0000000..60eb84e
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/schedule/ConcurrentCountMap.java
@@ -0,0 +1,75 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.schedule;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+
+@Contract(threading = ThreadingBehavior.SAFE)
+public final class ConcurrentCountMap<T> {
+
+    private final ConcurrentMap<T, AtomicInteger> map;
+
+    public ConcurrentCountMap() {
+        this.map = new ConcurrentHashMap<>();
+    }
+
+    public int getCount(final T identifier) {
+        Args.notNull(identifier, "Identifier");
+        final AtomicInteger count = map.get(identifier);
+        return count != null ? count.get() : 0;
+    }
+
+    public void resetCount(final T identifier) {
+        Args.notNull(identifier, "Identifier");
+        map.remove(identifier);
+    }
+
+    public int increaseCount(final T identifier) {
+        Args.notNull(identifier, "Identifier");
+        final AtomicInteger count = get(identifier);
+        return count.incrementAndGet();
+    }
+
+    private AtomicInteger get(final T identifier) {
+        AtomicInteger entry = map.get(identifier);
+        if (entry == null) {
+            final AtomicInteger newEntry = new AtomicInteger();
+            entry = map.putIfAbsent(identifier, newEntry);
+            if (entry == null) {
+                entry = newEntry;
+            }
+        }
+        return entry;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultFailureCache.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultFailureCache.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultFailureCache.java
deleted file mode 100644
index 729694e..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestDefaultFailureCache.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-public class TestDefaultFailureCache
-{
-
-    private static final String IDENTIFIER = "some-identifier";
-
-    private FailureCache failureCache = new DefaultFailureCache();
-
-    @Test
-    public void testResetErrorCount() {
-        failureCache.increaseErrorCount(IDENTIFIER);
-        failureCache.resetErrorCount(IDENTIFIER);
-
-        final int errorCount = failureCache.getErrorCount(IDENTIFIER);
-        Assert.assertEquals(0, errorCount);
-    }
-
-    @Test
-    public void testIncrementErrorCount() {
-        failureCache.increaseErrorCount(IDENTIFIER);
-        failureCache.increaseErrorCount(IDENTIFIER);
-        failureCache.increaseErrorCount(IDENTIFIER);
-
-        final int errorCount = failureCache.getErrorCount(IDENTIFIER);
-        Assert.assertEquals(3, errorCount);
-    }
-
-    @Test
-    public void testMaxSize() {
-        failureCache = new DefaultFailureCache(3);
-        failureCache.increaseErrorCount("a");
-        failureCache.increaseErrorCount("b");
-        failureCache.increaseErrorCount("c");
-        failureCache.increaseErrorCount("d");
-
-        final int errorCount = failureCache.getErrorCount("a");
-        Assert.assertEquals(0, errorCount);
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/79b76030/httpclient5-cache/src/test/java/org/apache/hc/client5/http/schedule/TestConcurrentCountMap.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/schedule/TestConcurrentCountMap.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/schedule/TestConcurrentCountMap.java
new file mode 100644
index 0000000..50b3832
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/schedule/TestConcurrentCountMap.java
@@ -0,0 +1,50 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.schedule;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestConcurrentCountMap
+{
+
+    private static final String IDENTIFIER = "some-identifier";
+
+    private ConcurrentCountMap<String> map = new ConcurrentCountMap<>();
+
+    @Test
+    public void testBasics() {
+        map.increaseCount(IDENTIFIER);
+        map.increaseCount(IDENTIFIER);
+        Assert.assertThat(map.getCount(IDENTIFIER), CoreMatchers.equalTo(2));
+
+        map.resetCount(IDENTIFIER);
+        Assert.assertThat(map.getCount(IDENTIFIER), CoreMatchers.equalTo(0));
+    }
+
+}


[3/3] httpcomponents-client git commit: Redesigned cache entry re-validation logic; added classic and async implementations of cache re-validators

Posted by ol...@apache.org.
Redesigned cache entry re-validation logic; added classic and async implementations of cache re-validators


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/7cf4240b
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/7cf4240b
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/7cf4240b

Branch: refs/heads/master
Commit: 7cf4240b3f580ecf5ff67eeb71b3e91bf4a100fd
Parents: 79b7603
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Sat Jan 6 11:45:13 2018 +0100
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Sun Jan 7 12:48:59 2018 +0100

----------------------------------------------------------------------
 .../cache/AsynchronousValidationRequest.java    | 169 ----------------
 .../http/impl/cache/AsynchronousValidator.java  | 168 ----------------
 .../http/impl/cache/CacheInvalidatorBase.java   |   4 +-
 .../http/impl/cache/CacheRevalidatorBase.java   | 190 ++++++++++++++++++
 .../cache/DefaultAsyncCacheRevalidator.java     | 160 +++++++++++++++
 .../impl/cache/DefaultCacheRevalidator.java     | 113 +++++++++++
 .../TestAsynchronousValidationRequest.java      | 193 ------------------
 .../impl/cache/TestAsynchronousValidator.java   | 196 -------------------
 .../impl/cache/TestCacheRevalidatorBase.java    | 154 +++++++++++++++
 9 files changed, 619 insertions(+), 728 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidationRequest.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidationRequest.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidationRequest.java
deleted file mode 100644
index 01a8e6b..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidationRequest.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.io.IOException;
-
-import org.apache.hc.client5.http.cache.HeaderConstants;
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpResponse;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-/**
- * Class used to represent an asynchronous revalidation event, such as with
- * "stale-while-revalidate"
- */
-public class AsynchronousValidationRequest implements Runnable {
-    private final AsynchronousValidator parent;
-    private final CachingExec cachingExec;
-    private final HttpHost target;
-    private final ClassicHttpRequest request;
-    private final ExecChain.Scope scope;
-    private final ExecChain chain;
-    private final HttpCacheEntry cacheEntry;
-    private final String identifier;
-    private final int consecutiveFailedAttempts;
-
-    private final Logger log = LogManager.getLogger(getClass());
-
-    /**
-     * Used internally by {@link AsynchronousValidator} to schedule a
-     */
-    AsynchronousValidationRequest(
-            final AsynchronousValidator parent,
-            final CachingExec cachingExec,
-            final HttpHost target,
-            final ClassicHttpRequest request,
-            final ExecChain.Scope scope,
-            final ExecChain chain,
-            final HttpCacheEntry cacheEntry,
-            final String identifier,
-            final int consecutiveFailedAttempts) {
-        this.parent = parent;
-        this.cachingExec = cachingExec;
-        this.target = target;
-        this.request = request;
-        this.scope = scope;
-        this.chain = chain;
-        this.cacheEntry = cacheEntry;
-        this.identifier = identifier;
-        this.consecutiveFailedAttempts = consecutiveFailedAttempts;
-    }
-
-    @Override
-    public void run() {
-        try {
-            if (revalidateCacheEntry()) {
-                parent.jobSuccessful(identifier);
-            } else {
-                parent.jobFailed(identifier);
-            }
-        } finally {
-            parent.markComplete(identifier);
-        }
-    }
-
-    /**
-     * Revalidate the cache entry and return if the operation was successful.
-     * Success means a connection to the server was established and replay did
-     * not indicate a server error.
-     * @return {@code true} if the cache entry was successfully validated;
-     * otherwise {@code false}
-     */
-    private boolean revalidateCacheEntry() {
-        try {
-            try (ClassicHttpResponse httpResponse = cachingExec.revalidateCacheEntry(target, request, scope, chain, cacheEntry)) {
-                final int statusCode = httpResponse.getCode();
-                return isNotServerError(statusCode) && isNotStale(httpResponse);
-            }
-        } catch (final IOException ioe) {
-            log.debug("Asynchronous revalidation failed due to I/O error", ioe);
-            return false;
-        } catch (final HttpException pe) {
-            log.error("HTTP protocol exception during asynchronous revalidation", pe);
-            return false;
-        } catch (final RuntimeException re) {
-            log.error("RuntimeException thrown during asynchronous revalidation: " + re);
-            return false;
-        }
-    }
-
-    /**
-     * Return whether the status code indicates a server error or not.
-     * @param statusCode the status code to be checked
-     * @return if the status code indicates a server error or not
-     */
-    private boolean isNotServerError(final int statusCode) {
-        return statusCode < 500;
-    }
-
-    /**
-     * Try to detect if the returned response is generated from a stale cache entry.
-     * @param httpResponse the response to be checked
-     * @return whether the response is stale or not
-     */
-    private boolean isNotStale(final HttpResponse httpResponse) {
-        final Header[] warnings = httpResponse.getHeaders(HeaderConstants.WARNING);
-        if (warnings != null)
-        {
-            for (final Header warning : warnings)
-            {
-                /**
-                 * warn-codes
-                 * 110 = Response is stale
-                 * 111 = Revalidation failed
-                 */
-                final String warningValue = warning.getValue();
-                if (warningValue.startsWith("110") || warningValue.startsWith("111"))
-                {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    public String getIdentifier() {
-        return identifier;
-    }
-
-    /**
-     * The number of consecutively failed revalidation attempts.
-     * @return the number of consecutively failed revalidation attempts.
-     */
-    public int getConsecutiveFailedAttempts() {
-        return consecutiveFailedAttempts;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
deleted file mode 100644
index b5c90ca..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AsynchronousValidator.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.client5.http.impl.schedule.ImmediateSchedulingStrategy;
-import org.apache.hc.client5.http.schedule.ConcurrentCountMap;
-import org.apache.hc.client5.http.schedule.SchedulingStrategy;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.util.Args;
-import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-/**
- * Class used for asynchronous revalidations to be used when the "stale-
- * while-revalidate" directive is present
- */
-class AsynchronousValidator implements Closeable {
-
-    private final ScheduledExecutorService executorService;
-    private final SchedulingStrategy schedulingStrategy;
-    private final Set<String> queued;
-    private final CacheKeyGenerator cacheKeyGenerator;
-    private final ConcurrentCountMap<String> failureCache;
-
-    private final Logger log = LogManager.getLogger(getClass());
-
-    /**
-     * Create AsynchronousValidator which will make revalidation requests
-     * using an {@link ImmediateSchedulingStrategy}. Its thread
-     * pool will be configured according to the given {@link CacheConfig}.
-     * @param config specifies thread pool settings. See
-     * {@link CacheConfig#getAsynchronousWorkersMax()},
-     * {@link CacheConfig#getAsynchronousWorkersCore()},
-     * {@link CacheConfig#getAsynchronousWorkerIdleLifetimeSecs()},
-     * and {@link CacheConfig#getRevalidationQueueSize()}.
-     */
-    public AsynchronousValidator(final CacheConfig config) {
-        this(new ScheduledThreadPoolExecutor(config.getAsynchronousWorkersCore()), new ImmediateSchedulingStrategy());
-    }
-
-    /**
-     * Create AsynchronousValidator which will make revalidation requests
-     * using the supplied {@link SchedulingStrategy}. Closing the validator
-     * will also close the given schedulingStrategy.
-     * @param schedulingStrategy used to maintain a pool of worker threads and
-     *                           schedules when requests are executed
-     */
-    AsynchronousValidator(final ScheduledExecutorService executorService, final SchedulingStrategy schedulingStrategy) {
-        this.executorService = executorService;
-        this.schedulingStrategy = schedulingStrategy;
-        this.queued = new HashSet<>();
-        this.cacheKeyGenerator = CacheKeyGenerator.INSTANCE;
-        this.failureCache = new ConcurrentCountMap<>();
-    }
-
-    /**
-     * Schedules an asynchronous revalidation
-     */
-    public synchronized void revalidateCacheEntry(
-            final CachingExec cachingExec,
-            final HttpHost target,
-            final ClassicHttpRequest request,
-            final ExecChain.Scope scope,
-            final ExecChain chain,
-            final HttpCacheEntry entry) {
-        // getVariantURI will fall back on getURI if no variants exist
-        final String cacheKey = cacheKeyGenerator.generateVariantURI(target, request, entry);
-
-        if (!queued.contains(cacheKey)) {
-            final int consecutiveFailedAttempts = failureCache.getCount(cacheKey);
-            final AsynchronousValidationRequest revalidationRequest =
-                new AsynchronousValidationRequest(
-                        this, cachingExec, target, request, scope, chain, entry, cacheKey, consecutiveFailedAttempts);
-
-            final TimeValue executionTime = schedulingStrategy.schedule(consecutiveFailedAttempts);
-            try {
-                executorService.schedule(revalidationRequest, executionTime.getDuration(), executionTime.getTimeUnit());
-                queued.add(cacheKey);
-            } catch (final RejectedExecutionException ree) {
-                log.debug("Revalidation for [" + cacheKey + "] not scheduled: " + ree);
-            }
-        }
-    }
-
-    @Override
-    public void close() throws IOException {
-        executorService.shutdown();
-    }
-
-    public void awaitTermination(final Timeout timeout) throws InterruptedException {
-        Args.notNull(timeout, "Timeout");
-        executorService.awaitTermination(timeout.getDuration(), timeout.getTimeUnit());
-    }
-
-    /**
-     * Removes an identifier from the internal list of revalidation jobs in
-     * progress.  This is meant to be called by
-     * {@link AsynchronousValidationRequest#run()} once the revalidation is
-     * complete, using the identifier passed in during constructions.
-     * @param identifier
-     */
-    synchronized void markComplete(final String identifier) {
-        queued.remove(identifier);
-    }
-
-    /**
-     * The revalidation job was successful thus the number of consecutive
-     * failed attempts will be reset to zero. Should be called by
-     * {@link AsynchronousValidationRequest#run()}.
-     * @param identifier the revalidation job's unique identifier
-     */
-    void jobSuccessful(final String identifier) {
-        failureCache.resetCount(identifier);
-    }
-
-    /**
-     * The revalidation job did fail and thus the number of consecutive failed
-     * attempts will be increased. Should be called by
-     * {@link AsynchronousValidationRequest#run()}.
-     * @param identifier the revalidation job's unique identifier
-     */
-    void jobFailed(final String identifier) {
-        failureCache.increaseCount(identifier);
-    }
-
-    Set<String> getScheduledIdentifiers() {
-        return Collections.unmodifiableSet(queued);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
index d229011..604ed61 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheInvalidatorBase.java
@@ -61,9 +61,9 @@ class CacheInvalidatorBase {
     }
 
     static boolean notGetOrHeadRequest(final String method) {
-        return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD
-                .equals(method));
+        return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD.equals(method));
     }
+
     private static URI getLocationURI(final URI requestUri, final HttpResponse response, final String headerName) {
         final Header h = response.getFirstHeader(headerName);
         if (h == null) {

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheRevalidatorBase.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheRevalidatorBase.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheRevalidatorBase.java
new file mode 100644
index 0000000..25c0ecf
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/CacheRevalidatorBase.java
@@ -0,0 +1,190 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+import org.apache.hc.client5.http.cache.HeaderConstants;
+import org.apache.hc.client5.http.schedule.ConcurrentCountMap;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Abstract cache re-validation class.
+ */
+class CacheRevalidatorBase implements Closeable {
+
+    interface ScheduledExecutor {
+
+        Future<?> schedule(Runnable command, TimeValue timeValue) throws RejectedExecutionException;
+
+        void shutdown();
+
+        void awaitTermination(final Timeout timeout) throws InterruptedException;
+
+    }
+
+    public static ScheduledExecutor wrap(final ScheduledThreadPoolExecutor threadPoolExecutor) {
+
+        return new ScheduledExecutor() {
+
+            @Override
+            public ScheduledFuture<?> schedule(final Runnable command, final TimeValue timeValue) throws RejectedExecutionException {
+                Args.notNull(command, "Runnable");
+                Args.notNull(timeValue, "Time value");
+                return threadPoolExecutor.schedule(command, timeValue.getDuration(), timeValue.getTimeUnit());
+            }
+
+            @Override
+            public void shutdown() {
+                threadPoolExecutor.shutdown();
+            }
+
+            @Override
+            public void awaitTermination(final Timeout timeout) throws InterruptedException {
+                Args.notNull(timeout, "Timeout");
+                threadPoolExecutor.awaitTermination(timeout.getDuration(), timeout.getTimeUnit());
+            }
+
+        };
+
+    }
+
+    private final ScheduledExecutor scheduledExecutor;
+    private final SchedulingStrategy schedulingStrategy;
+    private final Set<String> pendingRequest;
+    private final ConcurrentCountMap<String> failureCache;
+
+    final Logger log = LogManager.getLogger(getClass());
+
+    /**
+     * Create CacheValidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledExecutor}.
+     */
+    public CacheRevalidatorBase(
+            final ScheduledExecutor scheduledExecutor,
+            final SchedulingStrategy schedulingStrategy) {
+        this.scheduledExecutor = scheduledExecutor;
+        this.schedulingStrategy = schedulingStrategy;
+        this.pendingRequest = new HashSet<>();
+        this.failureCache = new ConcurrentCountMap<>();
+    }
+
+    /**
+     * Create CacheValidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledThreadPoolExecutor}.
+     */
+    public CacheRevalidatorBase(
+            final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor,
+            final SchedulingStrategy schedulingStrategy) {
+        this(wrap(scheduledThreadPoolExecutor), schedulingStrategy);
+    }
+
+    /**
+     * Schedules an asynchronous re-validation
+     */
+    void scheduleRevalidation(final String cacheKey, final Runnable command) {
+        synchronized (pendingRequest) {
+            if (!pendingRequest.contains(cacheKey)) {
+                final int consecutiveFailedAttempts = failureCache.getCount(cacheKey);
+                final TimeValue executionTime = schedulingStrategy.schedule(consecutiveFailedAttempts);
+                try {
+                    scheduledExecutor.schedule(command, executionTime);
+                    pendingRequest.add(cacheKey);
+                } catch (final RejectedExecutionException ex) {
+                    log.debug("Revalidation of cache entry with key " + cacheKey + "could not be scheduled: " + ex);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        scheduledExecutor.shutdown();
+    }
+
+    public void awaitTermination(final Timeout timeout) throws InterruptedException {
+        Args.notNull(timeout, "Timeout");
+        scheduledExecutor.awaitTermination(timeout);
+    }
+
+    void jobSuccessful(final String identifier) {
+        failureCache.resetCount(identifier);
+        synchronized (pendingRequest) {
+            pendingRequest.remove(identifier);
+        }
+    }
+
+    void jobFailed(final String identifier) {
+        failureCache.increaseCount(identifier);
+        synchronized (pendingRequest) {
+            pendingRequest.remove(identifier);
+        }
+    }
+
+    Set<String> getScheduledIdentifiers() {
+        synchronized (pendingRequest) {
+            return new HashSet<>(pendingRequest);
+        }
+    }
+
+    /**
+     * Determines if the given response is generated from a stale cache entry.
+     * @param httpResponse the response to be checked
+     * @return whether the response is stale or not
+     */
+    boolean isStale(final HttpResponse httpResponse) {
+        for (final Iterator<Header> it = httpResponse.headerIterator(HeaderConstants.WARNING); it.hasNext(); ) {
+            /*
+             * warn-codes
+             * 110 = Response is stale
+             * 111 = Revalidation failed
+             */
+            final Header warning = it.next();
+            final String warningValue = warning.getValue();
+            if (warningValue.startsWith("110") || warningValue.startsWith("111")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheRevalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheRevalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheRevalidator.java
new file mode 100644
index 0000000..f0bed25
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultAsyncCacheRevalidator.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.hc.client5.http.async.AsyncExecCallback;
+import org.apache.hc.client5.http.async.AsyncExecChain;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.nio.AsyncEntityProducer;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Class used for asynchronous revalidations to be used when the {@code stale-while-revalidate}
+ * directive is present
+ */
+class DefaultAsyncCacheRevalidator extends CacheRevalidatorBase {
+
+    private static final Future<Void> NOOP_FUTURE = new Future<Void>() {
+
+        @Override
+        public Void get() throws InterruptedException, ExecutionException {
+            return null;
+        }
+
+        @Override
+        public Void get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+            return null;
+        }
+        @Override
+        public boolean cancel(final boolean mayInterruptIfRunning) {
+            return false;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public boolean isDone() {
+            return true;
+        }
+
+    };
+
+    static class InternalScheduledExecutor implements ScheduledExecutor {
+
+        private final ScheduledExecutor executor;
+
+        InternalScheduledExecutor(final ScheduledExecutor executor) {
+            this.executor = executor;
+        }
+
+        @Override
+        public Future<?> schedule(final Runnable command, final TimeValue timeValue) throws RejectedExecutionException {
+            if (timeValue.toMillis() <= 0) {
+                command.run();
+                return NOOP_FUTURE;
+            } else {
+                return executor.schedule(command, timeValue);
+            }
+        }
+
+        @Override
+        public void shutdown() {
+            executor.shutdown();
+        }
+
+        @Override
+        public void awaitTermination(final Timeout timeout) throws InterruptedException {
+            executor.awaitTermination(timeout);
+        }
+
+    }
+
+    private final AsyncCachingExec cachingExec;
+    private final CacheKeyGenerator cacheKeyGenerator;
+
+    /**
+     * Create DefaultCacheRevalidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledExecutor}.
+     */
+    public DefaultAsyncCacheRevalidator(
+            final ScheduledExecutor scheduledExecutor,
+            final SchedulingStrategy schedulingStrategy,
+            final AsyncCachingExec cachingExec) {
+        super(new InternalScheduledExecutor(scheduledExecutor), schedulingStrategy);
+        this.cachingExec = cachingExec;
+        this.cacheKeyGenerator = CacheKeyGenerator.INSTANCE;
+
+    }
+
+    /**
+     * Create CacheValidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledThreadPoolExecutor}.
+     */
+    public DefaultAsyncCacheRevalidator(
+            final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor,
+            final SchedulingStrategy schedulingStrategy,
+            final AsyncCachingExec cachingExec) {
+        this(wrap(scheduledThreadPoolExecutor), schedulingStrategy, cachingExec);
+    }
+
+    /**
+     * Schedules an asynchronous re-validation
+     */
+    public void revalidateCacheEntry(
+            final HttpHost target,
+            final HttpRequest request,
+            final AsyncEntityProducer entityProducer,
+            final AsyncExecChain.Scope scope,
+            final AsyncExecChain chain,
+            final AsyncExecCallback asyncExecCallback,
+            final HttpCacheEntry entry) {
+        final String cacheKey = cacheKeyGenerator.generateVariantURI(target, request, entry);
+        scheduleRevalidation(cacheKey, new Runnable() {
+
+                        @Override
+                        public void run() {
+                            cachingExec.revalidateCacheEntry(target, request, entityProducer, scope, chain, asyncExecCallback, entry);
+                        }
+
+                    });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheRevalidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheRevalidator.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheRevalidator.java
new file mode 100644
index 0000000..8c752b8
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultCacheRevalidator.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.classic.ExecChain;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+
+/**
+ * Class used for asynchronous revalidations to be used when
+ * the {@code stale-while-revalidate} directive is present
+ */
+class DefaultCacheRevalidator extends CacheRevalidatorBase {
+
+    private final CachingExec cachingExec;
+    private final CacheKeyGenerator cacheKeyGenerator;
+
+    /**
+     * Create DefaultCacheRevalidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledExecutor}.
+     */
+    public DefaultCacheRevalidator(
+            final CacheRevalidatorBase.ScheduledExecutor scheduledExecutor,
+            final SchedulingStrategy schedulingStrategy,
+            final CachingExec cachingExec) {
+        super(scheduledExecutor, schedulingStrategy);
+        this.cachingExec = cachingExec;
+        this.cacheKeyGenerator = CacheKeyGenerator.INSTANCE;
+
+    }
+
+    /**
+     * Create CacheValidator which will make ache revalidation requests
+     * using the supplied {@link SchedulingStrategy} and {@link ScheduledThreadPoolExecutor}.
+     */
+    public DefaultCacheRevalidator(
+            final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor,
+            final SchedulingStrategy schedulingStrategy,
+            final CachingExec cachingExec) {
+        this(wrap(scheduledThreadPoolExecutor), schedulingStrategy, cachingExec);
+    }
+
+    /**
+     * Schedules an asynchronous re-validation
+     */
+    public void revalidateCacheEntry(
+            final HttpHost target,
+            final ClassicHttpRequest request,
+            final ExecChain.Scope scope,
+            final ExecChain chain,
+            final HttpCacheEntry entry) {
+        final String cacheKey = cacheKeyGenerator.generateVariantURI(target, request, entry);
+        scheduleRevalidation(cacheKey, new Runnable() {
+
+                        @Override
+                        public void run() {
+                            try {
+                                try (ClassicHttpResponse httpResponse = cachingExec.revalidateCacheEntry(target, request, scope, chain, entry)) {
+                                    if (httpResponse.getCode() < HttpStatus.SC_SERVER_ERROR && !isStale(httpResponse)) {
+                                        jobSuccessful(cacheKey);
+                                    } else {
+                                        jobFailed(cacheKey);
+                                    }
+                                }
+                            } catch (final IOException ioe) {
+                                jobFailed(cacheKey);
+                                log.debug("Asynchronous revalidation failed due to I/O error", ioe);
+                            } catch (final HttpException pe) {
+                                jobFailed(cacheKey);
+                                log.error("HTTP protocol exception during asynchronous revalidation", pe);
+                            } catch (final RuntimeException re) {
+                                jobFailed(cacheKey);
+                                log.error("Unexpected runtime exception thrown during asynchronous revalidation" + re);
+                            }
+
+                        }
+
+                    });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidationRequest.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidationRequest.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidationRequest.java
deleted file mode 100644
index 227f240..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidationRequest.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.cache.HeaderConstants;
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.client5.http.classic.ExecRuntime;
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.ClassicHttpResponse;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.ProtocolException;
-import org.apache.hc.core5.http.message.BasicHeader;
-import org.junit.Before;
-import org.junit.Test;
-
-@SuppressWarnings({"boxing","static-access"}) // test code
-public class TestAsynchronousValidationRequest {
-
-    private AsynchronousValidator mockParent;
-    private CachingExec mockClient;
-    private HttpHost host;
-    private HttpRoute route;
-    private ClassicHttpRequest request;
-    private HttpClientContext context;
-    private ExecChain.Scope scope;
-    private ExecChain execChain;
-    private ExecRuntime mockEndpoint;
-    private HttpCacheEntry mockCacheEntry;
-    private ClassicHttpResponse mockResponse;
-
-    @Before
-    public void setUp() {
-        mockParent = mock(AsynchronousValidator.class);
-        mockClient = mock(CachingExec.class);
-        host = new HttpHost("foo.example.com", 80);
-        route = new HttpRoute(host);
-        request = new HttpGet("/");
-        context = HttpClientContext.create();
-        mockEndpoint = mock(ExecRuntime.class);
-        execChain = mock(ExecChain.class);
-        mockCacheEntry = mock(HttpCacheEntry.class);
-        mockResponse = mock(ClassicHttpResponse.class);
-        scope = new ExecChain.Scope("test", route, request, mockEndpoint, context);
-    }
-
-    @Test
-    public void testRunCallsCachingClientAndRemovesIdentifier() throws Exception {
-        final String identifier = "foo";
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry)).thenReturn(mockResponse);
-        when(mockResponse.getCode()).thenReturn(200);
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobSuccessful(identifier);
-    }
-
-    @Test
-    public void testRunReportsJobFailedForServerError() throws Exception {
-        final String identifier = "foo";
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry)).thenReturn(mockResponse);
-        when(mockResponse.getCode()).thenReturn(200);
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobSuccessful(identifier);
-    }
-
-    @Test
-    public void testRunReportsJobFailedForStaleResponse() throws Exception {
-        final String identifier = "foo";
-        final Header[] warning = new Header[] {new BasicHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"")};
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry))
-                .thenReturn(mockResponse);
-        when(mockResponse.getCode()).thenReturn(200);
-        when(mockResponse.getHeaders(HeaderConstants.WARNING)).thenReturn(warning);
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry);
-        verify(mockResponse).getHeaders(HeaderConstants.WARNING);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobFailed(identifier);
-    }
-
-    @Test
-    public void testRunGracefullyHandlesProtocolException() throws Exception {
-        final String identifier = "foo";
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry))
-                .thenThrow(new ProtocolException());
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobFailed(identifier);
-    }
-
-    @Test
-    public void testRunGracefullyHandlesIOException() throws Exception {
-        final String identifier = "foo";
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry))
-                .thenThrow(new IOException());
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobFailed(identifier);
-    }
-
-    @Test
-    public void testRunGracefullyHandlesRuntimeException() throws Exception {
-        final String identifier = "foo";
-
-        final AsynchronousValidationRequest impl = new AsynchronousValidationRequest(
-                mockParent, mockClient, host, request, scope, execChain, mockCacheEntry,
-                identifier, 0);
-
-        when(mockClient.revalidateCacheEntry(host, request, scope, execChain, mockCacheEntry))
-                .thenThrow(new RuntimeException());
-
-        impl.run();
-
-        verify(mockClient).revalidateCacheEntry(
-                host, request, scope, execChain, mockCacheEntry);
-        verify(mockParent).markComplete(identifier);
-        verify(mockParent).jobFailed(identifier);
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
deleted file mode 100644
index 8450fa3..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAsynchronousValidator.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.cache.HeaderConstants;
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.classic.ExecChain;
-import org.apache.hc.client5.http.classic.ExecRuntime;
-import org.apache.hc.client5.http.classic.methods.HttpGet;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.schedule.SchedulingStrategy;
-import org.apache.hc.core5.http.ClassicHttpRequest;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.message.BasicHeader;
-import org.apache.hc.core5.http.message.BasicHeaderIterator;
-import org.apache.hc.core5.util.TimeValue;
-import org.apache.hc.core5.util.Timeout;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TestAsynchronousValidator {
-
-    @Mock
-    private CachingExec mockClient;
-    @Mock
-    private ExecChain mockExecChain;
-    @Mock
-    private ExecRuntime mockEndpoint;
-    @Mock
-    private HttpCacheEntry mockCacheEntry;
-    @Mock
-    private SchedulingStrategy mockSchedulingStrategy;
-    @Mock
-    ScheduledExecutorService mockExecutorService;
-
-    private HttpHost host;
-    private HttpRoute route;
-    private ClassicHttpRequest request;
-    private HttpClientContext context;
-    private ExecChain.Scope scope;
-    private AsynchronousValidator impl;
-
-
-    @Before
-    public void setUp() {
-        host = new HttpHost("foo.example.com", 80);
-        route = new HttpRoute(host);
-        request = new HttpGet("/");
-        context = HttpClientContext.create();
-        scope = new ExecChain.Scope("test", route, request, mockEndpoint, context);
-        impl = new AsynchronousValidator(mockExecutorService, mockSchedulingStrategy);
-    }
-
-    @Test
-    public void testRevalidateCacheEntrySchedulesExecutionAndPopulatesIdentifier() {
-        when(mockCacheEntry.hasVariants()).thenReturn(false);
-        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(1));
-
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-
-        verify(mockCacheEntry).hasVariants();
-        verify(mockSchedulingStrategy).schedule(0);
-        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(1L), Mockito.eq(TimeUnit.SECONDS));
-
-        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
-    }
-
-    @Test
-    public void testMarkCompleteRemovesIdentifier() {
-        when(mockCacheEntry.hasVariants()).thenReturn(false);
-        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(3));
-
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-
-        verify(mockCacheEntry).hasVariants();
-        verify(mockSchedulingStrategy).schedule(0);
-        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(3L), Mockito.eq(TimeUnit.SECONDS));
-
-        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
-        final String cacheKey = CacheKeyGenerator.INSTANCE.generateVariantURI(host, request, mockCacheEntry);
-        Assert.assertTrue(impl.getScheduledIdentifiers().contains(cacheKey));
-
-        impl.markComplete(cacheKey);
-
-        Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
-    }
-
-    @Test
-    public void testRevalidateCacheEntryDoesNotPopulateIdentifierOnRejectedExecutionException() {
-        when(mockCacheEntry.hasVariants()).thenReturn(false);
-        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
-        doThrow(new RejectedExecutionException()).when(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.anyLong(), Mockito.<TimeUnit>any());
-
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-
-        verify(mockCacheEntry).hasVariants();
-
-        Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
-        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
-    }
-
-    @Test
-    public void testRevalidateCacheEntryProperlyCollapsesRequest() {
-        when(mockCacheEntry.hasVariants()).thenReturn(false);
-        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
-
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-        impl.revalidateCacheEntry(mockClient, host, request, scope, mockExecChain, mockCacheEntry);
-
-        verify(mockCacheEntry, Mockito.times(2)).hasVariants();
-        verify(mockSchedulingStrategy).schedule(Mockito.anyInt());
-        verify(mockExecutorService).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
-
-        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
-    }
-
-    @Test
-    public void testVariantsBothRevalidated() {
-        final ClassicHttpRequest req1 = new HttpGet("/");
-        req1.addHeader(new BasicHeader("Accept-Encoding", "identity"));
-
-        final ClassicHttpRequest req2 = new HttpGet("/");
-        req2.addHeader(new BasicHeader("Accept-Encoding", "gzip"));
-
-        final Header[] variantHeaders = new Header[] {
-                new BasicHeader(HeaderConstants.VARY, "Accept-Encoding")
-        };
-
-        when(mockCacheEntry.hasVariants()).thenReturn(true);
-        when(mockCacheEntry.headerIterator(HeaderConstants.VARY)).thenReturn(
-                new BasicHeaderIterator(variantHeaders, HeaderConstants.VARY));
-        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
-
-        impl.revalidateCacheEntry(mockClient, host, req1, new ExecChain.Scope("test", route, req1, mockEndpoint, context),
-                mockExecChain, mockCacheEntry);
-        impl.revalidateCacheEntry(mockClient, host, req2, new ExecChain.Scope("test", route, req2, mockEndpoint, context),
-                mockExecChain, mockCacheEntry);
-
-        verify(mockCacheEntry, Mockito.times(2)).hasVariants();
-        verify(mockCacheEntry, Mockito.times(2)).headerIterator(HeaderConstants.VARY);
-        verify(mockSchedulingStrategy, Mockito.times(2)).schedule(Mockito.anyInt());
-        verify(mockExecutorService, Mockito.times(2)).schedule(Mockito.<Runnable>any(), Mockito.eq(2L), Mockito.eq(TimeUnit.SECONDS));
-
-        Assert.assertEquals(2, impl.getScheduledIdentifiers().size());
-    }
-
-    @Test
-    public void testShutdown() throws Exception {
-        impl.close();
-        impl.awaitTermination(Timeout.ofMinutes(2));
-
-        verify(mockExecutorService).shutdown();
-        verify(mockExecutorService).awaitTermination(2L, TimeUnit.MINUTES);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/7cf4240b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheRevalidatorBase.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheRevalidatorBase.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheRevalidatorBase.java
new file mode 100644
index 0000000..0326aba
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestCacheRevalidatorBase.java
@@ -0,0 +1,154 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.RejectedExecutionException;
+
+import org.apache.hc.client5.http.cache.HeaderConstants;
+import org.apache.hc.client5.http.schedule.SchedulingStrategy;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestCacheRevalidatorBase {
+
+    @Mock
+    private SchedulingStrategy mockSchedulingStrategy;
+    @Mock
+    private CacheRevalidatorBase.ScheduledExecutor mockScheduledExecutor;
+    @Mock
+    private Runnable mockOperation;
+
+    private CacheRevalidatorBase impl;
+
+
+    @Before
+    public void setUp() {
+        impl = new CacheRevalidatorBase(mockScheduledExecutor, mockSchedulingStrategy);
+    }
+
+    @Test
+    public void testRevalidateCacheEntrySchedulesExecutionAndPopulatesIdentifier() {
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(1));
+
+        final String cacheKey = "blah";
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+
+        verify(mockSchedulingStrategy).schedule(0);
+        verify(mockScheduledExecutor).schedule(Mockito.same(mockOperation), Mockito.eq(TimeValue.ofSeconds(1)));
+
+        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
+    }
+
+    @Test
+    public void testMarkCompleteRemovesIdentifier() {
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(3));
+
+        final String cacheKey = "blah";
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+
+        verify(mockSchedulingStrategy).schedule(0);
+        verify(mockScheduledExecutor).schedule(Mockito.<Runnable>any(), Mockito.eq(TimeValue.ofSeconds(3)));
+
+        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
+        Assert.assertTrue(impl.getScheduledIdentifiers().contains(cacheKey));
+
+        impl.jobSuccessful(cacheKey);
+
+        Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
+    }
+
+    @Test
+    public void testRevalidateCacheEntryDoesNotPopulateIdentifierOnRejectedExecutionException() {
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
+        doThrow(new RejectedExecutionException()).when(mockScheduledExecutor).schedule(Mockito.<Runnable>any(), Mockito.<TimeValue>any());
+
+        final String cacheKey = "blah";
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+
+        Assert.assertEquals(0, impl.getScheduledIdentifiers().size());
+        verify(mockScheduledExecutor).schedule(Mockito.<Runnable>any(), Mockito.eq(TimeValue.ofSeconds(2)));
+    }
+
+    @Test
+    public void testRevalidateCacheEntryProperlyCollapsesRequest() {
+        when(mockSchedulingStrategy.schedule(Mockito.anyInt())).thenReturn(TimeValue.ofSeconds(2));
+
+        final String cacheKey = "blah";
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+        impl.scheduleRevalidation(cacheKey, mockOperation);
+
+        verify(mockSchedulingStrategy).schedule(Mockito.anyInt());
+        verify(mockScheduledExecutor).schedule(Mockito.<Runnable>any(), Mockito.eq(TimeValue.ofSeconds(2)));
+
+        Assert.assertEquals(1, impl.getScheduledIdentifiers().size());
+    }
+
+    @Test
+    public void testStaleResponse() {
+        final HttpResponse response1 = new BasicHttpResponse(HttpStatus.SC_OK);
+        response1.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
+        Assert.assertThat(impl.isStale(response1), CoreMatchers.equalTo(true));
+
+        final HttpResponse response2 = new BasicHttpResponse(HttpStatus.SC_OK);
+        response2.addHeader(HeaderConstants.WARNING, "111 localhost \"Revalidation failed\"");
+        Assert.assertThat(impl.isStale(response2), CoreMatchers.equalTo(true));
+
+        final HttpResponse response3 = new BasicHttpResponse(HttpStatus.SC_OK);
+        response3.addHeader(HeaderConstants.WARNING, "xxx localhost \"Huh?\"");
+        Assert.assertThat(impl.isStale(response3), CoreMatchers.equalTo(false));
+
+        final HttpResponse response4 = new BasicHttpResponse(HttpStatus.SC_OK);
+        Assert.assertThat(impl.isStale(response4), CoreMatchers.equalTo(false));
+    }
+
+    @Test
+    public void testShutdown() throws Exception {
+        impl.close();
+        impl.awaitTermination(Timeout.ofMinutes(2));
+
+        verify(mockScheduledExecutor).shutdown();
+        verify(mockScheduledExecutor).awaitTermination(Timeout.ofMinutes(2));
+    }
+
+}