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.Collection;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.concurrent.ScheduledExecutorService;
38  import java.util.concurrent.atomic.AtomicBoolean;
39  import java.util.concurrent.atomic.AtomicReference;
40  import java.util.function.Consumer;
41  
42  import org.apache.hc.client5.http.HttpRoute;
43  import org.apache.hc.client5.http.async.AsyncExecCallback;
44  import org.apache.hc.client5.http.async.AsyncExecChain;
45  import org.apache.hc.client5.http.async.AsyncExecChainHandler;
46  import org.apache.hc.client5.http.async.methods.SimpleBody;
47  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
48  import org.apache.hc.client5.http.cache.CacheResponseStatus;
49  import org.apache.hc.client5.http.cache.HttpCacheContext;
50  import org.apache.hc.client5.http.cache.HttpCacheEntry;
51  import org.apache.hc.client5.http.cache.RequestCacheControl;
52  import org.apache.hc.client5.http.cache.ResourceIOException;
53  import org.apache.hc.client5.http.cache.ResponseCacheControl;
54  import org.apache.hc.client5.http.impl.ExecSupport;
55  import org.apache.hc.client5.http.protocol.HttpClientContext;
56  import org.apache.hc.client5.http.schedule.SchedulingStrategy;
57  import org.apache.hc.client5.http.validator.ETag;
58  import org.apache.hc.core5.annotation.Contract;
59  import org.apache.hc.core5.annotation.ThreadingBehavior;
60  import org.apache.hc.core5.concurrent.CancellableDependency;
61  import org.apache.hc.core5.concurrent.ComplexFuture;
62  import org.apache.hc.core5.concurrent.FutureCallback;
63  import org.apache.hc.core5.http.ContentType;
64  import org.apache.hc.core5.http.EntityDetails;
65  import org.apache.hc.core5.http.Header;
66  import org.apache.hc.core5.http.HttpException;
67  import org.apache.hc.core5.http.HttpHeaders;
68  import org.apache.hc.core5.http.HttpHost;
69  import org.apache.hc.core5.http.HttpRequest;
70  import org.apache.hc.core5.http.HttpResponse;
71  import org.apache.hc.core5.http.HttpStatus;
72  import org.apache.hc.core5.http.impl.BasicEntityDetails;
73  import org.apache.hc.core5.http.nio.AsyncDataConsumer;
74  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
75  import org.apache.hc.core5.http.nio.CapacityChannel;
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.setRequest(request);
208                         context.setResponse(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 HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
241         final CancellableDependency operation = scope.cancellableDependency;
242 
243         if (LOG.isDebugEnabled()) {
244             LOG.debug("{} request via cache: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
245         }
246 
247         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MISS);
248         context.setCacheEntry(null);
249 
250         if (clientRequestsOurOptions(request)) {
251             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
252             triggerResponse(SimpleHttpResponse.create(HttpStatus.SC_NOT_IMPLEMENTED), scope, asyncExecCallback);
253             return;
254         }
255 
256         final RequestCacheControl requestCacheControl;
257         if (request.containsHeader(HttpHeaders.CACHE_CONTROL)) {
258             requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
259             context.setRequestCacheControl(requestCacheControl);
260         } else {
261             requestCacheControl = context.getRequestCacheControlOrDefault();
262             CacheControlHeaderGenerator.INSTANCE.generate(requestCacheControl, request);
263         }
264 
265         if (LOG.isDebugEnabled()) {
266             LOG.debug("{} request cache control: {}", exchangeId, requestCacheControl);
267         }
268 
269         if (cacheableRequestPolicy.canBeServedFromCache(requestCacheControl, request)) {
270             operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
271 
272                 @Override
273                 public void completed(final CacheMatch result) {
274                     final CacheHit hit = result != null ? result.hit : null;
275                     final CacheHit root = result != null ? result.root : null;
276                     if (hit == null) {
277                         handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
278                     } else {
279                         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
280                         if (LOG.isDebugEnabled()) {
281                             LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
282                         }
283                         context.setResponseCacheControl(responseCacheControl);
284                         handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
285                     }
286                 }
287 
288                 @Override
289                 public void failed(final Exception cause) {
290                     asyncExecCallback.failed(cause);
291                 }
292 
293                 @Override
294                 public void cancelled() {
295                     asyncExecCallback.failed(new InterruptedIOException());
296                 }
297 
298             }));
299 
300         } else {
301             if (LOG.isDebugEnabled()) {
302                 LOG.debug("{} request cannot be served from cache", exchangeId);
303             }
304             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
305         }
306     }
307 
308     void chainProceed(
309             final HttpRequest request,
310             final AsyncEntityProducer entityProducer,
311             final AsyncExecChain.Scope scope,
312             final AsyncExecChain chain,
313             final AsyncExecCallback asyncExecCallback) {
314         try {
315             chain.proceed(request, entityProducer, scope, asyncExecCallback);
316         } catch (final HttpException | IOException ex) {
317             asyncExecCallback.failed(ex);
318         }
319     }
320 
321     void callBackend(
322             final HttpHost target,
323             final HttpRequest request,
324             final AsyncEntityProducer entityProducer,
325             final AsyncExecChain.Scope scope,
326             final AsyncExecChain chain,
327             final AsyncExecCallback asyncExecCallback) {
328         final String exchangeId = scope.exchangeId;
329 
330         if (LOG.isDebugEnabled()) {
331             LOG.debug("{} calling the backend", exchangeId);
332         }
333         final Instant requestDate = getCurrentDate();
334         final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
335         chainProceed(request, entityProducer, scope, chain, new AsyncExecCallback() {
336 
337             @Override
338             public AsyncDataConsumer handleResponse(
339                     final HttpResponse backendResponse,
340                     final EntityDetails entityDetails) throws HttpException, IOException {
341                 final Instant responseDate = getCurrentDate();
342                 final AsyncExecCallback callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
343                 callbackRef.set(callback);
344                 return callback.handleResponse(backendResponse, entityDetails);
345             }
346 
347             @Override
348             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
349                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
350                 if (callback != null) {
351                     callback.handleInformationResponse(response);
352                 } else {
353                     asyncExecCallback.handleInformationResponse(response);
354                 }
355             }
356 
357             @Override
358             public void completed() {
359                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
360                 if (callback != null) {
361                     callback.completed();
362                 } else {
363                     asyncExecCallback.completed();
364                 }
365             }
366 
367             @Override
368             public void failed(final Exception cause) {
369                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
370                 if (callback != null) {
371                     callback.failed(cause);
372                 } else {
373                     asyncExecCallback.failed(cause);
374                 }
375             }
376 
377         });
378     }
379 
380     class CachingAsyncDataConsumer implements AsyncDataConsumer {
381 
382         private final String exchangeId;
383         private final AsyncExecCallback fallback;
384         private final HttpResponse backendResponse;
385         private final EntityDetails entityDetails;
386         private final AtomicBoolean writtenThrough;
387         private final AtomicReference<ByteArrayBuffer> bufferRef;
388         private final AtomicReference<AsyncDataConsumer> dataConsumerRef;
389 
390         CachingAsyncDataConsumer(
391                 final String exchangeId,
392                 final AsyncExecCallback fallback,
393                 final HttpResponse backendResponse,
394                 final EntityDetails entityDetails) {
395             this.exchangeId = exchangeId;
396             this.fallback = fallback;
397             this.backendResponse = backendResponse;
398             this.entityDetails = entityDetails;
399             this.writtenThrough = new AtomicBoolean(false);
400             this.bufferRef = new AtomicReference<>(entityDetails != null ? new ByteArrayBuffer(1024) : null);
401             this.dataConsumerRef = new AtomicReference<>();
402         }
403 
404         @Override
405         public final void updateCapacity(final CapacityChannel capacityChannel) throws IOException {
406             final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
407             if (dataConsumer != null) {
408                 dataConsumer.updateCapacity(capacityChannel);
409             } else {
410                 capacityChannel.update(Integer.MAX_VALUE);
411             }
412         }
413 
414         @Override
415         public final void consume(final ByteBuffer src) throws IOException {
416             final ByteArrayBuffer buffer = bufferRef.get();
417             if (buffer != null) {
418                 if (src.hasArray()) {
419                     buffer.append(src.array(), src.arrayOffset() + src.position(), src.remaining());
420                 } else {
421                     while (src.hasRemaining()) {
422                         buffer.append(src.get());
423                     }
424                 }
425                 if (buffer.length() > cacheConfig.getMaxObjectSize()) {
426                     if (LOG.isDebugEnabled()) {
427                         LOG.debug("{} backend response content length exceeds maximum", exchangeId);
428                     }
429                     // Over the max limit. Stop buffering and forward the response
430                     // along with all the data buffered so far to the caller.
431                     bufferRef.set(null);
432                     try {
433                         final AsyncDataConsumer dataConsumer = fallback.handleResponse(backendResponse, entityDetails);
434                         if (dataConsumer != null) {
435                             dataConsumerRef.set(dataConsumer);
436                             writtenThrough.set(true);
437                             dataConsumer.consume(ByteBuffer.wrap(buffer.array(), 0, buffer.length()));
438                         }
439                     } catch (final HttpException ex) {
440                         fallback.failed(ex);
441                     }
442                 }
443             } else {
444                 final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
445                 if (dataConsumer != null) {
446                     dataConsumer.consume(src);
447                 }
448             }
449         }
450 
451         @Override
452         public final void streamEnd(final List<? extends Header> trailers) throws HttpException, IOException {
453             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
454             if (dataConsumer != null) {
455                 dataConsumer.streamEnd(trailers);
456             }
457         }
458 
459         @Override
460         public void releaseResources() {
461             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
462             if (dataConsumer != null) {
463                 dataConsumer.releaseResources();
464             }
465         }
466 
467     }
468 
469     class BackendResponseHandler implements AsyncExecCallback {
470 
471         private final HttpHost target;
472         private final HttpRequest request;
473         private final Instant requestDate;
474         private final Instant responseDate;
475         private final AsyncExecChain.Scope scope;
476         private final AsyncExecCallback asyncExecCallback;
477         private final AtomicReference<CachingAsyncDataConsumer> cachingConsumerRef;
478 
479         BackendResponseHandler(
480                 final HttpHost target,
481                 final HttpRequest request,
482                 final Instant requestDate,
483                 final Instant responseDate,
484                 final AsyncExecChain.Scope scope,
485                 final AsyncExecCallback asyncExecCallback) {
486             this.target = target;
487             this.request = request;
488             this.requestDate = requestDate;
489             this.responseDate = responseDate;
490             this.scope = scope;
491             this.asyncExecCallback = asyncExecCallback;
492             this.cachingConsumerRef = new AtomicReference<>();
493         }
494 
495         @Override
496         public AsyncDataConsumer handleResponse(
497                 final HttpResponse backendResponse,
498                 final EntityDetails entityDetails) throws HttpException, IOException {
499             final String exchangeId = scope.exchangeId;
500             responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback<Boolean>() {
501 
502                 @Override
503                 public void completed(final Boolean result) {
504                 }
505 
506                 @Override
507                 public void failed(final Exception ex) {
508                     if (LOG.isDebugEnabled()) {
509                         LOG.debug("{} unable to flush invalidated entries from cache", exchangeId, ex);
510                     }
511                 }
512 
513                 @Override
514                 public void cancelled() {
515                 }
516 
517             });
518             if (isResponseTooBig(entityDetails)) {
519                 if (LOG.isDebugEnabled()) {
520                     LOG.debug("{} backend response is known to be too big", exchangeId);
521                 }
522                 return asyncExecCallback.handleResponse(backendResponse, entityDetails);
523             }
524 
525             final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
526             final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse);
527             context.setResponseCacheControl(responseCacheControl);
528             final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse);
529             if (cacheable) {
530                 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
531                 if (LOG.isDebugEnabled()) {
532                     LOG.debug("{} caching backend response", exchangeId);
533                 }
534                 final CachingAsyncDataConsumer cachingDataConsumer = new CachingAsyncDataConsumer(
535                         exchangeId, asyncExecCallback, backendResponse, entityDetails);
536                 cachingConsumerRef.set(cachingDataConsumer);
537                 return cachingDataConsumer;
538             }
539             if (LOG.isDebugEnabled()) {
540                 LOG.debug("{} backend response is not cacheable", exchangeId);
541             }
542             return asyncExecCallback.handleResponse(backendResponse, entityDetails);
543         }
544 
545         @Override
546         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
547             asyncExecCallback.handleInformationResponse(response);
548         }
549 
550         void triggerNewCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate, final ByteArrayBuffer buffer) {
551             final String exchangeId = scope.exchangeId;
552             final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
553             final CancellableDependency operation = scope.cancellableDependency;
554             operation.setDependency(responseCache.store(
555                     target,
556                     request,
557                     backendResponse,
558                     buffer,
559                     requestDate,
560                     responseDate,
561                     new FutureCallback<CacheHit>() {
562 
563                         @Override
564                         public void completed(final CacheHit hit) {
565                             if (LOG.isDebugEnabled()) {
566                                 LOG.debug("{} backend response successfully cached", exchangeId);
567                             }
568                             try {
569                                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
570                                 context.setCacheEntry(hit.entry);
571                                 triggerResponse(cacheResponse, scope, asyncExecCallback);
572                             } catch (final ResourceIOException ex) {
573                                 asyncExecCallback.failed(ex);
574                             }
575                         }
576 
577                         @Override
578                         public void failed(final Exception ex) {
579                             asyncExecCallback.failed(ex);
580                         }
581 
582                         @Override
583                         public void cancelled() {
584                             asyncExecCallback.failed(new InterruptedIOException());
585                         }
586 
587                     }));
588 
589         }
590 
591         void triggerCachedResponse(final HttpCacheEntry entry) {
592             final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
593             try {
594                 final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, entry);
595                 context.setCacheEntry(entry);
596                 triggerResponse(cacheResponse, scope, asyncExecCallback);
597             } catch (final ResourceIOException ex) {
598                 asyncExecCallback.failed(ex);
599             }
600         }
601 
602         @Override
603         public void completed() {
604             final String exchangeId = scope.exchangeId;
605             final CachingAsyncDataConsumer cachingDataConsumer = cachingConsumerRef.getAndSet(null);
606             if (cachingDataConsumer == null || cachingDataConsumer.writtenThrough.get()) {
607                 asyncExecCallback.completed();
608                 return;
609             }
610             final HttpResponse backendResponse = cachingDataConsumer.backendResponse;
611             final ByteArrayBuffer buffer = cachingDataConsumer.bufferRef.getAndSet(null);
612 
613             // Handle 304 Not Modified responses
614             if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
615                 responseCache.match(target, request, new FutureCallback<CacheMatch>() {
616 
617                     @Override
618                     public void completed(final CacheMatch result) {
619                         final CacheHit hit = result != null ? result.hit : null;
620                         if (hit != null) {
621                             if (LOG.isDebugEnabled()) {
622                                 LOG.debug("{} existing cache entry found, updating cache entry", exchangeId);
623                             }
624                             responseCache.update(
625                                     hit,
626                                     target,
627                                     request,
628                                     backendResponse,
629                                     requestDate,
630                                     responseDate,
631                                     new FutureCallback<CacheHit>() {
632 
633                                         @Override
634                                         public void completed(final CacheHit updated) {
635                                             if (LOG.isDebugEnabled()) {
636                                                 LOG.debug("{} cache entry updated, generating response from updated entry", exchangeId);
637                                             }
638                                             triggerCachedResponse(updated.entry);
639                                         }
640                                         @Override
641                                         public void failed(final Exception cause) {
642                                             if (LOG.isDebugEnabled()) {
643                                                 LOG.debug("{} request failed: {}", exchangeId, cause.getMessage());
644                                             }
645                                             asyncExecCallback.failed(cause);
646                                         }
647 
648                                         @Override
649                                         public void cancelled() {
650                                             if (LOG.isDebugEnabled()) {
651                                                 LOG.debug("{} cache entry updated aborted", exchangeId);
652                                             }
653                                             asyncExecCallback.failed(new InterruptedIOException());
654                                         }
655 
656                                     });
657                         } else {
658                             triggerNewCacheEntryResponse(backendResponse, responseDate, buffer);
659                         }
660                     }
661 
662                     @Override
663                     public void failed(final Exception cause) {
664                         asyncExecCallback.failed(cause);
665                     }
666 
667                     @Override
668                     public void cancelled() {
669                         asyncExecCallback.failed(new InterruptedIOException());
670                     }
671 
672                 });
673             } else {
674                 if (cacheConfig.isFreshnessCheckEnabled()) {
675                     final CancellableDependency operation = scope.cancellableDependency;
676                     operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
677 
678                         @Override
679                         public void completed(final CacheMatch result) {
680                             final CacheHit hit = result != null ? result.hit : null;
681                             if (HttpCacheEntry.isNewer(hit != null ? hit.entry : null, backendResponse)) {
682                                 if (LOG.isDebugEnabled()) {
683                                     LOG.debug("{} backend already contains fresher cache entry", exchangeId);
684                                 }
685                                 triggerCachedResponse(hit.entry);
686                             } else {
687                                 triggerNewCacheEntryResponse(backendResponse, responseDate, buffer);
688                             }
689                         }
690 
691                         @Override
692                         public void failed(final Exception cause) {
693                             asyncExecCallback.failed(cause);
694                         }
695 
696                         @Override
697                         public void cancelled() {
698                             asyncExecCallback.failed(new InterruptedIOException());
699                         }
700 
701                     }));
702                 } else {
703                     triggerNewCacheEntryResponse(backendResponse, responseDate, buffer);
704                 }
705             }
706         }
707 
708         @Override
709         public void failed(final Exception cause) {
710             asyncExecCallback.failed(cause);
711         }
712 
713     }
714 
715     private void handleCacheHit(
716             final RequestCacheControl requestCacheControl,
717             final ResponseCacheControl responseCacheControl,
718             final CacheHit hit,
719             final HttpHost target,
720             final HttpRequest request,
721             final AsyncEntityProducer entityProducer,
722             final AsyncExecChain.Scope scope,
723             final AsyncExecChain chain,
724             final AsyncExecCallback asyncExecCallback) {
725         final HttpCacheContext context  = HttpCacheContext.cast(scope.clientContext);
726         final String exchangeId = scope.exchangeId;
727 
728         if (LOG.isDebugEnabled()) {
729             LOG.debug("{} cache hit: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
730         }
731 
732         context.setCacheResponseStatus(CacheResponseStatus.CACHE_HIT);
733         cacheHits.getAndIncrement();
734 
735         final Instant now = getCurrentDate();
736 
737         final CacheSuitability cacheSuitability = suitabilityChecker.assessSuitability(requestCacheControl, responseCacheControl, request, hit.entry, now);
738         if (LOG.isDebugEnabled()) {
739             LOG.debug("{} cache suitability: {}", exchangeId, cacheSuitability);
740         }
741         if (cacheSuitability == CacheSuitability.FRESH || cacheSuitability == CacheSuitability.FRESH_ENOUGH) {
742             if (LOG.isDebugEnabled()) {
743                 LOG.debug("{} cache hit is fresh enough", exchangeId);
744             }
745             try {
746                 final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, now);
747                 context.setCacheEntry(hit.entry);
748                 triggerResponse(cacheResponse, scope, asyncExecCallback);
749             } catch (final ResourceIOException ex) {
750                 if (requestCacheControl.isOnlyIfCached()) {
751                     if (LOG.isDebugEnabled()) {
752                         LOG.debug("{} request marked only-if-cached", exchangeId);
753                     }
754                     context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
755                     final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
756                     triggerResponse(cacheResponse, scope, asyncExecCallback);
757                 } else {
758                     context.setCacheResponseStatus(CacheResponseStatus.FAILURE);
759                     try {
760                         chain.proceed(request, entityProducer, scope, asyncExecCallback);
761                     } catch (final HttpException | IOException ex2) {
762                         asyncExecCallback.failed(ex2);
763                     }
764                 }
765             }
766         } else {
767             if (requestCacheControl.isOnlyIfCached()) {
768                 if (LOG.isDebugEnabled()) {
769                     LOG.debug("{} cache entry not is not fresh and only-if-cached requested", exchangeId);
770                 }
771                 context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
772                 final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
773                 triggerResponse(cacheResponse, scope, asyncExecCallback);
774             } else if (cacheSuitability == CacheSuitability.MISMATCH) {
775                 if (LOG.isDebugEnabled()) {
776                     LOG.debug("{} cache entry does not match the request; calling backend", exchangeId);
777                 }
778                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
779             } else if (entityProducer != null && !entityProducer.isRepeatable()) {
780                 if (LOG.isDebugEnabled()) {
781                     LOG.debug("{} request is not repeatable; calling backend", exchangeId);
782                 }
783                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
784             } else if (hit.entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request)) {
785                 if (LOG.isDebugEnabled()) {
786                     LOG.debug("{} non-modified cache entry does not match the non-conditional request; calling backend", exchangeId);
787                 }
788                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
789             } else if (cacheSuitability == CacheSuitability.REVALIDATION_REQUIRED) {
790                 if (LOG.isDebugEnabled()) {
791                     LOG.debug("{} revalidation required; revalidating cache entry", exchangeId);
792                 }
793                 revalidateCacheEntryWithoutFallback(responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
794             } else if (cacheSuitability == CacheSuitability.STALE_WHILE_REVALIDATED) {
795                 if (cacheRevalidator != null) {
796                     if (LOG.isDebugEnabled()) {
797                         LOG.debug("{} serving stale with asynchronous revalidation", exchangeId);
798                     }
799                     try {
800                         final String revalidationExchangeId = ExecSupport.getNextExchangeId();
801                         context.setExchangeId(revalidationExchangeId);
802                         final AsyncExecChain.Scope fork = new AsyncExecChain.Scope(
803                                 revalidationExchangeId,
804                                 scope.route,
805                                 scope.originalRequest,
806                                 new ComplexFuture<>(null),
807                                 HttpCacheContext.create(),
808                                 scope.execRuntime.fork(),
809                                 scope.scheduler,
810                                 scope.execCount);
811                         if (LOG.isDebugEnabled()) {
812                             LOG.debug("{} starting asynchronous revalidation exchange {}", exchangeId, revalidationExchangeId);
813                         }
814                         cacheRevalidator.revalidateCacheEntry(
815                                 hit.getEntryKey(),
816                                 asyncExecCallback,
817                                 c -> revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, fork, chain, c));
818                         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
819                         final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
820                         context.setCacheEntry(hit.entry);
821                         triggerResponse(cacheResponse, scope, asyncExecCallback);
822                     } catch (final IOException ex) {
823                         asyncExecCallback.failed(ex);
824                     }
825                 } else {
826                     if (LOG.isDebugEnabled()) {
827                         LOG.debug("{} revalidating stale cache entry (asynchronous revalidation disabled)", exchangeId);
828                     }
829                     revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
830                 }
831             } else if (cacheSuitability == CacheSuitability.STALE) {
832                 if (LOG.isDebugEnabled()) {
833                     LOG.debug("{} revalidating stale cache entry", exchangeId);
834                 }
835                 revalidateCacheEntryWithFallback(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
836             } else {
837                 if (LOG.isDebugEnabled()) {
838                     LOG.debug("{} cache entry not usable; calling backend", exchangeId);
839                 }
840                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
841             }
842         }
843     }
844 
845     void revalidateCacheEntry(
846             final ResponseCacheControl responseCacheControl,
847             final CacheHit hit,
848             final HttpHost target,
849             final HttpRequest request,
850             final AsyncEntityProducer entityProducer,
851             final AsyncExecChain.Scope scope,
852             final AsyncExecChain chain,
853             final AsyncExecCallback asyncExecCallback) {
854         final Instant requestDate = getCurrentDate();
855         final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
856                 responseCacheControl,
857                 BasicRequestBuilder.copy(request).build(),
858                 hit.entry);
859         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
860         chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
861 
862             final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
863 
864             void triggerUpdatedCacheEntryResponse(final HttpResponse backendResponse, final Instant responseDate) {
865                 final CancellableDependency operation = scope.cancellableDependency;
866                 operation.setDependency(responseCache.update(
867                         hit,
868                         target,
869                         request,
870                         backendResponse,
871                         requestDate,
872                         responseDate,
873                         new FutureCallback<CacheHit>() {
874 
875                             @Override
876                             public void completed(final CacheHit updated) {
877                                 try {
878                                     final SimpleHttpResponse cacheResponse = generateCachedResponse(request, updated.entry, responseDate);
879                                     context.setCacheEntry(updated.entry);
880                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
881                                 } catch (final ResourceIOException ex) {
882                                     asyncExecCallback.failed(ex);
883                                 }
884                             }
885 
886                             @Override
887                             public void failed(final Exception ex) {
888                                 asyncExecCallback.failed(ex);
889                             }
890 
891                             @Override
892                             public void cancelled() {
893                                 asyncExecCallback.failed(new InterruptedIOException());
894                             }
895 
896                         }));
897             }
898 
899             AsyncExecCallback evaluateResponse(final HttpResponse backendResponse, final Instant responseDate) {
900                 final int statusCode = backendResponse.getCode();
901                 if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
902                     context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
903                     cacheUpdates.getAndIncrement();
904                 }
905                 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
906                     return new AsyncExecCallbackWrapper(() -> triggerUpdatedCacheEntryResponse(backendResponse, responseDate), asyncExecCallback::failed);
907                 }
908                 return new BackendResponseHandler(target, conditionalRequest, requestDate, responseDate, scope, asyncExecCallback);
909             }
910 
911             @Override
912             public AsyncDataConsumer handleResponse(
913                     final HttpResponse backendResponse1,
914                     final EntityDetails entityDetails) throws HttpException, IOException {
915 
916                 final Instant responseDate = getCurrentDate();
917 
918                 final AsyncExecCallback callback1;
919                 if (HttpCacheEntry.isNewer(hit.entry, backendResponse1)) {
920 
921                     final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
922                             BasicRequestBuilder.copy(scope.originalRequest).build());
923 
924                     callback1 = new AsyncExecCallbackWrapper(() -> chainProceed(unconditional, entityProducer, scope, chain, new AsyncExecCallback() {
925 
926                         @Override
927                         public AsyncDataConsumer handleResponse(
928                                 final HttpResponse backendResponse2,
929                                 final EntityDetails entityDetails1) throws HttpException, IOException {
930                             final Instant responseDate2 = getCurrentDate();
931                             final AsyncExecCallback callback2 = evaluateResponse(backendResponse2, responseDate2);
932                             callbackRef.set(callback2);
933                             return callback2.handleResponse(backendResponse2, entityDetails1);
934                         }
935 
936                         @Override
937                         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
938                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
939                             if (callback2 != null) {
940                                 callback2.handleInformationResponse(response);
941                             } else {
942                                 asyncExecCallback.handleInformationResponse(response);
943                             }
944                         }
945 
946                         @Override
947                         public void completed() {
948                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
949                             if (callback2 != null) {
950                                 callback2.completed();
951                             } else {
952                                 asyncExecCallback.completed();
953                             }
954                         }
955 
956                         @Override
957                         public void failed(final Exception cause) {
958                             final AsyncExecCallback callback2 = callbackRef.getAndSet(null);
959                             if (callback2 != null) {
960                                 callback2.failed(cause);
961                             } else {
962                                 asyncExecCallback.failed(cause);
963                             }
964                         }
965 
966                     }), asyncExecCallback::failed);
967                 } else {
968                     callback1 = evaluateResponse(backendResponse1, responseDate);
969                 }
970                 callbackRef.set(callback1);
971                 return callback1.handleResponse(backendResponse1, entityDetails);
972             }
973 
974             @Override
975             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
976                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
977                 if (callback1 != null) {
978                     callback1.handleInformationResponse(response);
979                 } else {
980                     asyncExecCallback.handleInformationResponse(response);
981                 }
982             }
983 
984             @Override
985             public void completed() {
986                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
987                 if (callback1 != null) {
988                     callback1.completed();
989                 } else {
990                     asyncExecCallback.completed();
991                 }
992             }
993 
994             @Override
995             public void failed(final Exception cause) {
996                 final AsyncExecCallback callback1 = callbackRef.getAndSet(null);
997                 if (callback1 != null) {
998                     callback1.failed(cause);
999                 } else {
1000                     asyncExecCallback.failed(cause);
1001                 }
1002             }
1003 
1004         });
1005 
1006     }
1007 
1008     void revalidateCacheEntryWithoutFallback(
1009             final ResponseCacheControl responseCacheControl,
1010             final CacheHit hit,
1011             final HttpHost target,
1012             final HttpRequest request,
1013             final AsyncEntityProducer entityProducer,
1014             final AsyncExecChain.Scope scope,
1015             final AsyncExecChain chain,
1016             final AsyncExecCallback asyncExecCallback) {
1017         final String exchangeId = scope.exchangeId;
1018         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1019         revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
1020 
1021             private final AtomicBoolean committed = new AtomicBoolean();
1022 
1023             @Override
1024             public AsyncDataConsumer handleResponse(final HttpResponse response,
1025                                                     final EntityDetails entityDetails) throws HttpException, IOException {
1026                 committed.set(true);
1027                 return asyncExecCallback.handleResponse(response, entityDetails);
1028             }
1029 
1030             @Override
1031             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1032                 asyncExecCallback.handleInformationResponse(response);
1033             }
1034 
1035             @Override
1036             public void completed() {
1037                 asyncExecCallback.completed();
1038             }
1039 
1040             @Override
1041             public void failed(final Exception cause) {
1042                 if (!committed.get() && cause instanceof IOException) {
1043                     if (LOG.isDebugEnabled()) {
1044                         LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
1045                     }
1046                     final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1047                     context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1048                     triggerResponse(cacheResponse, scope, asyncExecCallback);
1049                 } else {
1050                     asyncExecCallback.failed(cause);
1051                 }
1052             }
1053 
1054         });
1055     }
1056 
1057     void revalidateCacheEntryWithFallback(
1058             final RequestCacheControl requestCacheControl,
1059             final ResponseCacheControl responseCacheControl,
1060             final CacheHit hit,
1061             final HttpHost target,
1062             final HttpRequest request,
1063             final AsyncEntityProducer entityProducer,
1064             final AsyncExecChain.Scope scope,
1065             final AsyncExecChain chain,
1066             final AsyncExecCallback asyncExecCallback) {
1067         final String exchangeId = scope.exchangeId;
1068         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1069         revalidateCacheEntry(responseCacheControl, hit, target, request, entityProducer, scope, chain, new AsyncExecCallback() {
1070 
1071             private final AtomicReference<HttpResponse> committed = new AtomicReference<>();
1072 
1073             @Override
1074             public AsyncDataConsumer handleResponse(final HttpResponse response, final EntityDetails entityDetails) throws HttpException, IOException {
1075                 final int status = response.getCode();
1076                 if (staleIfErrorAppliesTo(status) &&
1077                         suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
1078                     if (LOG.isDebugEnabled()) {
1079                         LOG.debug("{} serving stale response due to {} status and stale-if-error enabled", exchangeId, status);
1080                     }
1081                     return null;
1082                 }
1083                 committed.set(response);
1084                 return asyncExecCallback.handleResponse(response, entityDetails);
1085             }
1086 
1087             @Override
1088             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1089                 asyncExecCallback.handleInformationResponse(response);
1090             }
1091 
1092             @Override
1093             public void completed() {
1094                 final HttpResponse response = committed.get();
1095                 if (response == null) {
1096                     try {
1097                         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1098                         final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
1099                         context.setCacheEntry(hit.entry);
1100                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1101                     } catch (final IOException ex) {
1102                         asyncExecCallback.failed(ex);
1103                     }
1104                 } else {
1105                     asyncExecCallback.completed();
1106                 }
1107             }
1108 
1109             @Override
1110             public void failed(final Exception cause) {
1111                 final HttpResponse response = committed.get();
1112                 if (response == null) {
1113                     if (LOG.isDebugEnabled()) {
1114                         LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
1115                     }
1116                     context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1117                     if (cause instanceof IOException &&
1118                             suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
1119                         if (LOG.isDebugEnabled()) {
1120                             LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId);
1121                         }
1122                         try {
1123                             final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
1124                             context.setCacheEntry(hit.entry);
1125                             triggerResponse(cacheResponse, scope, asyncExecCallback);
1126                         } catch (final IOException ex) {
1127                             asyncExecCallback.failed(cause);
1128                         }
1129                     } else {
1130                         final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1131                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1132                     }
1133                 } else {
1134                     asyncExecCallback.failed(cause);
1135                 }
1136             }
1137 
1138         });
1139     }
1140     private void handleCacheMiss(
1141             final RequestCacheControl requestCacheControl,
1142             final CacheHit partialMatch,
1143             final HttpHost target,
1144             final HttpRequest request,
1145             final AsyncEntityProducer entityProducer,
1146             final AsyncExecChain.Scope scope,
1147             final AsyncExecChain chain,
1148             final AsyncExecCallback asyncExecCallback) {
1149         final String exchangeId = scope.exchangeId;
1150 
1151         if (LOG.isDebugEnabled()) {
1152             LOG.debug("{} cache miss: {} {}", exchangeId, request.getMethod(), request.getRequestUri());
1153         }
1154         cacheMisses.getAndIncrement();
1155 
1156         final CancellableDependency operation = scope.cancellableDependency;
1157         if (requestCacheControl.isOnlyIfCached()) {
1158             if (LOG.isDebugEnabled()) {
1159                 LOG.debug("{} request marked only-if-cached", exchangeId);
1160             }
1161             final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1162             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1163             final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1164             triggerResponse(cacheResponse, scope, asyncExecCallback);
1165         }
1166 
1167         if (partialMatch != null && partialMatch.entry.hasVariants() && entityProducer == null) {
1168             operation.setDependency(responseCache.getVariants(
1169                     partialMatch,
1170                     new FutureCallback<Collection<CacheHit>>() {
1171 
1172                         @Override
1173                         public void completed(final Collection<CacheHit> variants) {
1174                             if (variants != null && !variants.isEmpty()) {
1175                                 negotiateResponseFromVariants(target, request, entityProducer, scope, chain, asyncExecCallback, variants);
1176                             } else {
1177                                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1178                             }
1179                         }
1180 
1181                         @Override
1182                         public void failed(final Exception ex) {
1183                             asyncExecCallback.failed(ex);
1184                         }
1185 
1186                         @Override
1187                         public void cancelled() {
1188                             asyncExecCallback.failed(new InterruptedIOException());
1189                         }
1190 
1191                     }));
1192         } else {
1193             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1194         }
1195     }
1196 
1197     void negotiateResponseFromVariants(
1198             final HttpHost target,
1199             final HttpRequest request,
1200             final AsyncEntityProducer entityProducer,
1201             final AsyncExecChain.Scope scope,
1202             final AsyncExecChain chain,
1203             final AsyncExecCallback asyncExecCallback,
1204             final Collection<CacheHit> variants) {
1205         final String exchangeId = scope.exchangeId;
1206         final CancellableDependency operation = scope.cancellableDependency;
1207         final Map<ETag, CacheHit> variantMap = new HashMap<>();
1208         for (final CacheHit variant : variants) {
1209             final ETag eTag = variant.entry.getETag();
1210             if (eTag != null) {
1211                 variantMap.put(eTag, variant);
1212             }
1213         }
1214 
1215         final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
1216                 request,
1217                 variantMap.keySet());
1218 
1219         final Instant requestDate = getCurrentDate();
1220         chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
1221 
1222             final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
1223 
1224             void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final CacheHit match) {
1225                 final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1226                 context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
1227                 cacheUpdates.getAndIncrement();
1228 
1229                 operation.setDependency(responseCache.storeFromNegotiated(
1230                         match,
1231                         target,
1232                         request,
1233                         backendResponse,
1234                         requestDate,
1235                         responseDate,
1236                         new FutureCallback<CacheHit>() {
1237 
1238                             @Override
1239                             public void completed(final CacheHit hit) {
1240                                 try {
1241                                     final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, responseDate);
1242                                     context.setCacheEntry(hit.entry);
1243                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
1244                                 } catch (final ResourceIOException ex) {
1245                                     asyncExecCallback.failed(ex);
1246                                 }
1247                             }
1248 
1249                             @Override
1250                             public void failed(final Exception ex) {
1251                                 asyncExecCallback.failed(ex);
1252                             }
1253 
1254                             @Override
1255                             public void cancelled() {
1256                                 asyncExecCallback.failed(new InterruptedIOException());
1257                             }
1258 
1259                         }));
1260             }
1261 
1262             @Override
1263             public AsyncDataConsumer handleResponse(
1264                     final HttpResponse backendResponse,
1265                     final EntityDetails entityDetails) throws HttpException, IOException {
1266                 final Instant responseDate = getCurrentDate();
1267                 final AsyncExecCallback callback;
1268                 if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
1269                     callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
1270                 } else {
1271                     final ETag resultEtag = ETag.get(backendResponse);
1272                     if (resultEtag == null) {
1273                         if (LOG.isDebugEnabled()) {
1274                             LOG.debug("{} 304 response did not contain ETag", exchangeId);
1275                         }
1276                         callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1277                     } else {
1278                         final CacheHit match = variantMap.get(resultEtag);
1279                         if (match == null) {
1280                             if (LOG.isDebugEnabled()) {
1281                                 LOG.debug("{} 304 response did not contain ETag matching one sent in If-None-Match", exchangeId);
1282                             }
1283                             callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1284                         } else {
1285                             if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
1286                                 final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
1287                                         BasicRequestBuilder.copy(request).build());
1288                                 callback = new AsyncExecCallbackWrapper(() -> callBackend(target, unconditional, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1289                             } else {
1290                                 callback = new AsyncExecCallbackWrapper(() -> updateVariantCacheEntry(backendResponse, responseDate, match), asyncExecCallback::failed);
1291                             }
1292                         }
1293                     }
1294                 }
1295                 callbackRef.set(callback);
1296                 return callback.handleResponse(backendResponse, entityDetails);
1297             }
1298 
1299             @Override
1300             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1301                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1302                 if (callback != null) {
1303                     callback.handleInformationResponse(response);
1304                 } else {
1305                     asyncExecCallback.handleInformationResponse(response);
1306                 }
1307             }
1308 
1309             @Override
1310             public void completed() {
1311                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1312                 if (callback != null) {
1313                     callback.completed();
1314                 } else {
1315                     asyncExecCallback.completed();
1316                 }
1317             }
1318 
1319             @Override
1320             public void failed(final Exception cause) {
1321                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1322                 if (callback != null) {
1323                     callback.failed(cause);
1324                 } else {
1325                     asyncExecCallback.failed(cause);
1326                 }
1327             }
1328 
1329         });
1330 
1331     }
1332 
1333 }