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

software.amazon.awssdk.utils.cache.lru.LruCache Maven / Gradle / Ivy

There is a newer version: 2.29.16
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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.awssdk.utils.cache.lru;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.ThreadSafe;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.Validate;

/**
 * A thread-safe LRU (Least Recently Used) cache implementation that returns the value for a specified key,
 * retrieving it by either getting the stored value from the cache or using a supplied function to calculate that value
 * and add it to the cache.
 * 

* When the cache is full, a new value will push out the least recently used value. * When the cache is queried for an already stored value (cache hit), this value is moved to the back of the queue * before it's returned so that the order of most recently used to least recently used can be maintained. *

* The user can configure the maximum size of the cache, which is set to a default of 100. *

* Null values are accepted. */ @SdkProtectedApi @ThreadSafe public final class LruCache { private static final Logger log = Logger.loggerFor(LruCache.class); private static final int DEFAULT_SIZE = 100; private final Map> cache; private final Function valueSupplier; private final Object listLock = new Object(); private final int maxCacheSize; private CacheEntry leastRecentlyUsed = null; private CacheEntry mostRecentlyUsed = null; private LruCache(Builder b) { this.valueSupplier = b.supplier; Integer customSize = Validate.isPositiveOrNull(b.maxSize, "size"); this.maxCacheSize = customSize != null ? customSize : DEFAULT_SIZE; this.cache = new ConcurrentHashMap<>(); } /** * Get a value based on the key. If the value exists in the cache, it's returned, and it's position in the cache is updated. * Otherwise, the value is calculated based on the supplied function {@link Builder#builder(Function)}. */ public V get(K key) { while (true) { CacheEntry cachedEntry = cache.computeIfAbsent(key, this::newEntry); synchronized (listLock) { if (cachedEntry.evicted()) { continue; } moveToBackOfQueue(cachedEntry); return cachedEntry.value(); } } } private CacheEntry newEntry(K key) { V value = valueSupplier.apply(key); return new CacheEntry<>(key, value); } /** * Moves an entry to the back of the queue and sets it as the most recently used. If the entry is already the * most recently used, do nothing. *

* Summary of cache update: *

    *
  1. Detach the entry from its current place in the double linked list.
  2. *
  3. Add it to the back of the queue (most recently used)
  4. *
*/ private void moveToBackOfQueue(CacheEntry entry) { if (entry.equals(mostRecentlyUsed)) { return; } removeFromQueue(entry); addToQueue(entry); } /** * Detaches an entry from its neighbors in the cache. Remove the entry from its current place in the double linked list * by letting its previous neighbor point to its next neighbor, and vice versa, if those exist. *

* The least-recently-used and most-recently-used pointers are reset if needed. *

* Note: Detaching an entry does not delete it from the cache hash map. */ private void removeFromQueue(CacheEntry entry) { CacheEntry previousEntry = entry.previous(); if (previousEntry != null) { previousEntry.setNext(entry.next()); } CacheEntry nextEntry = entry.next(); if (nextEntry != null) { nextEntry.setPrevious(entry.previous()); } if (entry.equals(leastRecentlyUsed)) { leastRecentlyUsed = entry.previous(); } if (entry.equals(mostRecentlyUsed)) { mostRecentlyUsed = entry.next(); } } /** * Adds an entry to the queue as the most recently used, adjusts all pointers and triggers an evict * event if the cache is now full. */ private void addToQueue(CacheEntry entry) { if (mostRecentlyUsed != null) { mostRecentlyUsed.setPrevious(entry); entry.setNext(mostRecentlyUsed); } entry.setPrevious(null); mostRecentlyUsed = entry; if (leastRecentlyUsed == null) { leastRecentlyUsed = entry; } if (size() > maxCacheSize) { evict(); } } /** * Removes the least recently used entry from the cache, marks it as evicted and removes it from the queue. */ private void evict() { leastRecentlyUsed.isEvicted(true); closeEvictedResourcesIfPossible(leastRecentlyUsed.value); cache.remove(leastRecentlyUsed.key()); removeFromQueue(leastRecentlyUsed); } private void closeEvictedResourcesIfPossible(V value) { if (value instanceof AutoCloseable) { try { ((AutoCloseable) value).close(); } catch (Exception e) { log.warn(() -> "Attempted to close instance that was evicted by cache, but got exception: " + e.getMessage()); } } } public int size() { return cache.size(); } public static LruCache.Builder builder(Function supplier) { return new Builder<>(supplier); } public static LruCache.Builder builder() { return new Builder<>(null); } public static final class Builder { private final Function supplier; private Integer maxSize; private Builder(Function supplier) { this.supplier = supplier; } public Builder maxSize(Integer maxSize) { this.maxSize = maxSize; return this; } public LruCache build() { return new LruCache<>(this); } } private static final class CacheEntry { private final K key; private final V value; private boolean evicted = false; private CacheEntry previous; private CacheEntry next; private CacheEntry(K key, V value) { this.key = key; this.value = value; } K key() { return key; } V value() { return value; } boolean evicted() { return evicted; } void isEvicted(boolean evicted) { this.evicted = evicted; } CacheEntry next() { return next; } void setNext(CacheEntry next) { this.next = next; } CacheEntry previous() { return previous; } void setPrevious(CacheEntry previous) { this.previous = previous; } @Override @SuppressWarnings("unchecked") public boolean equals(Object o) { if (this == o) { return true; } if ((o == null) || getClass() != o.getClass()) { return false; } CacheEntry that = (CacheEntry) o; return Objects.equals(key, that.key) && Objects.equals(value, that.value); } @Override public int hashCode() { int result = key != null ? key.hashCode() : 0; result = 31 * result + (value != null ? value.hashCode() : 0); return result; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy