diff options
| author | 2020-03-04 12:19:20 -0800 | |
|---|---|---|
| committer | 2020-04-17 00:16:35 +0000 | |
| commit | 264abafff0ccbe8d310f4b3fd6f266406a7f0f49 (patch) | |
| tree | 29d44ee3b44648790f8a614db72262aafa2d4ecb | |
| parent | e025fcef40747e94f1d5e69a61a3e4f43c6abdf7 (diff) | |
RESTRICT AUTOMERGE: Add a "cork" mechanism to prevent cache invalidation flooding
Bug: 140788621
Test: subsequent CL
Change-Id: Idfc42110e655571578bae208b98ee61a6eb1b2c3
| -rw-r--r-- | core/java/android/app/PropertyInvalidatedCache.java | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index ce0d04b9d145..a24a5b7823b6 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -22,6 +22,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -168,6 +169,17 @@ public abstract class PropertyInvalidatedCache<Query, Result> { private static final boolean ENABLE = true; private static final boolean VERIFY = false; + private static final Object sCorkLock = new Object(); + + /** + * A map of cache keys that we've "corked". (The values are counts.) When a cache key is + * corked, we skip the cache invalidate when the cache key is in the unset state --- that + * is, when a cache key is corked, an invalidation does not enable the cache if somebody + * else hasn't disabled it. + */ + @GuardedBy("sCorkLock") + private static final HashMap<String, Integer> sCorks = new HashMap<>(); + private final Object mLock = new Object(); /** @@ -421,6 +433,25 @@ public abstract class PropertyInvalidatedCache<Query, Result> { * @param name Name of the cache-key property to invalidate */ public static void invalidateCache(@NonNull String name) { + // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't + // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork. + // The property service is single-threaded anyway, so we don't lose any concurrency by + // taking the cork lock around cache invalidations. If we see contention on this lock, + // we're invalidating too often. + synchronized (sCorkLock) { + Integer numberCorks = sCorks.get(name); + if (numberCorks != null && numberCorks > 0) { + if (DEBUG) { + Log.d(TAG, "ignoring invalidation due to cork: " + name); + } + return; + } + invalidateCacheLocked(name); + } + } + + @GuardedBy("sCorkLock") + private static void invalidateCacheLocked(@NonNull String name) { // There's no race here: we don't require that values strictly increase, but instead // only that each is unique in a single runtime-restart session. final long nonce = SystemProperties.getLong(name, NONCE_UNSET); @@ -430,6 +461,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { } return; } + long newValue; do { newValue = NoPreloadHolder.next(); @@ -445,6 +477,67 @@ public abstract class PropertyInvalidatedCache<Query, Result> { SystemProperties.set(name, newValueString); } + /** + * Temporarily put the cache in the uninitialized state and prevent invalidations from + * moving it out of that state: useful in cases where we want to avoid the overhead of a + * large number of cache invalidations in a short time. While the cache is corked, clients + * bypass the cache and talk to backing services directly. This property makes corking + * correctness-preserving even if corked outside the lock that controls access to the + * cache's backing service. + * + * corkInvalidations() and uncorkInvalidations() must be called in pairs. + * + * @param name Name of the cache-key property to cork + */ + public static void corkInvalidations(@NonNull String name) { + synchronized (sCorkLock) { + int numberCorks = sCorks.getOrDefault(name, 0); + // If we're the first ones to cork this cache, set the cache to the unset state so + // existing caches talk directly to their services while we've corked updates. + // Make sure we don't clobber a disabled cache value. + + // TODO(dancol): we can skip this property write and leave the cache enabled if the + // caller promises not to make observable changes to the cache backing state before + // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair. + // Implement this more dangerous mode of operation if necessary. + if (numberCorks == 0) { + final long nonce = SystemProperties.getLong(name, NONCE_UNSET); + if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { + SystemProperties.set(name, Long.toString(NONCE_UNSET)); + } + } + sCorks.put(name, numberCorks + 1); + if (DEBUG) { + Log.d(TAG, "corked: " + name); + } + } + } + + /** + * Undo the effect of a cork, allowing cache invalidations to proceed normally. + * Removing the last cork on a cache name invalidates the cache by side effect, + * transitioning it to normal operation (unless explicitly disabled system-wide). + * + * @param name Name of the cache-key property to uncork + */ + public static void uncorkInvalidations(@NonNull String name) { + synchronized (sCorkLock) { + int numberCorks = sCorks.getOrDefault(name, 0); + if (numberCorks < 1) { + throw new AssertionError("cork underflow: " + name); + } + if (numberCorks == 1) { + sCorks.remove(name); + invalidateCacheLocked(name); + if (DEBUG) { + Log.d(TAG, "uncorked: " + name); + } + } else { + sCorks.put(name, numberCorks - 1); + } + } + } + protected Result maybeCheckConsistency(Query query, Result proposedResult) { if (VERIFY) { Result resultToCompare = recompute(query); |