All Downloads are FREE. Search and download functionalities are using the official Maven repository.

keycloakjar.com.github.benmanes.caffeine.cache.LocalAsyncCache Maven / Gradle / Ivy

There is a newer version: 7.22.0
Show newest version
/*
 * Copyright 2018 Ben Manes. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.benmanes.caffeine.cache;

import static java.util.Objects.requireNonNull;

import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import com.github.benmanes.caffeine.cache.LocalAsyncCache.AsyncBulkCompleter.NullMapCompletionException;
import com.github.benmanes.caffeine.cache.stats.CacheStats;

/**
 * This class provides a skeletal implementation of the {@link AsyncCache} interface to minimize the
 * effort required to implement a {@link LocalCache}.
 *
 * @author [email protected] (Ben Manes)
 */
interface LocalAsyncCache extends AsyncCache {
  Logger logger = Logger.getLogger(LocalAsyncCache.class.getName());

  /** Returns the backing {@link LocalCache} data store. */
  LocalCache> cache();

  /** Returns the policy supported by this implementation and its configuration. */
  Policy policy();

  @Override
  default @Nullable CompletableFuture getIfPresent(@NonNull Object key) {
    return cache().getIfPresent(key, /* recordStats */ true);
  }

  @Override
  default CompletableFuture get(@NonNull K key,
      @NonNull Function mappingFunction) {
    requireNonNull(mappingFunction);
    return get(key, (k1, executor) -> CompletableFuture.supplyAsync(
        () -> mappingFunction.apply(key), executor));
  }

  @Override
  default CompletableFuture get(K key,
      BiFunction> mappingFunction) {
    return get(key, mappingFunction, /* recordStats */ true);
  }

  @SuppressWarnings({"FutureReturnValueIgnored", "NullAway"})
  default CompletableFuture get(K key,
      BiFunction> mappingFunction, boolean recordStats) {
    long startTime = cache().statsTicker().read();
    @SuppressWarnings({"unchecked", "rawtypes"})
    CompletableFuture[] result = new CompletableFuture[1];
    CompletableFuture future = cache().computeIfAbsent(key, k -> {
      result[0] = mappingFunction.apply(key, cache().executor());
      return requireNonNull(result[0]);
    }, recordStats, /* recordLoad */ false);
    if (result[0] != null) {
      handleCompletion(key, result[0], startTime, /* recordMiss */ false);
    }
    return future;
  }

  @Override
  default CompletableFuture> getAll(Iterable keys,
      Function, Map> mappingFunction) {
    requireNonNull(mappingFunction);
    return getAll(keys, (keysToLoad, executor) ->
        CompletableFuture.supplyAsync(() -> mappingFunction.apply(keysToLoad), executor));
  }

  @Override
  @SuppressWarnings("FutureReturnValueIgnored")
  default CompletableFuture> getAll(Iterable keys,
      BiFunction, Executor, CompletableFuture>> mappingFunction) {
    requireNonNull(mappingFunction);
    requireNonNull(keys);

    Map> futures = new LinkedHashMap<>();
    Map> proxies = new HashMap<>();
    for (K key : keys) {
      if (futures.containsKey(key)) {
        continue;
      }
      CompletableFuture future = cache().getIfPresent(key, /* recordStats */ false);
      if (future == null) {
        CompletableFuture proxy = new CompletableFuture<>();
        future = cache().putIfAbsent(key, proxy);
        if (future == null) {
          future = proxy;
          proxies.put(key, proxy);
        }
      }
      futures.put(key, future);
    }
    cache().statsCounter().recordMisses(proxies.size());
    cache().statsCounter().recordHits(futures.size() - proxies.size());
    if (proxies.isEmpty()) {
      return composeResult(futures);
    }

    AsyncBulkCompleter completer = new AsyncBulkCompleter<>(cache(), proxies);
    try {
      mappingFunction.apply(proxies.keySet(), cache().executor()).whenComplete(completer);
      return composeResult(futures);
    } catch (Throwable t) {
      completer.accept(/* result */ null, t);
      throw t;
    }
  }

  /**
   * Returns a future that waits for all of the dependent futures to complete and returns the
   * combined mapping if successful. If any future fails then it is automatically removed from
   * the cache if still present.
   */
  default CompletableFuture> composeResult(Map> futures) {
    if (futures.isEmpty()) {
      return CompletableFuture.completedFuture(Collections.emptyMap());
    }
    @SuppressWarnings("rawtypes")
    CompletableFuture[] array = futures.values().toArray(new CompletableFuture[0]);
    return CompletableFuture.allOf(array).thenApply(ignored -> {
      Map result = new LinkedHashMap<>(futures.size());
      futures.forEach((key, future) -> {
        V value = future.getNow(null);
        if (value != null) {
          result.put(key, value);
        }
      });
      return Collections.unmodifiableMap(result);
    });
  }

  @Override
  @SuppressWarnings("FutureReturnValueIgnored")
  default void put(K key, CompletableFuture valueFuture) {
    if (valueFuture.isCompletedExceptionally()
        || (valueFuture.isDone() && (valueFuture.join() == null))) {
      cache().statsCounter().recordLoadFailure(0L);
      cache().remove(key);
      return;
    }
    long startTime = cache().statsTicker().read();
    cache().put(key, valueFuture);
    handleCompletion(key, valueFuture, startTime, /* recordMiss */ false);
  }

  @SuppressWarnings("FutureReturnValueIgnored")
  default void handleCompletion(K key, CompletableFuture valueFuture,
      long startTime, boolean recordMiss) {
    AtomicBoolean completed = new AtomicBoolean();
    valueFuture.whenComplete((value, error) -> {
      if (!completed.compareAndSet(false, true)) {
        // Ignore multiple invocations due to ForkJoinPool retrying on delays
        return;
      }
      long loadTime = cache().statsTicker().read() - startTime;
      if (value == null) {
        if ((error != null) && !(error instanceof CancellationException)
            && !(error instanceof TimeoutException)) {
          logger.log(Level.WARNING, "Exception thrown during asynchronous load", error);
        }
        cache().remove(key, valueFuture);
        cache().statsCounter().recordLoadFailure(loadTime);
        if (recordMiss) {
          cache().statsCounter().recordMisses(1);
        }
      } else {
        // update the weight and expiration timestamps
        cache().replace(key, valueFuture, valueFuture);
        cache().statsCounter().recordLoadSuccess(loadTime);
        if (recordMiss) {
          cache().statsCounter().recordMisses(1);
        }
      }
    });
  }

  /** A function executed asynchronously after a bulk load completes. */
  final class AsyncBulkCompleter implements BiConsumer, Throwable> {
    private final LocalCache> cache;
    private final Map> proxies;
    private final long startTime;

    AsyncBulkCompleter(LocalCache> cache,
        Map> proxies) {
      this.startTime = cache.statsTicker().read();
      this.proxies = proxies;
      this.cache = cache;
    }

    @Override
    public void accept(@Nullable Map result, @Nullable Throwable error) {
      long loadTime = cache.statsTicker().read() - startTime;

      if (result == null) {
        if (error == null) {
          error = new NullMapCompletionException();
        }
        for (Map.Entry> entry : proxies.entrySet()) {
          cache.remove(entry.getKey(), entry.getValue());
          entry.getValue().obtrudeException(error);
        }
        cache.statsCounter().recordLoadFailure(loadTime);
        if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
          logger.log(Level.WARNING, "Exception thrown during asynchronous load", error);
        }
      } else {
        fillProxies(result);
        addNewEntries(result);
        cache.statsCounter().recordLoadSuccess(loadTime);
      }
    }

    /** Populates the proxies with the computed result. */
    private void fillProxies(Map result) {
      proxies.forEach((key, future) -> {
        V value = result.get(key);
        future.obtrudeValue(value);
        if (value == null) {
          cache.remove(key, future);
        } else {
          // update the weight and expiration timestamps
          cache.replace(key, future, future);
        }
      });
    }

    /** Adds to the cache any extra entries computed that were not requested. */
    private void addNewEntries(Map result) {
      if (proxies.size() == result.size()) {
        return;
      }
      result.forEach((key, value) -> {
        if (!proxies.containsKey(key)) {
          cache.put(key, CompletableFuture.completedFuture(value));
        }
      });
    }

    static final class NullMapCompletionException extends CompletionException {
      private static final long serialVersionUID = 1L;

      public NullMapCompletionException() {
        super("null map", null);
      }
    }
  }

  /* --------------- Asynchronous view --------------- */
  final class AsyncAsMapView implements ConcurrentMap> {
    final LocalAsyncCache asyncCache;

    AsyncAsMapView(LocalAsyncCache asyncCache) {
      this.asyncCache = requireNonNull(asyncCache);
    }
    @Override public boolean isEmpty() {
      return asyncCache.cache().isEmpty();
    }
    @Override public int size() {
      return asyncCache.cache().size();
    }
    @Override public void clear() {
      asyncCache.cache().clear();
    }
    @Override public boolean containsKey(Object key) {
      return asyncCache.cache().containsKey(key);
    }
    @Override public boolean containsValue(Object value) {
      return asyncCache.cache().containsValue(value);
    }
    @Override public @Nullable CompletableFuture get(Object key) {
      return asyncCache.cache().get(key);
    }
    @Override public CompletableFuture putIfAbsent(K key, CompletableFuture value) {
      CompletableFuture prior = asyncCache.cache().putIfAbsent(key, value);
      long startTime = asyncCache.cache().statsTicker().read();
      if (prior == null) {
        asyncCache.handleCompletion(key, value, startTime, /* recordMiss */ false);
      }
      return prior;
    }
    @Override public CompletableFuture put(K key, CompletableFuture value) {
      CompletableFuture prior = asyncCache.cache().put(key, value);
      long startTime = asyncCache.cache().statsTicker().read();
      asyncCache.handleCompletion(key, value, startTime, /* recordMiss */ false);
      return prior;
    }
    @SuppressWarnings("FutureReturnValueIgnored")
    @Override public void putAll(Map> map) {
      map.forEach(this::put);
    }
    @Override public CompletableFuture replace(K key, CompletableFuture value) {
      CompletableFuture prior = asyncCache.cache().replace(key, value);
      long startTime = asyncCache.cache().statsTicker().read();
      if (prior != null) {
        asyncCache.handleCompletion(key, value, startTime, /* recordMiss */ false);
      }
      return prior;
    }
    @Override
    public boolean replace(K key, CompletableFuture oldValue, CompletableFuture newValue) {
      boolean replaced = asyncCache.cache().replace(key, oldValue, newValue);
      long startTime = asyncCache.cache().statsTicker().read();
      if (replaced) {
        asyncCache.handleCompletion(key, newValue, startTime, /* recordMiss */ false);
      }
      return replaced;
    }
    @Override public CompletableFuture remove(Object key) {
      return asyncCache.cache().remove(key);
    }
    @Override public boolean remove(Object key, Object value) {
      return asyncCache.cache().remove(key, value);
    }
    @SuppressWarnings("FutureReturnValueIgnored")
    @Override public @Nullable CompletableFuture computeIfAbsent(K key,
        Function> mappingFunction) {
      requireNonNull(mappingFunction);
      @SuppressWarnings({"rawtypes", "unchecked"})
      CompletableFuture[] result = new CompletableFuture[1];
      long startTime = asyncCache.cache().statsTicker().read();
      CompletableFuture future = asyncCache.cache().computeIfAbsent(key, k -> {
        result[0] = mappingFunction.apply(k);
        return result[0];
      }, /* recordStats */ false, /* recordLoad */ false);

      if (result[0] == null) {
        if ((future != null) && asyncCache.cache().isRecordingStats()) {
          future.whenComplete((r, e) -> {
            if ((r != null) || (e == null)) {
              asyncCache.cache().statsCounter().recordHits(1);
            }
          });
        }
      } else {
        asyncCache.handleCompletion(key, result[0], startTime, /* recordMiss */ true);
      }
      return future;
    }
    @Override public CompletableFuture computeIfPresent(K key, BiFunction, ? extends CompletableFuture> remappingFunction) {
      requireNonNull(remappingFunction);

      @SuppressWarnings({"rawtypes", "unchecked"})
      CompletableFuture[] result = new CompletableFuture[1];
      long startTime = asyncCache.cache().statsTicker().read();
      asyncCache.cache().compute(key, (k, oldValue) -> {
        result[0] = (oldValue == null) ? null : remappingFunction.apply(k, oldValue);
        return result[0];
      }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

      if (result[0] != null) {
        asyncCache.handleCompletion(key, result[0], startTime, /* recordMiss */ false);
      }
      return result[0];
    }
    @Override public CompletableFuture compute(K key, BiFunction, ? extends CompletableFuture> remappingFunction) {
      requireNonNull(remappingFunction);

      @SuppressWarnings({"rawtypes", "unchecked"})
      CompletableFuture[] result = new CompletableFuture[1];
      long startTime = asyncCache.cache().statsTicker().read();
      asyncCache.cache().compute(key, (k, oldValue) -> {
        result[0] = remappingFunction.apply(k, oldValue);
        return result[0];
      }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

      if (result[0] != null) {
        asyncCache.handleCompletion(key, result[0], startTime, /* recordMiss */ false);
      }
      return result[0];
    }
    @Override public CompletableFuture merge(K key, CompletableFuture value,
        BiFunction, ? super CompletableFuture,
            ? extends CompletableFuture> remappingFunction) {
      requireNonNull(value);
      requireNonNull(remappingFunction);

      @SuppressWarnings({"rawtypes", "unchecked"})
      CompletableFuture[] result = new CompletableFuture[1];
      long startTime = asyncCache.cache().statsTicker().read();
      asyncCache.cache().compute(key, (k, oldValue) -> {
        result[0] = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
        return result[0];
      }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

      if (result[0] != null) {
        asyncCache.handleCompletion(key, result[0], startTime, /* recordMiss */ false);
      }
      return result[0];
    }
    @Override public Set keySet() {
      return asyncCache.cache().keySet();
    }
    @Override public Collection> values() {
      return asyncCache.cache().values();
    }
    @Override public Set>> entrySet() {
      return asyncCache.cache().entrySet();
    }
    @Override public boolean equals(Object o) {
      return asyncCache.cache().equals(o);
    }
    @Override public int hashCode() {
      return asyncCache.cache().hashCode();
    }
    @Override public String toString() {
      return asyncCache.cache().toString();
    }
  }

  /* --------------- Synchronous view --------------- */
  final class CacheView extends AbstractCacheView {
    private static final long serialVersionUID = 1L;

    final LocalAsyncCache asyncCache;

    CacheView(LocalAsyncCache asyncCache) {
      this.asyncCache = requireNonNull(asyncCache);
    }
    @Override LocalAsyncCache asyncCache() {
      return asyncCache;
    }
  }

  @SuppressWarnings("serial")
  abstract class AbstractCacheView implements Cache, Serializable {
    transient @Nullable AsMapView asMapView;

    abstract LocalAsyncCache asyncCache();

    @Override
    public @Nullable V getIfPresent(Object key) {
      CompletableFuture future = asyncCache().cache().getIfPresent(key, /* recordStats */ true);
      return Async.getIfReady(future);
    }

    @Override
    public Map getAllPresent(Iterable keys) {
      Set uniqueKeys = new LinkedHashSet<>();
      for (Object key : keys) {
        uniqueKeys.add(key);
      }

      int misses = 0;
      Map result = new LinkedHashMap<>();
      for (Object key : uniqueKeys) {
        CompletableFuture future = asyncCache().cache().get(key);
        Object value = Async.getIfReady(future);
        if (value == null) {
          misses++;
        } else {
          result.put(key, value);
        }
      }
      asyncCache().cache().statsCounter().recordMisses(misses);
      asyncCache().cache().statsCounter().recordHits(result.size());

      @SuppressWarnings("unchecked")
      Map castedResult = (Map) result;
      return Collections.unmodifiableMap(castedResult);
    }

    @Override
    public V get(K key, Function mappingFunction) {
      return resolve(asyncCache().get(key, mappingFunction));
    }

    @Override
    public Map getAll(Iterable keys,
        Function, Map> mappingFunction) {
      return resolve(asyncCache().getAll(keys, mappingFunction));
    }

    @SuppressWarnings({"PMD.AvoidThrowingNullPointerException", "PMD.PreserveStackTrace"})
    protected static  T resolve(CompletableFuture future) throws Error {
      try {
        return future.get();
      } catch (ExecutionException e) {
        if (e.getCause() instanceof NullMapCompletionException) {
          throw new NullPointerException(e.getCause().getMessage());
        } else if (e.getCause() instanceof RuntimeException) {
          throw (RuntimeException) e.getCause();
        } else if (e.getCause() instanceof Error) {
          throw (Error) e.getCause();
        }
        throw new CompletionException(e.getCause());
      } catch (InterruptedException e) {
        throw new CompletionException(e);
      }
    }

    @Override
    public void put(K key, V value) {
      requireNonNull(value);
      asyncCache().cache().put(key, CompletableFuture.completedFuture(value));
    }

    @Override
    public void putAll(Map map) {
      map.forEach(this::put);
    }

    @Override
    public void invalidate(Object key) {
      asyncCache().cache().remove(key);
    }

    @Override
    public void invalidateAll(Iterable keys) {
      asyncCache().cache().invalidateAll(keys);
    }

    @Override
    public void invalidateAll() {
      asyncCache().cache().clear();
    }

    @Override
    public long estimatedSize() {
      return asyncCache().cache().estimatedSize();
    }

    @Override
    public CacheStats stats() {
      return asyncCache().cache().statsCounter().snapshot();
    }

    @Override
    public void cleanUp() {
      asyncCache().cache().cleanUp();
    }

    @Override
    public Policy policy() {
      return asyncCache().policy();
    }

    @Override
    public ConcurrentMap asMap() {
      return (asMapView == null) ? (asMapView = new AsMapView<>(asyncCache().cache())) : asMapView;
    }
  }

  final class AsMapView extends AbstractMap implements ConcurrentMap {
    final LocalCache> delegate;

    @Nullable Collection values;
    @Nullable Set> entries;

    AsMapView(LocalCache> delegate) {
      this.delegate = delegate;
    }

    @Override
    public boolean isEmpty() {
      return delegate.isEmpty();
    }

    @Override
    public int size() {
      return delegate.size();
    }

    @Override
    public void clear() {
      delegate.clear();
    }

    @Override
    public boolean containsKey(Object key) {
      return delegate.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
      requireNonNull(value);

      for (CompletableFuture valueFuture : delegate.values()) {
        if (value.equals(Async.getIfReady(valueFuture))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public @Nullable V get(Object key) {
      return Async.getIfReady(delegate.get(key));
    }

    @Override
    public @Nullable V putIfAbsent(K key, V value) {
      requireNonNull(value);

      for (;;) {
        CompletableFuture priorFuture = delegate.get(key);
        if (priorFuture != null) {
          if (!priorFuture.isDone()) {
            Async.getWhenSuccessful(priorFuture);
            continue;
          }

          V prior = Async.getWhenSuccessful(priorFuture);
          if (prior != null) {
            return prior;
          }
        }

        boolean[] added = { false };
        CompletableFuture computed = delegate.compute(key, (k, valueFuture) -> {
          added[0] = (valueFuture == null)
              || (valueFuture.isDone() && (Async.getIfReady(valueFuture) == null));
          return added[0] ? CompletableFuture.completedFuture(value) : valueFuture;
        }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

        if (added[0]) {
          return null;
        } else {
          V prior = Async.getWhenSuccessful(computed);
          if (prior != null) {
            return prior;
          }
        }
      }
    }

    @Override
    public @Nullable V put(K key, V value) {
      requireNonNull(value);
      CompletableFuture oldValueFuture =
          delegate.put(key, CompletableFuture.completedFuture(value));
      return Async.getWhenSuccessful(oldValueFuture);
    }

    @Override
    public @Nullable V remove(Object key) {
      CompletableFuture oldValueFuture = delegate.remove(key);
      return Async.getWhenSuccessful(oldValueFuture);
    }

    @Override
    public boolean remove(Object key, Object value) {
      requireNonNull(key);
      if (value == null) {
        return false;
      }

      @SuppressWarnings("unchecked")
      K castedKey = (K) key;
      boolean[] done = { false };
      boolean[] removed = { false };
      for (;;) {
        CompletableFuture future = delegate.get(key);
        if ((future == null) || future.isCompletedExceptionally()) {
          return false;
        }

        Async.getWhenSuccessful(future);
        delegate.compute(castedKey, (k, oldValueFuture) -> {
          if (oldValueFuture == null) {
            done[0] = true;
            return null;
          } else if (!oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          done[0] = true;
          V oldValue = Async.getIfReady(oldValueFuture);
          removed[0] = value.equals(oldValue);
          return (oldValue == null) || removed[0] ? null : oldValueFuture;
        }, /* recordStats */ false, /* recordLoad */ false, /* recordLoadFailure */ true);

        if (done[0]) {
          return removed[0];
        }
      }
    }

    @Override
    public @Nullable V replace(K key, V value) {
      requireNonNull(value);

      @SuppressWarnings({"unchecked", "rawtypes"})
      V[] oldValue = (V[]) new Object[1];
      boolean[] done = { false };
      for (;;) {
        CompletableFuture future = delegate.get(key);
        if ((future == null) || future.isCompletedExceptionally()) {
          return null;
        }

        Async.getWhenSuccessful(future);
        delegate.compute(key, (k, oldValueFuture) -> {
          if (oldValueFuture == null) {
            done[0] = true;
            return null;
          } else if (!oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          done[0] = true;
          oldValue[0] = Async.getIfReady(oldValueFuture);
          return (oldValue[0] == null) ? null : CompletableFuture.completedFuture(value);
        }, /* recordStats */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

        if (done[0]) {
          return oldValue[0];
        }
      }
    }

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
      requireNonNull(oldValue);
      requireNonNull(newValue);

      boolean[] done = { false };
      boolean[] replaced = { false };
      for (;;) {
        CompletableFuture future = delegate.get(key);
        if ((future == null) || future.isCompletedExceptionally()) {
          return false;
        }

        Async.getWhenSuccessful(future);
        delegate.compute(key, (k, oldValueFuture) -> {
          if (oldValueFuture == null) {
            done[0] = true;
            return null;
          } else if (!oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          done[0] = true;
          replaced[0] = oldValue.equals(Async.getIfReady(oldValueFuture));
          return replaced[0] ? CompletableFuture.completedFuture(newValue) : oldValueFuture;
        }, /* recordStats */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

        if (done[0]) {
          return replaced[0];
        }
      }
    }

    @Override
    public @Nullable V computeIfAbsent(K key, Function mappingFunction) {
      requireNonNull(mappingFunction);

      for (;;) {
        CompletableFuture priorFuture = delegate.get(key);
        if (priorFuture != null) {
          if (!priorFuture.isDone()) {
            Async.getWhenSuccessful(priorFuture);
            continue;
          }

          V prior = Async.getWhenSuccessful(priorFuture);
          if (prior != null) {
            delegate.statsCounter().recordHits(1);
            return prior;
          }
        }

        @SuppressWarnings({"unchecked", "rawtypes"})
        CompletableFuture[] future = new CompletableFuture[1];
        CompletableFuture computed = delegate.compute(key, (k, valueFuture) -> {
          if ((valueFuture != null) && valueFuture.isDone()
              && (Async.getIfReady(valueFuture) != null)) {
            return valueFuture;
          }

          V newValue = delegate.statsAware(mappingFunction, /* recordLoad */ true).apply(key);
          if (newValue == null) {
            return null;
          }
          future[0] = CompletableFuture.completedFuture(newValue);
          return future[0];
        }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

        V result = Async.getWhenSuccessful(computed);
        if ((computed == future[0]) || (result != null)) {
          return result;
        }
      }
    }

    @Override
    public @Nullable V computeIfPresent(K key,
        BiFunction remappingFunction) {
      requireNonNull(remappingFunction);

      @SuppressWarnings({"unchecked", "rawtypes"})
      V[] newValue = (V[]) new Object[1];
      for (;;) {
        Async.getWhenSuccessful(delegate.get(key));

        CompletableFuture valueFuture = delegate.computeIfPresent(key, (k, oldValueFuture) -> {
          if (!oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          V oldValue = Async.getIfReady(oldValueFuture);
          if (oldValue == null) {
            return null;
          }

          newValue[0] = remappingFunction.apply(key, oldValue);
          return (newValue[0] == null) ? null : CompletableFuture.completedFuture(newValue[0]);
        });

        if (newValue[0] != null) {
          return newValue[0];
        } else if (valueFuture == null) {
          return null;
        }
      }
    }

    @Override
    public @Nullable V compute(K key,
        BiFunction remappingFunction) {
      requireNonNull(remappingFunction);

      @SuppressWarnings({"unchecked", "rawtypes"})
      V[] newValue = (V[]) new Object[1];
      for (;;) {
        Async.getWhenSuccessful(delegate.get(key));

        CompletableFuture valueFuture = delegate.compute(key, (k, oldValueFuture) -> {
          if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          V oldValue = Async.getIfReady(oldValueFuture);
          BiFunction function = delegate.statsAware(
              remappingFunction, /* recordMiss */ false, /* recordLoad */ true,
              /* recordLoadFailure */ true);
          newValue[0] = function.apply(key, oldValue);
          return (newValue[0] == null) ? null : CompletableFuture.completedFuture(newValue[0]);
        }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ false);

        if (newValue[0] != null) {
          return newValue[0];
        } else if (valueFuture == null) {
          return null;
        }
      }
    }

    @Override
    public @Nullable V merge(K key, V value,
        BiFunction remappingFunction) {
      requireNonNull(value);
      requireNonNull(remappingFunction);

      CompletableFuture newValueFuture = CompletableFuture.completedFuture(value);
      boolean[] merged = { false };
      for (;;) {
        Async.getWhenSuccessful(delegate.get(key));

        CompletableFuture mergedValueFuture = delegate.merge(
            key, newValueFuture, (oldValueFuture, valueFuture) -> {
          if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
            return oldValueFuture;
          }

          merged[0] = true;
          V oldValue = Async.getIfReady(oldValueFuture);
          if (oldValue == null) {
            return valueFuture;
          }
          V mergedValue = remappingFunction.apply(oldValue, value);
          if (mergedValue == null) {
            return null;
          } else if (mergedValue == oldValue) {
            return oldValueFuture;
          } else if (mergedValue == value) {
            return valueFuture;
          }
          return CompletableFuture.completedFuture(mergedValue);
        });

        if (merged[0] || (mergedValueFuture == newValueFuture)) {
          return Async.getWhenSuccessful(mergedValueFuture);
        }
      }
    }

    @Override
    public Set keySet() {
      return delegate.keySet();
    }

    @Override
    public Collection values() {
      return (values == null) ? (values = new Values()) : values;
    }

    @Override
    public Set> entrySet() {
      return (entries == null) ? (entries = new EntrySet()) : entries;
    }

    private final class Values extends AbstractCollection {

      @Override
      public boolean isEmpty() {
        return AsMapView.this.isEmpty();
      }

      @Override
      public int size() {
        return AsMapView.this.size();
      }

      @Override
      public boolean contains(Object o) {
        return AsMapView.this.containsValue(o);
      }

      @Override
      public void clear() {
        AsMapView.this.clear();
      }

      @Override
      public Iterator iterator() {
        return new Iterator() {
          Iterator> iterator = entrySet().iterator();

          @Override
          public boolean hasNext() {
            return iterator.hasNext();
          }

          @Override
          public V next() {
            return iterator.next().getValue();
          }

          @Override
          public void remove() {
            iterator.remove();
          }
        };
      }
    }

    private final class EntrySet extends AbstractSet> {

      @Override
      public boolean isEmpty() {
        return AsMapView.this.isEmpty();
      }

      @Override
      public int size() {
        return AsMapView.this.size();
      }

      @Override
      public boolean contains(Object o) {
        if (!(o instanceof Entry)) {
          return false;
        }
        Entry entry = (Entry) o;
        Object key = entry.getKey();
        Object value = entry.getValue();
        if ((key == null) || (value == null)) {
          return false;
        }
        V cachedValue = AsMapView.this.get(key);
        return (cachedValue != null) && cachedValue.equals(value);
      }

      @Override
      public boolean remove(Object obj) {
        if (!(obj instanceof Entry)) {
          return false;
        }
        Entry entry = (Entry) obj;
        return AsMapView.this.remove(entry.getKey(), entry.getValue());
      }

      @Override
      public void clear() {
        AsMapView.this.clear();
      }

      @Override
      public Iterator> iterator() {
        return new Iterator>() {
          Iterator>> iterator = delegate.entrySet().iterator();
          @Nullable Entry cursor;
          @Nullable K removalKey;

          @Override
          public boolean hasNext() {
            while ((cursor == null) && iterator.hasNext()) {
              Entry> entry = iterator.next();
              V value = Async.getIfReady(entry.getValue());
              if (value != null) {
                cursor = new WriteThroughEntry<>(AsMapView.this, entry.getKey(), value);
              }
            }
            return (cursor != null);
          }

          @Override
          public Entry next() {
            if (!hasNext()) {
              throw new NoSuchElementException();
            }
            @SuppressWarnings("NullAway")
            K key = cursor.getKey();
            Entry entry = cursor;
            removalKey = key;
            cursor = null;
            return entry;
          }

          @Override
          public void remove() {
            Caffeine.requireState(removalKey != null);
            delegate.remove(removalKey);
            removalKey = null;
          }
        };
      }
    }
  }
}