1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
86
87
88
89
90
91
92
93
94
95 @Contract(threading = ThreadingBehavior.SAFE)
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
431
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
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 }