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

software.amazon.jdbc.util.SlidingExpirationCache Maven / Gradle / Ivy

There is a newer version: 2.5.2
Show newest version
/*
 * Copyright Amazon.com, Inc. or its affiliates. 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 software.amazon.jdbc.util;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

public class SlidingExpirationCache {

  protected final Map cache = new ConcurrentHashMap<>();
  protected long cleanupIntervalNanos = TimeUnit.MINUTES.toNanos(10);
  protected final AtomicLong cleanupTimeNanos = new AtomicLong(System.nanoTime() + cleanupIntervalNanos);
  protected final ShouldDisposeFunc shouldDisposeFunc;
  protected final ItemDisposalFunc itemDisposalFunc;

  /**
   * A cache that periodically cleans up expired entries. Fetching an expired entry marks that entry
   * as not-expired and renews its expiration time.
   */
  public SlidingExpirationCache() {
    this.shouldDisposeFunc = null;
    this.itemDisposalFunc = null;
  }

  /**
   * A cache that periodically cleans up expired entries. Fetching an expired entry marks that entry
   * as not-expired and renews its expiration time.
   *
   * @param shouldDisposeFunc a function defining the conditions under which an expired entry should
   *                          be cleaned up when we hit the cleanup time
   * @param itemDisposalFunc  a function that will be called on any item that meets the cleanup
   *                          criteria at cleanup time. The criteria for cleanup is that the item
   *                          is both expired and marked for cleanup via a call to
   *                          shouldDisposeFunc.
   */
  public SlidingExpirationCache(
      final ShouldDisposeFunc shouldDisposeFunc,
      final ItemDisposalFunc itemDisposalFunc) {
    this.shouldDisposeFunc = shouldDisposeFunc;
    this.itemDisposalFunc = itemDisposalFunc;
  }

  public SlidingExpirationCache(
      final ShouldDisposeFunc shouldDisposeFunc,
      final ItemDisposalFunc itemDisposalFunc,
      final long cleanupIntervalNanos) {
    this.shouldDisposeFunc = shouldDisposeFunc;
    this.itemDisposalFunc = itemDisposalFunc;
    this.cleanupIntervalNanos = cleanupIntervalNanos;
  }

  /**
   * In addition to performing the logic defined by {@link Map#computeIfAbsent}, cleans up expired
   * entries if we have hit cleanup time. If an expired entry is requested and we have not hit
   * cleanup time or {@link ShouldDisposeFunc} indicated the entry should not be closed, the entry
   * will be marked as non-expired.
   *
   * @param key                the key with which the specified value is to be associated
   * @param mappingFunction    the function to compute a value
   * @param itemExpirationNano the expiration time of the new or renewed entry
   * @return the current (existing or computed) value associated with the specified key, or null if
   *     the computed value is null
   */
  public V computeIfAbsent(
      final K key,
      Function mappingFunction,
      final long itemExpirationNano) {

    cleanUp();
    final CacheItem cacheItem = cache.computeIfAbsent(
        key,
        k -> new CacheItem(
            mappingFunction.apply(k),
            System.nanoTime() + itemExpirationNano));
    return cacheItem.withExtendExpiration(itemExpirationNano).item;
  }

  public V get(final K key, final long itemExpirationNano) {
    cleanUp();
    final CacheItem cacheItem = cache.get(key);
    return cacheItem == null ? null : cacheItem.withExtendExpiration(itemExpirationNano).item;
  }

  /**
   * Cleanup expired entries if we have hit the cleanup time, then remove and dispose the value
   * associated with the given key.
   *
   * @param key the key associated with the value to be removed/disposed
   */
  public void remove(final K key) {
    removeAndDispose(key);
    cleanUp();
  }

  protected void removeAndDispose(K key) {
    final CacheItem cacheItem = cache.remove(key);
    if (cacheItem != null && itemDisposalFunc != null) {
      itemDisposalFunc.dispose(cacheItem.item);
    }
  }

  protected void removeIfExpired(K key) {
    final CacheItem cacheItem = cache.get(key);
    if (cacheItem == null || cacheItem.shouldCleanup()) {
      removeAndDispose(key);
    }
  }

  /**
   * Remove and dispose of all entries in the cache.
   */
  public void clear() {
    for (K key : cache.keySet()) {
      removeAndDispose(key);
    }
    cache.clear();
  }

  /**
   * Get a map copy of all entries in the cache, including expired entries.
   *
   * @return a map copy of all entries in the cache, including expired entries
   */
  public Map getEntries() {
    final Map entries = new HashMap<>();
    for (final Map.Entry entry : this.cache.entrySet()) {
      entries.put(entry.getKey(), entry.getValue().item);
    }
    return entries;
  }

  /**
   * Get the current size of the cache, including expired entries.
   *
   * @return the current size of the cache, including expired entries.
   */
  public int size() {
    return this.cache.size();
  }

  protected void cleanUp() {
    if (this.cleanupTimeNanos.get() > System.nanoTime()) {
      return;
    }

    this.cleanupTimeNanos.set(System.nanoTime() + cleanupIntervalNanos);
    cache.forEach((key, value) -> removeIfExpired(key));
  }

  /**
   * Set the cleanup interval for the cache. At cleanup time, expired entries marked for cleanup via
   * {@link ShouldDisposeFunc} (if defined) are disposed.
   *
   * @param cleanupIntervalNanos the time interval defining when we should clean up expired
   *                             entries marked for cleanup, in nanoseconds
   */
  public void setCleanupIntervalNanos(long cleanupIntervalNanos) {
    this.cleanupIntervalNanos = cleanupIntervalNanos;
    this.cleanupTimeNanos.set(System.nanoTime() + cleanupIntervalNanos);
  }

  /**
   * An optional function defining the conditions under which an expired entry should be cleaned up
   * at cleanup time.
   *
   * @param  the type of object being analyzed for disposal
   */
  public interface ShouldDisposeFunc {
    boolean shouldDispose(V item);
  }

  /**
   * An optional function defining extra cleanup steps to take when a cache item is cleaned up.
   *
   * @param  the type of object being disposed
   */
  public interface ItemDisposalFunc {
    void dispose(V item);
  }

  // For testing purposes only
  Map getCache() {
    return cache;
  }

  class CacheItem {
    private final V item;
    private long expirationTimeNano;

    /**
     * CacheItem constructor.
     *
     * @param item               the item value
     * @param expirationTimeNano the amount of time before a CacheItem should be marked as expired.
     */
    public CacheItem(final V item, final long expirationTimeNano) {
      this.item = item;
      this.expirationTimeNano = expirationTimeNano;
    }

    /**
     * Determines if a cache item should be cleaned up. An item should be cleaned up if it has past
     * its expiration time and {@link ShouldDisposeFunc} (if defined) indicates that it should be
     * cleaned up.
     *
     * @return true if the cache item should be cleaned up at cleanup time. Otherwise, returns
     *     false.
     */
    boolean shouldCleanup() {
      if (shouldDisposeFunc != null) {
        return System.nanoTime() > expirationTimeNano && shouldDisposeFunc.shouldDispose(this.item);
      }
      return System.nanoTime() > expirationTimeNano;
    }

    /**
     * Renew a cache item's expiration time and return the value.
     *
     * @param itemExpirationNano the new expiration duration for the item
     * @return the item value
     */
    public CacheItem withExtendExpiration(final long itemExpirationNano) {
      this.expirationTimeNano = System.nanoTime() + itemExpirationNano;
      return this;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((item == null) ? 0 : item.hashCode());
      return result;
    }

    @Override
    public boolean equals(final Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      final CacheItem other = (CacheItem) obj;
      if (item == null) {
        return other.item == null;
      } else {
        return item.equals(other.item);
      }
    }

    @Override
    public String toString() {
      return "CacheItem [item=" + item + ", expirationTime=" + expirationTimeNano + "]";
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy