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

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

There is a newer version: 7.22.0
Show newest version
/*
 * Copyright 2015 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 com.github.benmanes.caffeine.cache.Caffeine.calculateHashMapCapacity;
import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult; // NOPMD
import static java.util.Objects.requireNonNull;

import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;
import java.util.function.Function;

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

/**
 * This class provides a skeletal implementation of the {@link AsyncLoadingCache} interface to
 * minimize the effort required to implement a {@link LocalCache}.
 *
 * @author [email protected] (Ben Manes)
 */
abstract class LocalAsyncLoadingCache
    implements LocalAsyncCache, AsyncLoadingCache {
  static final Logger logger = System.getLogger(LocalAsyncLoadingCache.class.getName());

  final @Nullable BiFunction, ? super Executor,
      ? extends CompletableFuture>> bulkMappingFunction;
  final BiFunction> mappingFunction;
  final AsyncCacheLoader cacheLoader;

  @Nullable LoadingCacheView cacheView;

  @SuppressWarnings("unchecked")
  LocalAsyncLoadingCache(AsyncCacheLoader cacheLoader) {
    this.bulkMappingFunction = newBulkMappingFunction(cacheLoader);
    this.cacheLoader = (AsyncCacheLoader) cacheLoader;
    this.mappingFunction = newMappingFunction(cacheLoader);
  }

  /** Returns a mapping function that adapts to {@link AsyncCacheLoader#asyncLoad}. */
  BiFunction> newMappingFunction(
      AsyncCacheLoader cacheLoader) {
    return (key, executor) -> {
      try {
        return cacheLoader.asyncLoad(key, executor);
      } catch (RuntimeException e) {
        throw e;
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new CompletionException(e);
      } catch (Exception e) {
        throw new CompletionException(e);
      }
    };
  }

  /**
   * Returns a mapping function that adapts to {@link AsyncCacheLoader#asyncLoadAll}, if
   * implemented.
   */
  @Nullable
  BiFunction, Executor, CompletableFuture>> newBulkMappingFunction(
      AsyncCacheLoader cacheLoader) {
    if (!canBulkLoad(cacheLoader)) {
      return null;
    }
    return (keysToLoad, executor) -> {
      try {
        @SuppressWarnings("unchecked")
        var loaded = (CompletableFuture>) cacheLoader.asyncLoadAll(keysToLoad, executor);
        return loaded;
      } catch (RuntimeException e) {
        throw e;
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new CompletionException(e);
      } catch (Exception e) {
        throw new CompletionException(e);
      }
    };
  }

  /** Returns whether the supplied cache loader has bulk load functionality. */
  boolean canBulkLoad(AsyncCacheLoader loader) {
    try {
      Class defaultLoaderClass = AsyncCacheLoader.class;
      if (loader instanceof CacheLoader) {
        defaultLoaderClass = CacheLoader.class;

        Method classLoadAll = loader.getClass().getMethod("loadAll", Set.class);
        Method defaultLoadAll = CacheLoader.class.getMethod("loadAll", Set.class);
        if (!classLoadAll.equals(defaultLoadAll)) {
          return true;
        }
      }

      Method classAsyncLoadAll = loader.getClass().getMethod(
          "asyncLoadAll", Set.class, Executor.class);
      Method defaultAsyncLoadAll = defaultLoaderClass.getMethod(
          "asyncLoadAll", Set.class, Executor.class);
      return !classAsyncLoadAll.equals(defaultAsyncLoadAll);
    } catch (NoSuchMethodException | SecurityException e) {
      logger.log(Level.WARNING, "Cannot determine if CacheLoader can bulk load", e);
      return false;
    }
  }

  @Override
  public CompletableFuture get(K key) {
    return get(key, mappingFunction);
  }

  @Override
  public CompletableFuture> getAll(Iterable keys) {
    if (bulkMappingFunction != null) {
      return getAll(keys, bulkMappingFunction);
    }

    Function> mappingFunction = this::get;
    var result = new LinkedHashMap>(calculateHashMapCapacity(keys));
    for (K key : keys) {
      var future = result.computeIfAbsent(key, mappingFunction);
      requireNonNull(future);
    }
    return composeResult(result);
  }

  @Override
  public LoadingCache synchronous() {
    return (cacheView == null) ? (cacheView = new LoadingCacheView<>(this)) : cacheView;
  }

  /* --------------- Synchronous views --------------- */

  static final class LoadingCacheView
      extends AbstractCacheView implements LoadingCache {
    private static final long serialVersionUID = 1L;

    @SuppressWarnings("serial")
    final LocalAsyncLoadingCache asyncCache;

    LoadingCacheView(LocalAsyncLoadingCache asyncCache) {
      this.asyncCache = requireNonNull(asyncCache);
    }

    @Override
    LocalAsyncLoadingCache asyncCache() {
      return asyncCache;
    }

    @Override
    @SuppressWarnings("PMD.PreserveStackTrace")
    public V get(K key) {
      return resolve(asyncCache.get(key));
    }

    @Override
    @SuppressWarnings("PMD.PreserveStackTrace")
    public Map getAll(Iterable keys) {
      return resolve(asyncCache.getAll(keys));
    }

    @Override
    public CompletableFuture refresh(K key) {
      requireNonNull(key);

      Object keyReference = asyncCache.cache().referenceKey(key);
      for (;;) {
        var future = tryOptimisticRefresh(key, keyReference);
        if (future == null) {
          future = tryComputeRefresh(key, keyReference);
        }
        if (future != null) {
          return future;
        }
      }
    }

    @Override
    public CompletableFuture> refreshAll(Iterable keys) {
      var result = new LinkedHashMap>(calculateHashMapCapacity(keys));
      for (K key : keys) {
        result.computeIfAbsent(key, this::refresh);
      }
      return composeResult(result);
    }

    /** Attempts to avoid a reload if the entry is absent, or a load or reload is in-flight. */
    @SuppressWarnings("FutureReturnValueIgnored")
    private @Nullable CompletableFuture tryOptimisticRefresh(K key, Object keyReference) {
      // If a refresh is in-flight, then return it directly. If completed and not yet removed, then
      // remove to trigger a new reload.
      @SuppressWarnings("unchecked")
      var lastRefresh = (CompletableFuture) asyncCache.cache().refreshes().get(keyReference);
      if (lastRefresh != null) {
        if (Async.isReady(lastRefresh) || asyncCache.cache().isPendingEviction(key)) {
          asyncCache.cache().refreshes().remove(keyReference, lastRefresh);
        } else {
          return lastRefresh;
        }
      }

      // If the entry is absent then perform a new load, else if in-flight then return it
      var oldValueFuture = asyncCache.cache().getIfPresentQuietly(key);
      if ((oldValueFuture == null)
          || (oldValueFuture.isDone() && oldValueFuture.isCompletedExceptionally())) {
        if (oldValueFuture != null) {
          asyncCache.cache().remove(key, oldValueFuture);
        }
        var future = asyncCache.get(key, asyncCache.mappingFunction, /* recordStats */ false);
        @SuppressWarnings("unchecked")
        var prior = (CompletableFuture) asyncCache.cache()
            .refreshes().putIfAbsent(keyReference, future);
        var result = (prior == null) ? future : prior;
        result.whenComplete((r, e) -> asyncCache.cache().refreshes().remove(keyReference, result));
        return result;
      } else if (!oldValueFuture.isDone()) {
        // no-op if load is pending
        return oldValueFuture;
      }

      // Fallback to the slow path, possibly retrying
      return null;
    }

    /** Begins a refresh if the entry has materialized and no reload is in-flight. */
    @SuppressWarnings("FutureReturnValueIgnored")
    private @Nullable CompletableFuture tryComputeRefresh(K key, Object keyReference) {
      long[] startTime = new long[1];
      boolean[] refreshed = new boolean[1];
      @SuppressWarnings({"rawtypes", "unchecked"})
      CompletableFuture[] oldValueFuture = new CompletableFuture[1];
      var future = asyncCache.cache().refreshes().computeIfAbsent(keyReference, k -> {
        oldValueFuture[0] = asyncCache.cache().getIfPresentQuietly(key);
        V oldValue = Async.getIfReady(oldValueFuture[0]);
        if (oldValue == null) {
          return null;
        }

        refreshed[0] = true;
        startTime[0] = asyncCache.cache().statsTicker().read();
        try {
          var reloadFuture = asyncCache.cacheLoader.asyncReload(
              key, oldValue, asyncCache.cache().executor());
          return requireNonNull(reloadFuture, "Null future");
        } catch (RuntimeException e) {
          throw e;
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw new CompletionException(e);
        } catch (Exception e) {
          throw new CompletionException(e);
        }
      });

      if (future == null) {
        // Retry the optimistic path
        return null;
      }

      @SuppressWarnings("unchecked")
      var castedFuture = (CompletableFuture) future;
      if (refreshed[0]) {
        castedFuture.whenComplete((newValue, error) -> {
          long loadTime = asyncCache.cache().statsTicker().read() - startTime[0];
          if (error != null) {
            if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
              logger.log(Level.WARNING, "Exception thrown during refresh", error);
            }
            asyncCache.cache().refreshes().remove(keyReference, castedFuture);
            asyncCache.cache().statsCounter().recordLoadFailure(loadTime);
            return;
          }

          boolean[] discard = new boolean[1];
          var value = asyncCache.cache().compute(key, (ignored, currentValue) -> {
            var successful = asyncCache.cache().refreshes().remove(keyReference, castedFuture);
            if (successful && (currentValue == oldValueFuture[0])) {
              if (currentValue == null) {
                // If the entry is absent then discard the refresh and maybe notifying the listener
                discard[0] = (newValue != null);
                return null;
              } else if ((currentValue == newValue) || (currentValue == castedFuture)) {
                // If the reloaded value is the same instance then no-op
                return currentValue;
              } else if (newValue == Async.getIfReady((CompletableFuture) currentValue)) {
                // If the completed futures hold the same value instance then no-op
                return currentValue;
              }
              return (newValue == null) ? null : castedFuture;
            }
            // Otherwise, a write invalidated the refresh so discard it and notify the listener
            discard[0] = true;
            return currentValue;
          }, asyncCache.cache().expiry(), /* recordLoad */ false, /* recordLoadFailure */ true);

          if (discard[0] && (newValue != null)) {
            var cause = (value == null) ? RemovalCause.EXPLICIT : RemovalCause.REPLACED;
            asyncCache.cache().notifyRemoval(key, castedFuture, cause);
          }
          if (newValue == null) {
            asyncCache.cache().statsCounter().recordLoadFailure(loadTime);
          } else {
            asyncCache.cache().statsCounter().recordLoadSuccess(loadTime);
          }
        });
      }
      return castedFuture;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy