Add LRU cache with expiry class
This is a no-op refactoring which extracts an
LruCacheWithExpiry<K, V> superclass from
TrafficStatsRateLimitCache to provide an LRU
cache with an adjustable expiry duration and generic types,
allowing entries to be automatically removed from the cache
after a certain duration.
This is needed for follow-up changes to provide similar
functionality with other types without manually handling expiry.
Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.net.TrafficStatsRateLimitCacheTest
Bug: 343260158
Change-Id: Ia413b8c6e73c376dd821cd2bb676ef996cd85a91
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index ca97d07..667aad1 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,9 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkStats;
-import android.util.LruCache;
-import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.LruCacheWithExpiry;
import java.time.Clock;
import java.util.Objects;
@@ -31,9 +30,8 @@
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
* with an adjustable expiry duration to manage data freshness.
*/
-class TrafficStatsRateLimitCache {
- private final Clock mClock;
- private final long mExpiryDurationMs;
+class TrafficStatsRateLimitCache extends
+ LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
/**
* Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
@@ -43,19 +41,17 @@
* @param maxSize Maximum number of entries.
*/
TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
- mClock = clock;
- mExpiryDurationMs = expiryDurationMs;
- mMap = new LruCache<>(maxSize);
+ super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
}
- private static class TrafficStatsCacheKey {
+ public static class TrafficStatsCacheKey {
@Nullable
- public final String iface;
- public final int uid;
+ private final String mIface;
+ private final int mUid;
TrafficStatsCacheKey(@Nullable String iface, int uid) {
- this.iface = iface;
- this.uid = uid;
+ this.mIface = iface;
+ this.mUid = uid;
}
@Override
@@ -63,29 +59,15 @@
if (this == o) return true;
if (!(o instanceof TrafficStatsCacheKey)) return false;
TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
- return uid == that.uid && Objects.equals(iface, that.iface);
+ return mUid == that.mUid && Objects.equals(mIface, that.mIface);
}
@Override
public int hashCode() {
- return Objects.hash(iface, uid);
+ return Objects.hash(mIface, mUid);
}
}
- private static class TrafficStatsCacheValue {
- public final long timestamp;
- @NonNull
- public final NetworkStats.Entry entry;
-
- TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
- this.timestamp = timestamp;
- this.entry = entry;
- }
- }
-
- @GuardedBy("mMap")
- private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
-
/**
* Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
*
@@ -95,16 +77,7 @@
*/
@Nullable
NetworkStats.Entry get(String iface, int uid) {
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- final TrafficStatsCacheValue value = mMap.get(key);
- if (value != null && !isExpired(value.timestamp)) {
- return value.entry;
- } else {
- mMap.remove(key); // Remove expired entries
- return null;
- }
- }
+ return super.get(new TrafficStatsCacheKey(iface, uid));
}
/**
@@ -122,19 +95,7 @@
@Nullable
NetworkStats.Entry getOrCompute(String iface, int uid,
@NonNull Supplier<NetworkStats.Entry> supplier) {
- synchronized (mMap) {
- final NetworkStats.Entry cachedValue = get(iface, uid);
- if (cachedValue != null) {
- return cachedValue;
- }
-
- // Entry not found or expired, compute it
- final NetworkStats.Entry computedEntry = supplier.get();
- if (computedEntry != null && !computedEntry.isEmpty()) {
- put(iface, uid, computedEntry);
- }
- return computedEntry;
- }
+ return super.getOrCompute(new TrafficStatsCacheKey(iface, uid), supplier);
}
/**
@@ -145,23 +106,7 @@
* @param entry The {@link NetworkStats.Entry} to store in the cache.
*/
void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
- Objects.requireNonNull(entry);
- final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
- synchronized (mMap) { // Synchronize for thread-safety
- mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
- }
+ super.put(new TrafficStatsCacheKey(iface, uid), entry);
}
- /**
- * Clear the cache.
- */
- void clear() {
- synchronized (mMap) {
- mMap.evictAll();
- }
- }
-
- private boolean isExpired(long timestamp) {
- return mClock.millis() > timestamp + mExpiryDurationMs;
- }
}
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
new file mode 100644
index 0000000..80088b9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * An LRU cache that stores key-value pairs with an expiry time.
+ *
+ * <p>This cache uses an {@link LruCache} to store entries and evicts the least
+ * recently used entries when the cache reaches its maximum capacity. It also
+ * supports an expiry time for each entry, allowing entries to be automatically
+ * removed from the cache after a certain duration.
+ *
+ * @param <K> The type of keys used to identify cached entries.
+ * @param <V> The type of values stored in the cache.
+ *
+ * @hide
+ */
+public class LruCacheWithExpiry<K, V> {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+ @GuardedBy("mMap")
+ private final LruCache<K, CacheValue<V>> mMap;
+ private final Predicate<V> mShouldCacheValue;
+
+ /**
+ * Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration for cached entries in milliseconds.
+ * @param maxSize The maximum number of entries to hold in the cache.
+ * @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
+ * cached. This can be used to filter out certain values from being
+ * stored in the cache.
+ */
+ public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
+ Predicate<V> shouldCacheValue) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ mMap = new LruCache<>(maxSize);
+ mShouldCacheValue = shouldCacheValue;
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ *
+ * @param key The key to look up in the cache.
+ * @return The cached value, or {@code null} if not found or expired.
+ */
+ @Nullable
+ public V get(@NonNull K key) {
+ synchronized (mMap) {
+ final CacheValue<V> value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Retrieves a value from the cache, associated with the given key.
+ * If the entry is not found in the cache or has expired, computes it using the provided
+ * {@code supplier} and stores the result in the cache.
+ *
+ * @param key The key to look up in the cache.
+ * @param supplier The {@link Supplier} to compute the value if not found or expired.
+ * @return The cached or computed value, or {@code null} if the {@code supplier} returns null.
+ */
+ @Nullable
+ public V getOrCompute(@NonNull K key, @NonNull Supplier<V> supplier) {
+ synchronized (mMap) {
+ final V cachedValue = get(key);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ // Entry not found or expired, compute it
+ final V computedValue = supplier.get();
+ if (computedValue != null && mShouldCacheValue.test(computedValue)) {
+ put(key, computedValue);
+ }
+ return computedValue;
+ }
+ }
+
+ /**
+ * Stores a value in the cache, associated with the given key.
+ *
+ * @param key The key to associate with the value.
+ * @param value The value to store in the cache.
+ */
+ public void put(@NonNull K key, @NonNull V value) {
+ Objects.requireNonNull(value);
+ synchronized (mMap) {
+ mMap.put(key, new CacheValue<>(mClock.millis(), value));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ public void clear() {
+ synchronized (mMap) {
+ mMap.evictAll();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+
+ private static class CacheValue<V> {
+ public final long timestamp;
+ @NonNull
+ public final V entry;
+
+ CacheValue(long timestamp, V entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+}