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.message.RequestLine;
74  import org.apache.hc.core5.http.nio.AsyncDataConsumer;
75  import org.apache.hc.core5.http.nio.AsyncEntityProducer;
76  import org.apache.hc.core5.http.nio.CapacityChannel;
77  import org.apache.hc.core5.http.support.BasicRequestBuilder;
78  import org.apache.hc.core5.net.URIAuthority;
79  import org.apache.hc.core5.util.Args;
80  import org.apache.hc.core5.util.ByteArrayBuffer;
81  import org.slf4j.Logger;
82  import org.slf4j.LoggerFactory;
83  
84  /**
85   * Request executor in the request execution chain that is responsible for
86   * transparent client-side caching.
87   * <p>
88   * The current implementation is conditionally
89   * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
90   * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
91   * are obeyed too.
92   *
93   * @since 5.0
94   */
95  @Contract(threading = ThreadingBehavior.SAFE) // So long as the responseCache implementation is threadsafe
96  class AsyncCachingExec extends CachingExecBase implements AsyncExecChainHandler {
97  
98      private static final Logger LOG = LoggerFactory.getLogger(AsyncCachingExec.class);
99      private final HttpAsyncCache responseCache;
100     private final DefaultAsyncCacheRevalidator cacheRevalidator;
101     private final ConditionalRequestBuilder<HttpRequest> conditionalRequestBuilder;
102 
103     AsyncCachingExec(final HttpAsyncCache cache, final DefaultAsyncCacheRevalidator cacheRevalidator, final CacheConfig config) {
104         super(config);
105         this.responseCache = Args.notNull(cache, "Response cache");
106         this.cacheRevalidator = cacheRevalidator;
107         this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(request ->
108                 BasicRequestBuilder.copy(request).build());
109     }
110 
111     AsyncCachingExec(
112             final HttpAsyncCache cache,
113             final ScheduledExecutorService executorService,
114             final SchedulingStrategy schedulingStrategy,
115             final CacheConfig config) {
116         this(cache,
117                 executorService != null ? new DefaultAsyncCacheRevalidator(executorService, schedulingStrategy) : null,
118                 config);
119     }
120 
121     private void triggerResponse(
122             final SimpleHttpResponse cacheResponse,
123             final AsyncExecChain.Scope scope,
124             final AsyncExecCallback asyncExecCallback) {
125         scope.execRuntime.releaseEndpoint();
126 
127         final SimpleBody body = cacheResponse.getBody();
128         final byte[] content = body != null ? body.getBodyBytes() : null;
129         final ContentType contentType = body != null ? body.getContentType() : null;
130         try {
131             final AsyncDataConsumer dataConsumer = asyncExecCallback.handleResponse(
132                     cacheResponse,
133                     content != null ? new BasicEntityDetails(content.length, contentType) : null);
134             if (dataConsumer != null) {
135                 if (content != null) {
136                     dataConsumer.consume(ByteBuffer.wrap(content));
137                 }
138                 dataConsumer.streamEnd(null);
139             }
140             asyncExecCallback.completed();
141         } catch (final HttpException | IOException ex) {
142             asyncExecCallback.failed(ex);
143         }
144     }
145 
146     static class AsyncExecCallbackWrapper implements AsyncExecCallback {
147 
148         private final Runnable command;
149         private final Consumer<Exception> exceptionConsumer;
150 
151         AsyncExecCallbackWrapper(final Runnable command, final Consumer<Exception> exceptionConsumer) {
152             this.command = command;
153             this.exceptionConsumer = exceptionConsumer;
154         }
155 
156         @Override
157         public AsyncDataConsumer handleResponse(
158                 final HttpResponse response,
159                 final EntityDetails entityDetails) throws HttpException, IOException {
160             return null;
161         }
162 
163         @Override
164         public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
165         }
166 
167         @Override
168         public void completed() {
169             command.run();
170         }
171 
172         @Override
173         public void failed(final Exception cause) {
174             if (exceptionConsumer != null) {
175                 exceptionConsumer.accept(cause);
176             }
177         }
178 
179     }
180 
181     @Override
182     public void execute(
183             final HttpRequest request,
184             final AsyncEntityProducer entityProducer,
185             final AsyncExecChain.Scope scope,
186             final AsyncExecChain chain,
187             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
188         Args.notNull(request, "HTTP request");
189         Args.notNull(scope, "Scope");
190 
191         final HttpRoute route = scope.route;
192         final HttpClientContext context = scope.clientContext;
193 
194         final URIAuthority authority = request.getAuthority();
195         final String scheme = request.getScheme();
196         final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
197         doExecute(target,
198                 request,
199                 entityProducer,
200                 scope,
201                 chain,
202                 new AsyncExecCallback() {
203 
204                     @Override
205                     public AsyncDataConsumer handleResponse(
206                             final HttpResponse response,
207                             final EntityDetails entityDetails) throws HttpException, IOException {
208                         context.setRequest(request);
209                         context.setResponse(response);
210                         return asyncExecCallback.handleResponse(response, entityDetails);
211                     }
212 
213                     @Override
214                     public void handleInformationResponse(
215                             final HttpResponse response) throws HttpException, IOException {
216                         asyncExecCallback.handleInformationResponse(response);
217                     }
218 
219                     @Override
220                     public void completed() {
221                         asyncExecCallback.completed();
222                     }
223 
224                     @Override
225                     public void failed(final Exception cause) {
226                         asyncExecCallback.failed(cause);
227                     }
228 
229                 });
230     }
231 
232     public void doExecute(
233             final HttpHost target,
234             final HttpRequest request,
235             final AsyncEntityProducer entityProducer,
236             final AsyncExecChain.Scope scope,
237             final AsyncExecChain chain,
238             final AsyncExecCallback asyncExecCallback) throws HttpException, IOException {
239 
240         final String exchangeId = scope.exchangeId;
241         final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
242         final CancellableDependency operation = scope.cancellableDependency;
243 
244         if (LOG.isDebugEnabled()) {
245             LOG.debug("{} request via cache: {}", exchangeId, new RequestLine(request));
246         }
247 
248         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MISS);
249         context.setCacheEntry(null);
250 
251         if (clientRequestsOurOptions(request)) {
252             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
253             triggerResponse(SimpleHttpResponse.create(HttpStatus.SC_NOT_IMPLEMENTED), scope, asyncExecCallback);
254             return;
255         }
256 
257         final RequestCacheControl requestCacheControl;
258         if (request.containsHeader(HttpHeaders.CACHE_CONTROL)) {
259             requestCacheControl = CacheControlHeaderParser.INSTANCE.parse(request);
260             context.setRequestCacheControl(requestCacheControl);
261         } else {
262             requestCacheControl = context.getRequestCacheControlOrDefault();
263             CacheControlHeaderGenerator.INSTANCE.generate(requestCacheControl, request);
264         }
265 
266         if (LOG.isDebugEnabled()) {
267             LOG.debug("{} request cache control: {}", exchangeId, requestCacheControl);
268         }
269 
270         if (cacheableRequestPolicy.canBeServedFromCache(requestCacheControl, request)) {
271             operation.setDependency(responseCache.match(target, request, new FutureCallback<CacheMatch>() {
272 
273                 @Override
274                 public void completed(final CacheMatch result) {
275                     final CacheHit hit = result != null ? result.hit : null;
276                     final CacheHit root = result != null ? result.root : null;
277                     if (hit == null) {
278                         handleCacheMiss(requestCacheControl, root, target, request, entityProducer, scope, chain, asyncExecCallback);
279                     } else {
280                         final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(hit.entry);
281                         if (LOG.isDebugEnabled()) {
282                             LOG.debug("{} response cache control: {}", exchangeId, responseCacheControl);
283                         }
284                         context.setResponseCacheControl(responseCacheControl);
285                         handleCacheHit(requestCacheControl, responseCacheControl, hit, target, request, entityProducer, scope, chain, asyncExecCallback);
286                     }
287                 }
288 
289                 @Override
290                 public void failed(final Exception cause) {
291                     asyncExecCallback.failed(cause);
292                 }
293 
294                 @Override
295                 public void cancelled() {
296                     asyncExecCallback.failed(new InterruptedIOException());
297                 }
298 
299             }));
300 
301         } else {
302             if (LOG.isDebugEnabled()) {
303                 LOG.debug("{} request cannot be served from cache", exchangeId);
304             }
305             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
306         }
307     }
308 
309     void chainProceed(
310             final HttpRequest request,
311             final AsyncEntityProducer entityProducer,
312             final AsyncExecChain.Scope scope,
313             final AsyncExecChain chain,
314             final AsyncExecCallback asyncExecCallback) {
315         try {
316             chain.proceed(request, entityProducer, scope, asyncExecCallback);
317         } catch (final HttpException | IOException ex) {
318             asyncExecCallback.failed(ex);
319         }
320     }
321 
322     void callBackend(
323             final HttpHost target,
324             final HttpRequest request,
325             final AsyncEntityProducer entityProducer,
326             final AsyncExecChain.Scope scope,
327             final AsyncExecChain chain,
328             final AsyncExecCallback asyncExecCallback) {
329         final String exchangeId = scope.exchangeId;
330 
331         if (LOG.isDebugEnabled()) {
332             LOG.debug("{} calling the backend", exchangeId);
333         }
334         final Instant requestDate = getCurrentDate();
335         final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
336         chainProceed(request, entityProducer, scope, chain, new AsyncExecCallback() {
337 
338             @Override
339             public AsyncDataConsumer handleResponse(
340                     final HttpResponse backendResponse,
341                     final EntityDetails entityDetails) throws HttpException, IOException {
342                 final Instant responseDate = getCurrentDate();
343                 final AsyncExecCallback callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
344                 callbackRef.set(callback);
345                 return callback.handleResponse(backendResponse, entityDetails);
346             }
347 
348             @Override
349             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
350                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
351                 if (callback != null) {
352                     callback.handleInformationResponse(response);
353                 } else {
354                     asyncExecCallback.handleInformationResponse(response);
355                 }
356             }
357 
358             @Override
359             public void completed() {
360                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
361                 if (callback != null) {
362                     callback.completed();
363                 } else {
364                     asyncExecCallback.completed();
365                 }
366             }
367 
368             @Override
369             public void failed(final Exception cause) {
370                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
371                 if (callback != null) {
372                     callback.failed(cause);
373                 } else {
374                     asyncExecCallback.failed(cause);
375                 }
376             }
377 
378         });
379     }
380 
381     class CachingAsyncDataConsumer implements AsyncDataConsumer {
382 
383         private final String exchangeId;
384         private final AsyncExecCallback fallback;
385         private final HttpResponse backendResponse;
386         private final EntityDetails entityDetails;
387         private final AtomicBoolean writtenThrough;
388         private final AtomicReference<ByteArrayBuffer> bufferRef;
389         private final AtomicReference<AsyncDataConsumer> dataConsumerRef;
390 
391         CachingAsyncDataConsumer(
392                 final String exchangeId,
393                 final AsyncExecCallback fallback,
394                 final HttpResponse backendResponse,
395                 final EntityDetails entityDetails) {
396             this.exchangeId = exchangeId;
397             this.fallback = fallback;
398             this.backendResponse = backendResponse;
399             this.entityDetails = entityDetails;
400             this.writtenThrough = new AtomicBoolean(false);
401             this.bufferRef = new AtomicReference<>(entityDetails != null ? new ByteArrayBuffer(1024) : null);
402             this.dataConsumerRef = new AtomicReference<>();
403         }
404 
405         @Override
406         public final void updateCapacity(final CapacityChannel capacityChannel) throws IOException {
407             final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
408             if (dataConsumer != null) {
409                 dataConsumer.updateCapacity(capacityChannel);
410             } else {
411                 capacityChannel.update(Integer.MAX_VALUE);
412             }
413         }
414 
415         @Override
416         public final void consume(final ByteBuffer src) throws IOException {
417             final ByteArrayBuffer buffer = bufferRef.get();
418             if (buffer != null) {
419                 if (src.hasArray()) {
420                     buffer.append(src.array(), src.arrayOffset() + src.position(), src.remaining());
421                 } else {
422                     while (src.hasRemaining()) {
423                         buffer.append(src.get());
424                     }
425                 }
426                 if (buffer.length() > cacheConfig.getMaxObjectSize()) {
427                     if (LOG.isDebugEnabled()) {
428                         LOG.debug("{} backend response content length exceeds maximum", exchangeId);
429                     }
430                     // Over the max limit. Stop buffering and forward the response
431                     // along with all the data buffered so far to the caller.
432                     bufferRef.set(null);
433                     try {
434                         final AsyncDataConsumer dataConsumer = fallback.handleResponse(backendResponse, entityDetails);
435                         if (dataConsumer != null) {
436                             dataConsumerRef.set(dataConsumer);
437                             writtenThrough.set(true);
438                             dataConsumer.consume(ByteBuffer.wrap(buffer.array(), 0, buffer.length()));
439                         }
440                     } catch (final HttpException ex) {
441                         fallback.failed(ex);
442                     }
443                 }
444             } else {
445                 final AsyncDataConsumer dataConsumer = dataConsumerRef.get();
446                 if (dataConsumer != null) {
447                     dataConsumer.consume(src);
448                 }
449             }
450         }
451 
452         @Override
453         public final void streamEnd(final List<? extends Header> trailers) throws HttpException, IOException {
454             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
455             if (dataConsumer != null) {
456                 dataConsumer.streamEnd(trailers);
457             }
458         }
459 
460         @Override
461         public void releaseResources() {
462             final AsyncDataConsumer dataConsumer = dataConsumerRef.getAndSet(null);
463             if (dataConsumer != null) {
464                 dataConsumer.releaseResources();
465             }
466         }
467 
468     }
469 
470     class BackendResponseHandler implements AsyncExecCallback {
471 
472         private final HttpHost target;
473         private final HttpRequest request;
474         private final Instant requestDate;
475         private final Instant responseDate;
476         private final AsyncExecChain.Scope scope;
477         private final AsyncExecCallback asyncExecCallback;
478         private final AtomicReference<CachingAsyncDataConsumer> cachingConsumerRef;
479 
480         BackendResponseHandler(
481                 final HttpHost target,
482                 final HttpRequest request,
483                 final Instant requestDate,
484                 final Instant responseDate,
485                 final AsyncExecChain.Scope scope,
486                 final AsyncExecCallback asyncExecCallback) {
487             this.target = target;
488             this.request = request;
489             this.requestDate = requestDate;
490             this.responseDate = responseDate;
491             this.scope = scope;
492             this.asyncExecCallback = asyncExecCallback;
493             this.cachingConsumerRef = new AtomicReference<>();
494         }
495 
496         @Override
497         public AsyncDataConsumer handleResponse(
498                 final HttpResponse backendResponse,
499                 final EntityDetails entityDetails) throws HttpException, IOException {
500             final String exchangeId = scope.exchangeId;
501             responseCache.evictInvalidatedEntries(target, request, backendResponse, new FutureCallback<Boolean>() {
502 
503                 @Override
504                 public void completed(final Boolean result) {
505                 }
506 
507                 @Override
508                 public void failed(final Exception ex) {
509                     if (LOG.isDebugEnabled()) {
510                         LOG.debug("{} unable to flush invalidated entries from cache", exchangeId, ex);
511                     }
512                 }
513 
514                 @Override
515                 public void cancelled() {
516                 }
517 
518             });
519             if (isResponseTooBig(entityDetails)) {
520                 if (LOG.isDebugEnabled()) {
521                     LOG.debug("{} backend response is known to be too big", exchangeId);
522                 }
523                 return asyncExecCallback.handleResponse(backendResponse, entityDetails);
524             }
525 
526             final ResponseCacheControl responseCacheControl = CacheControlHeaderParser.INSTANCE.parse(backendResponse);
527             final boolean cacheable = responseCachingPolicy.isResponseCacheable(responseCacheControl, request, backendResponse);
528             if (cacheable) {
529                 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
530                 if (LOG.isDebugEnabled()) {
531                     LOG.debug("{} caching backend response", exchangeId);
532                 }
533                 final CachingAsyncDataConsumer cachingDataConsumer = new CachingAsyncDataConsumer(
534                         exchangeId, asyncExecCallback, backendResponse, entityDetails);
535                 cachingConsumerRef.set(cachingDataConsumer);
536                 return cachingDataConsumer;
537             } else {
538                 if (LOG.isDebugEnabled()) {
539                     LOG.debug("{} backend response is not cacheable", exchangeId);
540                 }
541                 return asyncExecCallback.handleResponse(backendResponse, entityDetails);
542             }
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, new RequestLine(request));
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(hit.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                 } else {
1083                     committed.set(response);
1084                     return asyncExecCallback.handleResponse(response, entityDetails);
1085                 }
1086             }
1087 
1088             @Override
1089             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1090                 asyncExecCallback.handleInformationResponse(response);
1091             }
1092 
1093             @Override
1094             public void completed() {
1095                 final HttpResponse response = committed.get();
1096                 if (response == null) {
1097                     try {
1098                         context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1099                         final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
1100                         context.setCacheEntry(hit.entry);
1101                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1102                     } catch (final IOException ex) {
1103                         asyncExecCallback.failed(ex);
1104                     }
1105                 } else {
1106                     asyncExecCallback.completed();
1107                 }
1108             }
1109 
1110             @Override
1111             public void failed(final Exception cause) {
1112                 final HttpResponse response = committed.get();
1113                 if (response == null) {
1114                     if (LOG.isDebugEnabled()) {
1115                         LOG.debug("{} I/O error while revalidating cache entry", exchangeId, cause);
1116                     }
1117                     context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1118                     if (cause instanceof IOException &&
1119                             suitabilityChecker.isSuitableIfError(requestCacheControl, responseCacheControl, hit.entry, getCurrentDate())) {
1120                         if (LOG.isDebugEnabled()) {
1121                             LOG.debug("{} serving stale response due to IOException and stale-if-error enabled", exchangeId);
1122                         }
1123                         try {
1124                             final SimpleHttpResponse cacheResponse = responseGenerator.generateResponse(request, hit.entry);
1125                             context.setCacheEntry(hit.entry);
1126                             triggerResponse(cacheResponse, scope, asyncExecCallback);
1127                         } catch (final IOException ex) {
1128                             asyncExecCallback.failed(cause);
1129                         }
1130                     } else {
1131                         final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1132                         triggerResponse(cacheResponse, scope, asyncExecCallback);
1133                     }
1134                 } else {
1135                     asyncExecCallback.failed(cause);
1136                 }
1137             }
1138 
1139         });
1140     }
1141     private void handleCacheMiss(
1142             final RequestCacheControl requestCacheControl,
1143             final CacheHit partialMatch,
1144             final HttpHost target,
1145             final HttpRequest request,
1146             final AsyncEntityProducer entityProducer,
1147             final AsyncExecChain.Scope scope,
1148             final AsyncExecChain chain,
1149             final AsyncExecCallback asyncExecCallback) {
1150         final String exchangeId = scope.exchangeId;
1151 
1152         if (LOG.isDebugEnabled()) {
1153             LOG.debug("{} cache miss: {}", exchangeId, new RequestLine(request));
1154         }
1155         cacheMisses.getAndIncrement();
1156 
1157         final CancellableDependency operation = scope.cancellableDependency;
1158         if (requestCacheControl.isOnlyIfCached()) {
1159             if (LOG.isDebugEnabled()) {
1160                 LOG.debug("{} request marked only-if-cached", exchangeId);
1161             }
1162             final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1163             context.setCacheResponseStatus(CacheResponseStatus.CACHE_MODULE_RESPONSE);
1164             final SimpleHttpResponse cacheResponse = generateGatewayTimeout();
1165             triggerResponse(cacheResponse, scope, asyncExecCallback);
1166         }
1167 
1168         if (partialMatch != null && partialMatch.entry.hasVariants() && entityProducer == null) {
1169             operation.setDependency(responseCache.getVariants(
1170                     partialMatch,
1171                     new FutureCallback<Collection<CacheHit>>() {
1172 
1173                         @Override
1174                         public void completed(final Collection<CacheHit> variants) {
1175                             if (variants != null && !variants.isEmpty()) {
1176                                 negotiateResponseFromVariants(target, request, entityProducer, scope, chain, asyncExecCallback, variants);
1177                             } else {
1178                                 callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1179                             }
1180                         }
1181 
1182                         @Override
1183                         public void failed(final Exception ex) {
1184                             asyncExecCallback.failed(ex);
1185                         }
1186 
1187                         @Override
1188                         public void cancelled() {
1189                             asyncExecCallback.failed(new InterruptedIOException());
1190                         }
1191 
1192                     }));
1193         } else {
1194             callBackend(target, request, entityProducer, scope, chain, asyncExecCallback);
1195         }
1196     }
1197 
1198     void negotiateResponseFromVariants(
1199             final HttpHost target,
1200             final HttpRequest request,
1201             final AsyncEntityProducer entityProducer,
1202             final AsyncExecChain.Scope scope,
1203             final AsyncExecChain chain,
1204             final AsyncExecCallback asyncExecCallback,
1205             final Collection<CacheHit> variants) {
1206         final String exchangeId = scope.exchangeId;
1207         final CancellableDependency operation = scope.cancellableDependency;
1208         final Map<ETag, CacheHit> variantMap = new HashMap<>();
1209         for (final CacheHit variant : variants) {
1210             final ETag eTag = variant.entry.getETag();
1211             if (eTag != null) {
1212                 variantMap.put(eTag, variant);
1213             }
1214         }
1215 
1216         final HttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(
1217                 request,
1218                 variantMap.keySet());
1219 
1220         final Instant requestDate = getCurrentDate();
1221         chainProceed(conditionalRequest, entityProducer, scope, chain, new AsyncExecCallback() {
1222 
1223             final AtomicReference<AsyncExecCallback> callbackRef = new AtomicReference<>();
1224 
1225             void updateVariantCacheEntry(final HttpResponse backendResponse, final Instant responseDate, final CacheHit match) {
1226                 final HttpCacheContext context = HttpCacheContext.cast(scope.clientContext);
1227                 context.setCacheResponseStatus(CacheResponseStatus.VALIDATED);
1228                 cacheUpdates.getAndIncrement();
1229 
1230                 operation.setDependency(responseCache.storeFromNegotiated(
1231                         match,
1232                         target,
1233                         request,
1234                         backendResponse,
1235                         requestDate,
1236                         responseDate,
1237                         new FutureCallback<CacheHit>() {
1238 
1239                             @Override
1240                             public void completed(final CacheHit hit) {
1241                                 try {
1242                                     final SimpleHttpResponse cacheResponse = generateCachedResponse(request, hit.entry, responseDate);
1243                                     context.setCacheEntry(hit.entry);
1244                                     triggerResponse(cacheResponse, scope, asyncExecCallback);
1245                                 } catch (final ResourceIOException ex) {
1246                                     asyncExecCallback.failed(ex);
1247                                 }
1248                             }
1249 
1250                             @Override
1251                             public void failed(final Exception ex) {
1252                                 asyncExecCallback.failed(ex);
1253                             }
1254 
1255                             @Override
1256                             public void cancelled() {
1257                                 asyncExecCallback.failed(new InterruptedIOException());
1258                             }
1259 
1260                         }));
1261             }
1262 
1263             @Override
1264             public AsyncDataConsumer handleResponse(
1265                     final HttpResponse backendResponse,
1266                     final EntityDetails entityDetails) throws HttpException, IOException {
1267                 final Instant responseDate = getCurrentDate();
1268                 final AsyncExecCallback callback;
1269                 if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
1270                     callback = new BackendResponseHandler(target, request, requestDate, responseDate, scope, asyncExecCallback);
1271                 } else {
1272                     final ETag resultEtag = ETag.get(backendResponse);
1273                     if (resultEtag == null) {
1274                         if (LOG.isDebugEnabled()) {
1275                             LOG.debug("{} 304 response did not contain ETag", exchangeId);
1276                         }
1277                         callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1278                     } else {
1279                         final CacheHit match = variantMap.get(resultEtag);
1280                         if (match == null) {
1281                             if (LOG.isDebugEnabled()) {
1282                                 LOG.debug("{} 304 response did not contain ETag matching one sent in If-None-Match", exchangeId);
1283                             }
1284                             callback = new AsyncExecCallbackWrapper(() -> callBackend(target, request, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1285                         } else {
1286                             if (HttpCacheEntry.isNewer(match.entry, backendResponse)) {
1287                                 final HttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
1288                                         BasicRequestBuilder.copy(request).build());
1289                                 callback = new AsyncExecCallbackWrapper(() -> callBackend(target, unconditional, entityProducer, scope, chain, asyncExecCallback), asyncExecCallback::failed);
1290                             } else {
1291                                 callback = new AsyncExecCallbackWrapper(() -> updateVariantCacheEntry(backendResponse, responseDate, match), asyncExecCallback::failed);
1292                             }
1293                         }
1294                     }
1295                 }
1296                 callbackRef.set(callback);
1297                 return callback.handleResponse(backendResponse, entityDetails);
1298             }
1299 
1300             @Override
1301             public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
1302                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1303                 if (callback != null) {
1304                     callback.handleInformationResponse(response);
1305                 } else {
1306                     asyncExecCallback.handleInformationResponse(response);
1307                 }
1308             }
1309 
1310             @Override
1311             public void completed() {
1312                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1313                 if (callback != null) {
1314                     callback.completed();
1315                 } else {
1316                     asyncExecCallback.completed();
1317                 }
1318             }
1319 
1320             @Override
1321             public void failed(final Exception cause) {
1322                 final AsyncExecCallback callback = callbackRef.getAndSet(null);
1323                 if (callback != null) {
1324                     callback.failed(cause);
1325                 } else {
1326                     asyncExecCallback.failed(cause);
1327                 }
1328             }
1329 
1330         });
1331 
1332     }
1333 
1334 }