• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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