View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.io.IOException;
30  import java.io.InterruptedIOException;
31  import java.nio.ByteBuffer;
32  import java.time.Instant;
33  import java.util.ArrayList;
34  import java.util.Collection;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.concurrent.ScheduledExecutorService;
39  import java.util.concurrent.atomic.AtomicBoolean;
40  import java.util.concurrent.atomic.AtomicReference;
41  import java.util.function.Consumer;
42  
43  import org.apache.hc.client5.http.HttpRoute;
44  import org.apache.hc.client5.http.async.AsyncExecCallback;
45  import org.apache.hc.client5.http.async.AsyncExecChain;
46  import org.apache.hc.client5.http.async.AsyncExecChainHandler;
47  import org.apache.hc.client5.http.async.methods.SimpleBody;
48  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
49  import org.apache.hc.client5.http.cache.CacheResponseStatus;
50  import org.apache.hc.client5.http.cache.HttpCacheContext;
51  import org.apache.hc.client5.http.cache.HttpCacheEntry;
52  import org.apache.hc.client5.http.cache.ResourceIOException;
53  import org.apache.hc.client5.http.impl.ExecSupport;
54  import org.apache.hc.client5.http.protocol.HttpClientContext;
55  import org.apache.hc.client5.http.schedule.SchedulingStrategy;
56  import org.apache.hc.core5.annotation.Contract;
57  import org.apache.hc.core5.annotation.ThreadingBehavior;
58  import org.apache.hc.core5.concurrent.CancellableDependency;
59  import org.apache.hc.core5.concurrent.ComplexFuture;
60  import org.apache.hc.core5.concurrent.FutureCallback;
61  import org.apache.hc.core5.http.ContentType;
62  import org.apache.hc.core5.http.EntityDetails;
63  import org.apache.hc.core5.http.Header;
64  import org.apache.hc.core5.http.HttpException;
65  import org.apache.hc.core5.http.HttpHeaders;
66  import org.apache.hc.core5.http.HttpHost;
67  import org.apache.hc.core5.http.HttpRequest;
68  import org.apache.hc.core5.http.HttpResponse;
69  import org.apache.hc.core5.http.HttpStatus;
70  import org.apache.hc.core5.http.impl.BasicEntityDetails;
71  import org.apache.hc.core5.http.message.RequestLine;
72  import org.apache.hc.core5.http.nio.AsyncDataConsumer;
73  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
74  import org.apache.hc.core5.http.nio.CapacityChannel;
75  import org.apache.hc.core5.http.protocol.HttpCoreContext;
76  import org.apache.hc.core5.http.support.BasicRequestBuilder;
77  import org.apache.hc.core5.net.URIAuthority;
78  import org.apache.hc.core5.util.Args;
79  import org.apache.hc.core5.util.ByteArrayBuffer;
80  import org.slf4j.Logger;
81  import org.slf4j.LoggerFactory;
82  
83  /**
84   * Request executor in the request execution chain that is responsible for
85   * transparent client-side caching.
86   * <p>
87   * The current implementation is conditionally
88   * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
89   * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
90   * are obeyed too.
91   *
92   * @since 5.0
93   */
94  @Contract(threading = ThreadingBehavior.SAFE) // So long as the responseCache implementation is threadsafe
95  class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler {
96  
97      private static final Logger LOG = LoggerFactory.getLogger(AsyncCachingExec.class);
98      private final HttpAsyncCache responseCache;
99      private final DefaultAsyncCacheRevalidator cacheRevalidator;
100     private final ConditionalRequestBuilder<HttpRequest> conditionalRequestBuilder;
101 
102     AsyncCachingExec(final HttpAsyncCache cache, final DefaultAsyncCacheRevalidator cacheRevalidator, final CacheConfig config) {
103         super(config);
104         this.responseCache = Args.notNull(cache, "Response cache");
105         this.cacheRevalidator = cacheRevalidator;
106         this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(request ->
107                 BasicRequestBuilder.copy(request).build());
108     }
109 
110     AsyncCachingExec(
111             final HttpAsyncCache cache,
112             final ScheduledExecutorService executorService,
113             final SchedulingStrategy schedulingStrategy,
114             final CacheConfig config) {
115         this(cache,
116                 executorService != null ? new DefaultAsyncCacheRevalidator(executorService, schedulingStrategy) : null,
117                 config);
118     }
119 
120     private void triggerResponse(
121             final SimpleHttpResponse cacheResponse,
122             final AsyncExecChain.Scope scope,
123             final AsyncExecCallback asyncExecCallback) {
124         scope.execRuntime.releaseEndpoint();
125 
126         final SimpleBody body = cacheResponse.getBody();
127         final byte[] content = body != null ? body.getBodyBytes() : null;
128         final ContentType contentType = body != null ? body.getContentType() : null;
129         try {
130             final AsyncDataConsumer dataConsumer = asyncExecCallback.handleResponse(
131                     cacheResponse,
132                     content != null ? new BasicEntityDetails(content.length, contentType) : null);
133             if (dataConsumer != null) {
134                 if (content != null) {
135                     dataConsumer.consume(ByteBuffer.wrap(content));
136                 }
137                 dataConsumer.streamEnd(null);
138             }
139             asyncExecCallback.completed();
140         } catch (final HttpException | IOException ex) {
141             asyncExecCallback.failed(ex);
142         }
143     }
144 
145     static class AsyncExecCallbackWrapper implements AsyncExecCallback {
146 
147         private final Runnable command;
148         private final Consumer<Exception> exceptionConsumer;
149 
150         AsyncExecCallbackWrapper(final Runnable command, final Consumer<Exception> exceptionConsumer) {
151             this.command = command;
152             this.exceptionConsumer = exceptionConsumer;
153         }
154 
155         @Override
156         public AsyncDataConsumer handleResponse(
157                 final HttpResponse response,
158                 final EntityDetails entityDetails) throws HttpException, IOException {
159             return null;
160         }
161 
162         @Override
163         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
164         }
165 
166         @Override
167         public void completed() {
168             command.run();
169         }
170 
171         @Override
172         public void failed(final Exception cause) {
173             if (exceptionConsumer != null) {
174                 exceptionConsumer.accept(cause);
175             }
176         }
177 
178     }
179 
180     @Override
181     public void execute(
182             final HttpRequest request,
183             final AsyncEntityProducer entityProducer,
184             final AsyncExecChain.Scope scope,
185             final AsyncExecChain chain,
186             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
187         Args.notNull(request, "HTTP request");
188         Args.notNull(scope, "Scope");
189 
190         final HttpRoute route = scope.route;
191         final HttpClientContext context = scope.clientContext;
192 
193         final URIAuthority authority = request.getAuthority();
194         final String scheme = request.getScheme();
195         final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
196         doExecute(target,
197                 request,
198                 entityProducer,
199                 scope,
200                 chain,
201                 new AsyncExecCallback() {
202 
203                     @Override
204                     public AsyncDataConsumer handleResponse(
205                             final HttpResponse response,
206                             final EntityDetails entityDetails) throws HttpException, IOException {
207                         context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
208                         context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
209                         return asyncExecCallback.handleResponse(response, entityDetails);
210                     }
211 
212                     @Override
213                     public void handleInformationResponse(
214                             final HttpResponse response) throws HttpException, IOException {
215                         asyncExecCallback.handleInformationResponse(response);
216                     }
217 
218                     @Override
219                     public void completed() {
220                         asyncExecCallback.completed();
221                     }
222 
223                     @Override
224                     public void failed(final Exception cause) {
225                         asyncExecCallback.failed(cause);
226                     }
227 
228                 });
229     }
230 
231     public void doExecute(
232             final HttpHost target,
233             final HttpRequest request,
234             final AsyncEntityProducer entityProducer,
235             final AsyncExecChain.Scope scope,
236             final AsyncExecChain chain,
237             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
238 
239         final String exchangeId = scope.exchangeId;
240         final HttpClientContext context = scope.clientContext;
241         final CancellableDependency operation = scope.cancellableDependency;
242 
243         if (LOG.isDebugEnabled()) {
244             LOG.debug("{} request via cache: {}", exchangeId, new RequestLine(request));
245         }
246 
247         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MISS);
248 
249         if (clientRequestsOurOptions(request)) {
250             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
251             triggerResponse(SimpleHttpResponse.create(HttpStatus.SC_NOT_IMPLEMENTED), scope, asyncExecCallback);
252             return;
253         }
254 
255         final RequestCacheControl requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
256         if (LOG.isDebugEnabled()) {
257             LOG.debug("{} request cache control: {}", exchangeId, requestCacheControl);
258         }
259 
260         if (cacheableRequestPolicy.canBeServedFromCache(requestCacheControl, request)) {
261             operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
262 
263                 @Override
264                 public void completed(final CacheMatch result) {
265                     final CacheHit hit = result != null ? result.hit : null;
266                     final CacheHit root = result != null ? result.root : null;
267                     if (hit == null) {
268                         handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
269                     } else {
270                         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
271                         if (LOG.isDebugEnabled()) {
272                             LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
273                         }
274                         handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
275                     }
276                 }
277 
278                 @Override
279                 public void failed(final Exception cause) {
280                     asyncExecCallback.failed(cause);
281                 }
282 
283                 @Override
284                 public void cancelled() {
285                     asyncExecCallback.failed(new InterruptedIOException());
286                 }
287 
288             }));
289 
290         } else {
291             if (LOG.isDebugEnabled()) {
292                 LOG.debug("{} request cannot be served from cache", exchangeId);
293             }
294             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
295         }
296     }
297 
298     void chainProceed(
299             final HttpRequest request,
300             final AsyncEntityProducer entityProducer,
301             final AsyncExecChain.Scope scope,
302             final AsyncExecChain chain,
303             final AsyncExecCallback asyncExecCallback) {
304         try {
305             chain.proceed(request, entityProducer, scope, asyncExecCallback);
306         } catch (final HttpException | IOException ex) {
307             asyncExecCallback.failed(ex);
308         }
309     }
310 
311     void callBackend(
312             final HttpHost target,
313             final HttpRequest request,
314             final AsyncEntityProducer entityProducer,
315             final AsyncExecChain.Scope scope,
316             final AsyncExecChain chain,
317             final AsyncExecCallback asyncExecCallback) {
318         final String exchangeId = scope.exchangeId;
319 
320         if (LOG.isDebugEnabled()) {
321             LOG.debug("{} calling the backend", exchangeId);
322         }
323         final Instant requestDate = getCurrentDate();
324         final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
325         chainProceed(request, entityProducer, scope, chain, new AsyncExecCallback() {
326 
327             @Override
328             public AsyncDataConsumer handleResponse(
329                     final HttpResponse backendResponse,
330                     final EntityDetails entityDetails) throws HttpException, IOException {
331                 final Instant responseDate = getCurrentDate();
332                 final AsyncExecCallback callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
333                 callbackRef.set(callback);
334                 return callback.handleResponse(backendResponse, entityDetails);
335             }
336 
337             @Override
338             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
339                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
340                 if (callback != null) {
341                     callback.handleInformationResponse(response);
342                 } else {
343                     asyncExecCallback.handleInformationResponse(response);
344                 }
345             }
346 
347             @Override
348             public void completed() {
349                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
350                 if (callback != null) {
351                     callback.completed();
352                 } else {
353                     asyncExecCallback.completed();
354                 }
355             }
356 
357             @Override
358             public void failed(final Exception cause) {
359                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
360                 if (callback != null) {
361                     callback.failed(cause);
362                 } else {
363                     asyncExecCallback.failed(cause);
364                 }
365             }
366 
367         });
368     }
369 
370     class CachingAsyncDataConsumer implements AsyncDataConsumer {
371 
372         private final String exchangeId;
373         private final AsyncExecCallback fallback;
374         private final HttpResponse backendResponse;
375         private final EntityDetails entityDetails;
376         private final AtomicBoolean writtenThrough;
377         private final AtomicReference<ByteArrayBuffer> bufferRef;
378         private final AtomicReference<AsyncDataConsumer> dataConsumerRef;
379 
380         CachingAsyncDataConsumer(
381                 final String exchangeId,
382                 final AsyncExecCallback fallback,
383                 final HttpResponse backendResponse,
384                 final EntityDetails entityDetails) {
385             this.exchangeId = exchangeId;
386             this.fallback = fallback;
387             this.backendResponse = backendResponse;
388             this.entityDetails = entityDetails;
389             this.writtenThrough = new AtomicBoolean(false);
390             this.bufferRef = new AtomicReference<>(entityDetails != null ? new ByteArrayBuffer(1024) : null);
391             this.dataConsumerRef = new AtomicReference<>();
392         }
393 
394         @Override
395         public final void updateCapacity(final CapacityChannel capacityChannel) throws IOException {
396             final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
397             if (dataConsumer != null) {
398                 dataConsumer.updateCapacity(capacityChannel);
399             } else {
400                 capacityChannel.update(Integer.MAX_VALUE);
401             }
402         }
403 
404         @Override
405         public final void consume(final ByteBuffer src) throws IOException {
406             final ByteArrayBuffer buffer = bufferRef.get();
407             if (buffer != null) {
408                 if (src.hasArray()) {
409                     buffer.append(src.array(), src.arrayOffset() + src.position(), src.remaining());
410                 } else {
411                     while (src.hasRemaining()) {
412                         buffer.append(src.get());
413                     }
414                 }
415                 if (buffer.length() > cacheConfig.getMaxObjectSize()) {
416                     if (LOG.isDebugEnabled()) {
417                         LOG.debug("{} backend response content length exceeds maximum", exchangeId);
418                     }
419                     // Over the max limit. Stop buffering and forward the response
420                     // along with all the data buffered so far to the caller.
421                     bufferRef.set(null);
422                     try {
423                         final AsyncDataConsumer dataConsumer = fallback.handleResponse(backendResponse, entityDetails);
424                         if (dataConsumer != null) {
425                             dataConsumerRef.set(dataConsumer);
426                             writtenThrough.set(true);
427                             dataConsumer.consume(ByteBuffer.wrap(buffer.array(), 0, buffer.length()));
428                         }
429                     } catch (final HttpException ex) {
430                         fallback.failed(ex);
431                     }
432                 }
433             } else {
434                 final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
435                 if (dataConsumer != null) {
436                     dataConsumer.consume(src);
437                 }
438             }
439         }
440 
441         @Override
442         public final void streamEnd(final List<? extends Header> trailers) throws HttpException, IOException {
443             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
444             if (dataConsumer != null) {
445                 dataConsumer.streamEnd(trailers);
446             }
447         }
448 
449         @Override
450         public void releaseResources() {
451             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
452             if (dataConsumer != null) {
453                 dataConsumer.releaseResources();
454             }
455         }
456 
457     }
458 
459     class BackendResponseHandler implements AsyncExecCallback {
460 
461         private final HttpHost target;
462         private final HttpRequest request;
463         private final Instant requestDate;
464         private final Instant responseDate;
465         private final AsyncExecChain.Scope scope;
466         private final AsyncExecCallback asyncExecCallback;
467         private final AtomicReference<CachingAsyncDataConsumer> cachingConsumerRef;
468 
469         BackendResponseHandler(
470                 final HttpHost target,
471                 final HttpRequest request,
472                 final Instant requestDate,
473                 final Instant responseDate,
474                 final AsyncExecChain.Scope scope,
475                 final AsyncExecCallback asyncExecCallback) {
476             this.target = target;
477             this.request = request;
478             this.requestDate = requestDate;
479             this.responseDate = responseDate;
480             this.scope = scope;
481             this.asyncExecCallback = asyncExecCallback;
482             this.cachingConsumerRef = new AtomicReference<>();
483         }
484 
485         @Override
486         public AsyncDataConsumer handleResponse(
487                 final HttpResponse backendResponse,
488                 final EntityDetails entityDetails) throws HttpException, IOException {
489             final String exchangeId = scope.exchangeId;
490             responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback<Boolean>() {
491 
492                 @Override
493                 public void completed(final Boolean result) {
494                 }
495 
496                 @Override
497                 public void failed(final Exception ex) {
498                     if (LOG.isDebugEnabled()) {
499                         LOG.debug("{} unable to flush invalidated entries from cache", exchangeId, ex);
500                     }
501                 }
502 
503                 @Override
504                 public void cancelled() {
505                 }
506 
507             });
508             if (isResponseTooBig(entityDetails)) {
509                 if (LOG.isDebugEnabled()) {
510                     LOG.debug("{} backend response is known to be too big", exchangeId);
511                 }
512                 return asyncExecCallback.handleResponse(backendResponse, entityDetails);
513             }
514 
515             final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse);
516             final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse);
517             if (cacheable) {
518                 cachingConsumerRef.set(new CachingAsyncDataConsumer(exchangeId, asyncExecCallback, backendResponse, entityDetails));
519                 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
520             } else {
521                 if (LOG.isDebugEnabled()) {
522                     LOG.debug("{} backend response is not cacheable", exchangeId);
523                 }
524             }
525             final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.get();
526             if (cachingDataConsumer != null) {
527                 if (LOG.isDebugEnabled()) {
528                     LOG.debug("{} caching backend response", exchangeId);
529                 }
530                 return cachingDataConsumer;
531             }
532             return asyncExecCallback.handleResponse(backendResponse, entityDetails);
533         }
534 
535         @Override
536         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
537             asyncExecCallback.handleInformationResponse(response);
538         }
539 
540         void triggerNewCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate, final ByteArrayBuffer buffer) {
541             final String exchangeId = scope.exchangeId;
542             final CancellableDependency operation = scope.cancellableDependency;
543             operation.setDependency(responseCache.store(
544                     target,
545                     request,
546                     backendResponse,
547                     buffer,
548                     requestDate,
549                     responseDate,
550                     new FutureCallback<CacheHit>() {
551 
552                         @Override
553                         public void completed(final CacheHit hit) {
554                             if (LOG.isDebugEnabled()) {
555                                 LOG.debug("{} backend response successfully cached", exchangeId);
556                             }
557                             try {
558                                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
559                                 triggerResponse(cacheResponse, scope, asyncExecCallback);
560                             } catch (final ResourceIOException ex) {
561                                 asyncExecCallback.failed(ex);
562                             }
563                         }
564 
565                         @Override
566                         public void failed(final Exception ex) {
567                             asyncExecCallback.failed(ex);
568                         }
569 
570                         @Override
571                         public void cancelled() {
572                             asyncExecCallback.failed(new InterruptedIOException());
573                         }
574 
575                     }));
576 
577         }
578 
579         @Override
580         public void completed() {
581             final String exchangeId = scope.exchangeId;
582             final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.getAndSet(null);
583             if (cachingDataConsumer != null && !cachingDataConsumer.writtenThrough.get()) {
584                 final ByteArrayBuffer buffer = cachingDataConsumer.bufferRef.getAndSet(null);
585                 final HttpResponse backendResponse = cachingDataConsumer.backendResponse;
586                 if (cacheConfig.isFreshnessCheckEnabled()) {
587                     final CancellableDependency operation = scope.cancellableDependency;
588                     operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
589 
590                         @Override
591                         public void completed(final CacheMatch result) {
592                             final CacheHit hit = result != null ? result.hit : null;
593                             if (HttpCacheEntry.isNewer(hit != null ? hit.entry : null, backendResponse)) {
594                                 if (LOG.isDebugEnabled()) {
595                                     LOG.debug("{} backend already contains fresher cache entry", exchangeId);
596                                 }
597                                 try {
598                                     final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
599                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
600                                 } catch (final ResourceIOException ex) {
601                                     asyncExecCallback.failed(ex);
602                                 }
603                             } else {
604                                 triggerNewCacheEntryResponse(backendResponse, responseDate, buffer);
605                             }
606                         }
607 
608                         @Override
609                         public void failed(final Exception cause) {
610                             asyncExecCallback.failed(cause);
611                         }
612 
613                         @Override
614                         public void cancelled() {
615                             asyncExecCallback.failed(new InterruptedIOException());
616                         }
617 
618                     }));
619                 } else {
620                     triggerNewCacheEntryResponse(backendResponse, responseDate, buffer);
621                 }
622             } else {
623                 asyncExecCallback.completed();
624             }
625         }
626 
627         @Override
628         public void failed(final Exception cause) {
629             asyncExecCallback.failed(cause);
630         }
631 
632     }
633 
634     private void handleCacheHit(
635             final RequestCacheControl requestCacheControl,
636             final ResponseCacheControl responseCacheControl,
637             final CacheHit hit,
638             final HttpHost target,
639             final HttpRequest request,
640             final AsyncEntityProducer entityProducer,
641             final AsyncExecChain.Scope scope,
642             final AsyncExecChain chain,
643             final AsyncExecCallback asyncExecCallback) {
644         final HttpClientContext context  = scope.clientContext;
645         final String exchangeId = scope.exchangeId;
646 
647         if (LOG.isDebugEnabled()) {
648             LOG.debug("{} cache hit: {}", exchangeId, new RequestLine(request));
649         }
650 
651         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_HIT);
652         cacheHits.getAndIncrement();
653 
654         final Instant now = getCurrentDate();
655 
656         final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
657         if (LOG.isDebugEnabled()) {
658             LOG.debug("{} cache suitability: {}", exchangeId, cacheSuitability);
659         }
660         if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
661             if (LOG.isDebugEnabled()) {
662                 LOG.debug("{} cache hit is fresh enough", exchangeId);
663             }
664             try {
665                 final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now);
666                 triggerResponse(cacheResponse, scope, asyncExecCallback);
667             } catch (final ResourceIOException ex) {
668                 if (requestCacheControl.isOnlyIfCached()) {
669                     if (LOG.isDebugEnabled()) {
670                         LOG.debug("{} request marked only-if-cached", exchangeId);
671                     }
672                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
673                     final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
674                     triggerResponse(cacheResponse, scope, asyncExecCallback);
675                 } else {
676                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.FAILURE);
677                     try {
678                         chain.proceed(request, entityProducer, scope, asyncExecCallback);
679                     } catch (final HttpException | IOException ex2) {
680                         asyncExecCallback.failed(ex2);
681                     }
682                 }
683             }
684         } else {
685             if (requestCacheControl.isOnlyIfCached()) {
686                 if (LOG.isDebugEnabled()) {
687                     LOG.debug("{} cache entry not is not fresh and only-if-cached requested", exchangeId);
688                 }
689                 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
690                 final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
691                 triggerResponse(cacheResponse, scope, asyncExecCallback);
692             } else if (cacheSuitability == CacheSuitability.MISMATCH) {
693                 if (LOG.isDebugEnabled()) {
694                     LOG.debug("{} cache entry does not match the request; calling backend", exchangeId);
695                 }
696                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
697             } else if (entityProducer != null && !entityProducer.isRepeatable()) {
698                 if (LOG.isDebugEnabled()) {
699                     LOG.debug("{} request is not repeatable; calling backend", exchangeId);
700                 }
701                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
702             } else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
703                 if (LOG.isDebugEnabled()) {
704                     LOG.debug("{} non-modified cache entry does not match the non-conditional request; calling backend", exchangeId);
705                 }
706                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
707             } else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
708                 if (LOG.isDebugEnabled()) {
709                     LOG.debug("{} revalidation required; revalidating cache entry", exchangeId);
710                 }
711                 revalidateCacheEntryWithoutFallback(responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
712             } else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
713                 if (cacheRevalidator != null) {
714                     if (LOG.isDebugEnabled()) {
715                         LOG.debug("{} serving stale with asynchronous revalidation", exchangeId);
716                     }
717                     try {
718                         final String revalidationExchangeId = ExecSupport.getNextExchangeId();
719                         context.setExchangeId(revalidationExchangeId);
720                         final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
721                                 revalidationExchangeId,
722                                 scope.route,
723                                 scope.originalRequest,
724                                 new ComplexFuture<>(null),
725                                 HttpClientContext.create(),
726                                 scope.execRuntime.fork(),
727                                 scope.scheduler,
728                                 scope.execCount);
729                         if (LOG.isDebugEnabled()) {
730                             LOG.debug("{} starting asynchronous revalidation exchange {}", exchangeId, revalidationExchangeId);
731                         }
732                         cacheRevalidator.revalidateCacheEntry(
733                                 hit.getEntryKey(),
734                                 asyncExecCallback,
735                                 c -> revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, fork, chain, c));
736                         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
737                         final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry);
738                         triggerResponse(cacheResponse, scope, asyncExecCallback);
739                     } catch (final IOException ex) {
740                         asyncExecCallback.failed(ex);
741                     }
742                 } else {
743                     if (LOG.isDebugEnabled()) {
744                         LOG.debug("{} revalidating stale cache entry (asynchronous revalidation disabled)", exchangeId);
745                     }
746                     revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
747                 }
748             } else if (cacheSuitability == CacheSuitability.STALE) {
749                 if (LOG.isDebugEnabled()) {
750                     LOG.debug("{} revalidating stale cache entry", exchangeId);
751                 }
752                 revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
753             } else {
754                 if (LOG.isDebugEnabled()) {
755                     LOG.debug("{} cache entry not usable; calling backend", exchangeId);
756                 }
757                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
758             }
759         }
760     }
761 
762     void revalidateCacheEntry(
763             final ResponseCacheControl responseCacheControl,
764             final CacheHit hit,
765             final HttpHost target,
766             final HttpRequest request,
767             final AsyncEntityProducer entityProducer,
768             final AsyncExecChain.Scope scope,
769             final AsyncExecChain chain,
770             final AsyncExecCallback asyncExecCallback) {
771         final Instant requestDate = getCurrentDate();
772         final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
773                 responseCacheControl,
774                 BasicRequestBuilder.copy(request).build(),
775                 hit.entry);
776         final HttpClientContext context = scope.clientContext;
777         chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
778 
779             final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
780 
781             void triggerUpdatedCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate) {
782                 final CancellableDependency operation = scope.cancellableDependency;
783                 operation.setDependency(responseCache.update(
784                         hit,
785                         target,
786                         request,
787                         backendResponse,
788                         requestDate,
789                         responseDate,
790                         new FutureCallback<CacheHit>() {
791 
792                             @Override
793                             public void completed(final CacheHit updated) {
794                                 try {
795                                     final SimpleHttpResponse cacheResponse = generateCachedResponse(request, updated.entry, responseDate);
796                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
797                                 } catch (final ResourceIOException ex) {
798                                     asyncExecCallback.failed(ex);
799                                 }
800                             }
801 
802                             @Override
803                             public void failed(final Exception ex) {
804                                 asyncExecCallback.failed(ex);
805                             }
806 
807                             @Override
808                             public void cancelled() {
809                                 asyncExecCallback.failed(new InterruptedIOException());
810                             }
811 
812                         }));
813             }
814 
815             AsyncExecCallback evaluateResponse(final HttpResponse backendResponse, final Instant responseDate) {
816                 final int statusCode = backendResponse.getCode();
817                 if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
818                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.VALIDATED);
819                     cacheUpdates.getAndIncrement();
820                 }
821                 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
822                     return new AsyncExecCallbackWrapper(() -> triggerUpdatedCacheEntryResponse(backendResponse, responseDate), asyncExecCallback::failed);
823                 }
824                 return new BackendResponseHandler(target, conditionalRequest, requestDate, responseDate, scope, asyncExecCallback);
825             }
826 
827             @Override
828             public AsyncDataConsumer handleResponse(
829                     final HttpResponse backendResponse1,
830                     final EntityDetails entityDetails) throws HttpException, IOException {
831 
832                 final Instant responseDate = getCurrentDate();
833 
834                 final AsyncExecCallback callback1;
835                 if (HttpCacheEntry.isNewer(hit.entry, backendResponse1)) {
836 
837                     final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
838                             BasicRequestBuilder.copy(scope.originalRequest).build());
839 
840                     callback1 = new AsyncExecCallbackWrapper(() -> chainProceed(unconditional, entityProducer, scope, chain, new AsyncExecCallback() {
841 
842                         @Override
843                         public AsyncDataConsumer handleResponse(
844                                 final HttpResponse backendResponse2,
845                                 final EntityDetails entityDetails1) throws HttpException, IOException {
846                             final Instant responseDate2 = getCurrentDate();
847                             final AsyncExecCallback callback2 = evaluateResponse(backendResponse2, responseDate2);
848                             callbackRef.set(callback2);
849                             return callback2.handleResponse(backendResponse2, entityDetails1);
850                         }
851 
852                         @Override
853                         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
854                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
855                             if (callback2 != null) {
856                                 callback2.handleInformationResponse(response);
857                             } else {
858                                 asyncExecCallback.handleInformationResponse(response);
859                             }
860                         }
861 
862                         @Override
863                         public void completed() {
864                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
865                             if (callback2 != null) {
866                                 callback2.completed();
867                             } else {
868                                 asyncExecCallback.completed();
869                             }
870                         }
871 
872                         @Override
873                         public void failed(final Exception cause) {
874                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
875                             if (callback2 != null) {
876                                 callback2.failed(cause);
877                             } else {
878                                 asyncExecCallback.failed(cause);
879                             }
880                         }
881 
882                     }), asyncExecCallback::failed);
883                 } else {
884                     callback1 = evaluateResponse(backendResponse1, responseDate);
885                 }
886                 callbackRef.set(callback1);
887                 return callback1.handleResponse(backendResponse1, entityDetails);
888             }
889 
890             @Override
891             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
892                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
893                 if (callback1 != null) {
894                     callback1.handleInformationResponse(response);
895                 } else {
896                     asyncExecCallback.handleInformationResponse(response);
897                 }
898             }
899 
900             @Override
901             public void completed() {
902                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
903                 if (callback1 != null) {
904                     callback1.completed();
905                 } else {
906                     asyncExecCallback.completed();
907                 }
908             }
909 
910             @Override
911             public void failed(final Exception cause) {
912                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
913                 if (callback1 != null) {
914                     callback1.failed(cause);
915                 } else {
916                     asyncExecCallback.failed(cause);
917                 }
918             }
919 
920         });
921 
922     }
923 
924     void revalidateCacheEntryWithoutFallback(
925             final ResponseCacheControl responseCacheControl,
926             final CacheHit hit,
927             final HttpHost target,
928             final HttpRequest request,
929             final AsyncEntityProducer entityProducer,
930             final AsyncExecChain.Scope scope,
931             final AsyncExecChain chain,
932             final AsyncExecCallback asyncExecCallback) {
933         final String exchangeId = scope.exchangeId;
934         final HttpClientContext context = scope.clientContext;
935         revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
936 
937             private final AtomicBoolean committed = new AtomicBoolean();
938 
939             @Override
940             public AsyncDataConsumer handleResponse(final HttpResponse response,
941                                                     final EntityDetails entityDetails) throws HttpException, IOException {
942                 committed.set(true);
943                 return asyncExecCallback.handleResponse(response, entityDetails);
944             }
945 
946             @Override
947             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
948                 asyncExecCallback.handleInformationResponse(response);
949             }
950 
951             @Override
952             public void completed() {
953                 asyncExecCallback.completed();
954             }
955 
956             @Override
957             public void failed(final Exception cause) {
958                 if (!committed.get() && cause instanceof IOException) {
959                     if (LOG.isDebugEnabled()) {
960                         LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
961                     }
962                     final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
963                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
964                     triggerResponse(cacheResponse, scope, asyncExecCallback);
965                 } else {
966                     asyncExecCallback.failed(cause);
967                 }
968             }
969 
970         });
971     }
972 
973     void revalidateCacheEntryWithFallback(
974             final RequestCacheControl requestCacheControl,
975             final ResponseCacheControl responseCacheControl,
976             final CacheHit hit,
977             final HttpHost target,
978             final HttpRequest request,
979             final AsyncEntityProducer entityProducer,
980             final AsyncExecChain.Scope scope,
981             final AsyncExecChain chain,
982             final AsyncExecCallback asyncExecCallback) {
983         final String exchangeId = scope.exchangeId;
984         final HttpClientContext context = scope.clientContext;
985         revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
986 
987             private final AtomicReference<HttpResponse> committed = new AtomicReference<>();
988 
989             @Override
990             public AsyncDataConsumer handleResponse(final HttpResponse response, final EntityDetails entityDetails) throws HttpException, IOException {
991                 final int status = response.getCode();
992                 if (staleIfErrorAppliesTo(status) &&
993                         suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
994                     if (LOG.isDebugEnabled()) {
995                         LOG.debug("{} serving stale response due to {} status and stale-if-error enabled", exchangeId, status);
996                     }
997                     return null;
998                 } else {
999                     committed.set(response);
1000                     return asyncExecCallback.handleResponse(response, entityDetails);
1001                 }
1002             }
1003 
1004             @Override
1005             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1006                 asyncExecCallback.handleInformationResponse(response);
1007             }
1008 
1009             @Override
1010             public void completed() {
1011                 final HttpResponse response = committed.get();
1012                 if (response == null) {
1013                     try {
1014                         context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
1015                         final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry);
1016                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1017                     } catch (final IOException ex) {
1018                         asyncExecCallback.failed(ex);
1019                     }
1020                 } else {
1021                     asyncExecCallback.completed();
1022                 }
1023             }
1024 
1025             @Override
1026             public void failed(final Exception cause) {
1027                 final HttpResponse response = committed.get();
1028                 if (response == null) {
1029                     if (LOG.isDebugEnabled()) {
1030                         LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
1031                     }
1032                     context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
1033                     if (cause instanceof IOException &&
1034                             suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
1035                         if (LOG.isDebugEnabled()) {
1036                             LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId);
1037                         }
1038                         try {
1039                             final SimpleHttpResponse cacheResponse = unvalidatedCacheHit(request, hit.entry);
1040                             triggerResponse(cacheResponse, scope, asyncExecCallback);
1041                         } catch (final IOException ex) {
1042                             asyncExecCallback.failed(cause);
1043                         }
1044                     } else {
1045                         final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1046                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1047                     }
1048                 } else {
1049                     asyncExecCallback.failed(cause);
1050                 }
1051             }
1052 
1053         });
1054     }
1055     private void handleCacheMiss(
1056             final RequestCacheControl requestCacheControl,
1057             final CacheHit partialMatch,
1058             final HttpHost target,
1059             final HttpRequest request,
1060             final AsyncEntityProducer entityProducer,
1061             final AsyncExecChain.Scope scope,
1062             final AsyncExecChain chain,
1063             final AsyncExecCallback asyncExecCallback) {
1064         final String exchangeId = scope.exchangeId;
1065 
1066         if (LOG.isDebugEnabled()) {
1067             LOG.debug("{} cache miss: {}", exchangeId, new RequestLine(request));
1068         }
1069         cacheMisses.getAndIncrement();
1070 
1071         final CancellableDependency operation = scope.cancellableDependency;
1072         if (requestCacheControl.isOnlyIfCached()) {
1073             if (LOG.isDebugEnabled()) {
1074                 LOG.debug("{} request marked only-if-cached", exchangeId);
1075             }
1076             final HttpClientContext context = scope.clientContext;
1077             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.CACHE_MODULE_RESPONSE);
1078             final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1079             triggerResponse(cacheResponse, scope, asyncExecCallback);
1080         }
1081 
1082         if (partialMatch != null && partialMatch.entry.hasVariants() && entityProducer == null) {
1083             operation.setDependency(responseCache.getVariants(
1084                     partialMatch,
1085                     new FutureCallback<Collection<CacheHit>>() {
1086 
1087                         @Override
1088                         public void completed(final Collection<CacheHit> variants) {
1089                             if (variants != null && !variants.isEmpty()) {
1090                                 negotiateResponseFromVariants(target, request, entityProducer, scope, chain, asyncExecCallback, variants);
1091                             } else {
1092                                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1093                             }
1094                         }
1095 
1096                         @Override
1097                         public void failed(final Exception ex) {
1098                             asyncExecCallback.failed(ex);
1099                         }
1100 
1101                         @Override
1102                         public void cancelled() {
1103                             asyncExecCallback.failed(new InterruptedIOException());
1104                         }
1105 
1106                     }));
1107         } else {
1108             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1109         }
1110     }
1111 
1112     void negotiateResponseFromVariants(
1113             final HttpHost target,
1114             final HttpRequest request,
1115             final AsyncEntityProducer entityProducer,
1116             final AsyncExecChain.Scope scope,
1117             final AsyncExecChain chain,
1118             final AsyncExecCallback asyncExecCallback,
1119             final Collection<CacheHit> variants) {
1120         final String exchangeId = scope.exchangeId;
1121         final CancellableDependency operation = scope.cancellableDependency;
1122         final Map<String, CacheHit> variantMap = new HashMap<>();
1123         for (final CacheHit variant : variants) {
1124             final Header header = variant.entry.getFirstHeader(HttpHeaders.ETAG);
1125             if (header != null) {
1126                 variantMap.put(header.getValue(), variant);
1127             }
1128         }
1129         final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
1130                 BasicRequestBuilder.copy(request).build(),
1131                 new ArrayList<>(variantMap.keySet()));
1132 
1133         final Instant requestDate = getCurrentDate();
1134         chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
1135 
1136             final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
1137 
1138             void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final CacheHit match) {
1139                 final HttpClientContext context = scope.clientContext;
1140                 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, CacheResponseStatus.VALIDATED);
1141                 cacheUpdates.getAndIncrement();
1142 
1143                 operation.setDependency(responseCache.storeFromNegotiated(
1144                         match,
1145                         target,
1146                         request,
1147                         backendResponse,
1148                         requestDate,
1149                         responseDate,
1150                         new FutureCallback<CacheHit>() {
1151 
1152                             @Override
1153                             public void completed(final CacheHit hit) {
1154                                 if (shouldSendNotModifiedResponse(request, hit.entry, Instant.now())) {
1155                                     final SimpleHttpResponse cacheResponse = responseGenerator.generateNotModifiedResponse(hit.entry);
1156                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
1157                                 } else {
1158                                     try {
1159                                         final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
1160                                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1161                                     } catch (final ResourceIOException ex) {
1162                                         asyncExecCallback.failed(ex);
1163                                     }
1164                                 }
1165                             }
1166 
1167                             @Override
1168                             public void failed(final Exception ex) {
1169                                 asyncExecCallback.failed(ex);
1170                             }
1171 
1172                             @Override
1173                             public void cancelled() {
1174                                 asyncExecCallback.failed(new InterruptedIOException());
1175                             }
1176 
1177                         }));
1178             }
1179 
1180             @Override
1181             public AsyncDataConsumer handleResponse(
1182                     final HttpResponse backendResponse,
1183                     final EntityDetails entityDetails) throws HttpException, IOException {
1184                 final Instant responseDate = getCurrentDate();
1185                 final AsyncExecCallback callback;
1186                 // Handle 304 Not Modified responses
1187                 if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
1188                     responseCache.match(target, request, new FutureCallback<CacheMatch>() {
1189                         @Override
1190                         public void completed(final CacheMatch result) {
1191                             final CacheHit hit = result != null ? result.hit : null;
1192                             if (hit != null) {
1193                                 if (LOG.isDebugEnabled()) {
1194                                     LOG.debug("{} existing cache entry found, updating cache entry", exchangeId);
1195                                 }
1196                                 responseCache.update(
1197                                         hit,
1198                                         target,
1199                                         request,
1200                                         backendResponse,
1201                                         requestDate,
1202                                         responseDate,
1203                                         new FutureCallback<CacheHit>() {
1204 
1205                                             @Override
1206                                             public void completed(final CacheHit updated) {
1207                                                 try {
1208                                                     if (LOG.isDebugEnabled()) {
1209                                                         LOG.debug("{} cache entry updated, generating response from updated entry", exchangeId);
1210                                                     }
1211                                                     final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, updated.entry);
1212                                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
1213                                                 } catch (final ResourceIOException ex) {
1214                                                     asyncExecCallback.failed(ex);
1215                                                 }
1216                                             }
1217                                             @Override
1218                                             public void failed(final Exception cause) {
1219                                                 if (LOG.isDebugEnabled()) {
1220                                                     LOG.debug("{} request failed: {}", exchangeId, cause.getMessage());
1221                                                 }
1222                                                 asyncExecCallback.failed(cause);
1223                                             }
1224 
1225                                             @Override
1226                                             public void cancelled() {
1227                                                 if (LOG.isDebugEnabled()) {
1228                                                     LOG.debug("{} cache entry updated aborted", exchangeId);
1229                                                 }
1230                                                 asyncExecCallback.failed(new InterruptedIOException());
1231                                             }
1232 
1233                                         });
1234                             }
1235                         }
1236 
1237                         @Override
1238                         public void failed(final Exception cause) {
1239                             asyncExecCallback.failed(cause);
1240                         }
1241 
1242                         @Override
1243                         public void cancelled() {
1244                             asyncExecCallback.failed(new InterruptedIOException());
1245                         }
1246                     });
1247                 }
1248 
1249                 if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
1250                     callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
1251                 } else {
1252                     final Header resultEtagHeader = backendResponse.getFirstHeader(HttpHeaders.ETAG);
1253                     if (resultEtagHeader == null) {
1254                         if (LOG.isDebugEnabled()) {
1255                             LOG.debug("{} 304 response did not contain ETag", exchangeId);
1256                         }
1257                         callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1258                     } else {
1259                         final String resultEtag = resultEtagHeader.getValue();
1260                         final CacheHit match = variantMap.get(resultEtag);
1261                         if (match == null) {
1262                             if (LOG.isDebugEnabled()) {
1263                                 LOG.debug("{} 304 response did not contain ETag matching one sent in If-None-Match", exchangeId);
1264                             }
1265                             callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1266                         } else {
1267                             if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
1268                                 final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
1269                                         BasicRequestBuilder.copy(request).build());
1270                                 callback = new AsyncExecCallbackWrapper(() -> callBackend(target, unconditional, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1271                             } else {
1272                                 callback = new AsyncExecCallbackWrapper(() -> updateVariantCacheEntry(backendResponse, responseDate, match), asyncExecCallback::failed);
1273                             }
1274                         }
1275                     }
1276                 }
1277                 callbackRef.set(callback);
1278                 return callback.handleResponse(backendResponse, entityDetails);
1279             }
1280 
1281             @Override
1282             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1283                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1284                 if (callback != null) {
1285                     callback.handleInformationResponse(response);
1286                 } else {
1287                     asyncExecCallback.handleInformationResponse(response);
1288                 }
1289             }
1290 
1291             @Override
1292             public void completed() {
1293                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1294                 if (callback != null) {
1295                     callback.completed();
1296                 } else {
1297                     asyncExecCallback.completed();
1298                 }
1299             }
1300 
1301             @Override
1302             public void failed(final Exception cause) {
1303                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1304                 if (callback != null) {
1305                     callback.failed(cause);
1306                 } else {
1307                     asyncExecCallback.failed(cause);
1308                 }
1309             }
1310 
1311         });
1312 
1313     }
1314 
1315 }