1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.net.module.util; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.LruCache; 22 23 import com.android.internal.annotations.GuardedBy; 24 25 import java.util.Objects; 26 import java.util.function.LongSupplier; 27 import java.util.function.Predicate; 28 import java.util.function.Supplier; 29 30 /** 31 * A thread-safe LRU cache that stores key-value pairs with an expiry time. 32 * 33 * <p>This cache uses an {@link LruCache} to store entries and evicts the least 34 * recently used entries when the cache reaches its maximum capacity. It also 35 * supports an expiry time for each entry, allowing entries to be automatically 36 * removed from the cache after a certain duration. 37 * 38 * @param <K> The type of keys used to identify cached entries. 39 * @param <V> The type of values stored in the cache. 40 * 41 * @hide 42 */ 43 public class LruCacheWithExpiry<K, V> { 44 private final LongSupplier mTimeSupplier; 45 private final long mExpiryDurationMs; 46 @GuardedBy("mMap") 47 private final LruCache<K, CacheValue<V>> mMap; 48 private final Predicate<V> mShouldCacheValue; 49 50 /** 51 * Constructs a new {@link LruCacheWithExpiry} with the specified parameters. 52 * 53 * @param timeSupplier The {@link java.util.function.LongSupplier} to use for 54 * determining timestamps. 55 * @param expiryDurationMs The expiry duration for cached entries in milliseconds. 56 * @param maxSize The maximum number of entries to hold in the cache. 57 * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be 58 * cached. This can be used to filter out certain values from being 59 * stored in the cache. 60 */ LruCacheWithExpiry(@onNull LongSupplier timeSupplier, long expiryDurationMs, int maxSize, Predicate<V> shouldCacheValue)61 public LruCacheWithExpiry(@NonNull LongSupplier timeSupplier, long expiryDurationMs, 62 int maxSize, Predicate<V> shouldCacheValue) { 63 mTimeSupplier = timeSupplier; 64 mExpiryDurationMs = expiryDurationMs; 65 mMap = new LruCache<>(maxSize); 66 mShouldCacheValue = shouldCacheValue; 67 } 68 69 /** 70 * Retrieves a value from the cache, associated with the given key. 71 * 72 * @param key The key to look up in the cache. 73 * @return The cached value, or {@code null} if not found or expired. 74 */ 75 @Nullable get(@onNull K key)76 public V get(@NonNull K key) { 77 synchronized (mMap) { 78 final CacheValue<V> value = mMap.get(key); 79 if (value != null && !isExpired(value.timestamp)) { 80 return value.entry; 81 } else { 82 mMap.remove(key); // Remove expired entries 83 return null; 84 } 85 } 86 } 87 88 /** 89 * Retrieves a value from the cache, associated with the given key. 90 * If the entry is not found in the cache or has expired, computes it using the provided 91 * {@code supplier} and stores the result in the cache. 92 * 93 * @param key The key to look up in the cache. 94 * @param supplier The {@link Supplier} to compute the value if not found or expired. 95 * @return The cached or computed value, or {@code null} if the {@code supplier} returns null. 96 */ 97 @Nullable getOrCompute(@onNull K key, @NonNull Supplier<V> supplier)98 public V getOrCompute(@NonNull K key, @NonNull Supplier<V> supplier) { 99 synchronized (mMap) { 100 final V cachedValue = get(key); 101 if (cachedValue != null) { 102 return cachedValue; 103 } 104 105 // Entry not found or expired, compute it 106 final V computedValue = supplier.get(); 107 if (computedValue != null && mShouldCacheValue.test(computedValue)) { 108 put(key, computedValue); 109 } 110 return computedValue; 111 } 112 } 113 114 /** 115 * Stores a value in the cache, associated with the given key. 116 * 117 * @param key The key to associate with the value. 118 * @param value The value to store in the cache. 119 */ put(@onNull K key, @NonNull V value)120 public void put(@NonNull K key, @NonNull V value) { 121 Objects.requireNonNull(value); 122 synchronized (mMap) { 123 mMap.put(key, new CacheValue<>(mTimeSupplier.getAsLong(), value)); 124 } 125 } 126 127 /** 128 * Stores a value in the cache if absent, associated with the given key. 129 * 130 * @param key The key to associate with the value. 131 * @param value The value to store in the cache. 132 * @return The existing value associated with the key, if present; otherwise, null. 133 */ 134 @Nullable putIfAbsent(@onNull K key, @NonNull V value)135 public V putIfAbsent(@NonNull K key, @NonNull V value) { 136 Objects.requireNonNull(value); 137 synchronized (mMap) { 138 final V existingValue = get(key); 139 if (existingValue == null) { 140 put(key, value); 141 } 142 return existingValue; 143 } 144 } 145 146 /** 147 * Clear the cache. 148 */ clear()149 public void clear() { 150 synchronized (mMap) { 151 mMap.evictAll(); 152 } 153 } 154 isExpired(long timestamp)155 private boolean isExpired(long timestamp) { 156 return mTimeSupplier.getAsLong() > timestamp + mExpiryDurationMs; 157 } 158 159 private static class CacheValue<V> { 160 public final long timestamp; 161 @NonNull 162 public final V entry; 163 CacheValue(long timestamp, V entry)164 CacheValue(long timestamp, V entry) { 165 this.timestamp = timestamp; 166 this.entry = entry; 167 } 168 } 169 } 170