summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Daniel Colascione <dancol@google.com> 2020-03-04 12:19:20 -0800
committer Daniel Colascione <dancol@google.com> 2020-04-17 00:16:35 +0000
commit264abafff0ccbe8d310f4b3fd6f266406a7f0f49 (patch)
tree29d44ee3b44648790f8a614db72262aafa2d4ecb
parente025fcef40747e94f1d5e69a61a3e4f43c6abdf7 (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.java93
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);