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