Create os/IpcDataCache
Bug: 219609105 Create os/IpcDataCache as a subclass of PropertyInvalidatedCache. The APIs in the new class are the SystemApi for mainline modules. Note that IpcDataCache.invalidateCache(String) is a new system API, relative to the list in PropertyInvalidateCache. The new API is static, which is the most common use case for invalidateCache calls. The corresponding CTS test is updated in the next review. However, it is also cloned into the current unit test. Test: * atest FrameworksCoreTests:PropertyInvalidatedCacheTests * atest FrameworksCoreTests:IpcDataCacheTest Change-Id: I83ce97a4154b9065e0686ee99a25c90f955366ff
This commit is contained in:
@@ -54,22 +54,6 @@ package android.app {
|
||||
method public void onCanceled(@NonNull android.app.PendingIntent);
|
||||
}
|
||||
|
||||
public class PropertyInvalidatedCache<Query, Result> {
|
||||
ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>);
|
||||
method public final void disableForCurrentProcess();
|
||||
method public final void invalidateCache();
|
||||
method public static void invalidateCache(@NonNull String, @NonNull String);
|
||||
method @Nullable public final Result query(@NonNull Query);
|
||||
field public static final String MODULE_BLUETOOTH = "bluetooth";
|
||||
field public static final String MODULE_TELEPHONY = "telephony";
|
||||
}
|
||||
|
||||
public abstract static class PropertyInvalidatedCache.QueryHandler<Q, R> {
|
||||
ctor public PropertyInvalidatedCache.QueryHandler();
|
||||
method @Nullable public abstract R apply(@NonNull Q);
|
||||
method public boolean shouldBypassCache(@NonNull Q);
|
||||
}
|
||||
|
||||
public class StatusBarManager {
|
||||
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean);
|
||||
}
|
||||
@@ -358,6 +342,22 @@ package android.os {
|
||||
field public static final int DEVICE_INITIAL_SDK_INT;
|
||||
}
|
||||
|
||||
public class IpcDataCache<Query, Result> {
|
||||
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
|
||||
method public void disableForCurrentProcess();
|
||||
method public static void disableForCurrentProcess(@NonNull String);
|
||||
method public void invalidateCache();
|
||||
method public static void invalidateCache(@NonNull String, @NonNull String);
|
||||
method @Nullable public Result query(@NonNull Query);
|
||||
field public static final String MODULE_BLUETOOTH = "bluetooth";
|
||||
}
|
||||
|
||||
public abstract static class IpcDataCache.QueryHandler<Q, R> {
|
||||
ctor public IpcDataCache.QueryHandler();
|
||||
method @Nullable public abstract R apply(@NonNull Q);
|
||||
method public boolean shouldBypassCache(@NonNull Q);
|
||||
}
|
||||
|
||||
public interface Parcelable {
|
||||
method public default int getStability();
|
||||
}
|
||||
|
||||
@@ -374,16 +374,17 @@ package android.app {
|
||||
public class PropertyInvalidatedCache<Query, Result> {
|
||||
ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>);
|
||||
method @NonNull public static String createPropertyName(@NonNull String, @NonNull String);
|
||||
method public final void disableForCurrentProcess();
|
||||
method public void disableForCurrentProcess();
|
||||
method public static void disableForCurrentProcess(@NonNull String);
|
||||
method public static void disableForTestMode();
|
||||
method public final void disableInstance();
|
||||
method public final void disableSystemWide();
|
||||
method public final void forgetDisableLocal();
|
||||
method public boolean getDisabledState();
|
||||
method public final void invalidateCache();
|
||||
method public void invalidateCache();
|
||||
method public static void invalidateCache(@NonNull String, @NonNull String);
|
||||
method public final boolean isDisabled();
|
||||
method @Nullable public final Result query(@NonNull Query);
|
||||
method @Nullable public Result query(@NonNull Query);
|
||||
method public static void setTestMode(boolean);
|
||||
method public void testPropertyName();
|
||||
field public static final String MODULE_BLUETOOTH = "bluetooth";
|
||||
@@ -1726,6 +1727,19 @@ package android.os {
|
||||
method @NonNull public static byte[] digest(@NonNull java.io.InputStream, @NonNull String) throws java.io.IOException, java.security.NoSuchAlgorithmException;
|
||||
}
|
||||
|
||||
public class IpcDataCache<Query, Result> extends android.app.PropertyInvalidatedCache<Query,Result> {
|
||||
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
|
||||
method public static void disableForCurrentProcess(@NonNull String);
|
||||
method public static void invalidateCache(@NonNull String, @NonNull String);
|
||||
field public static final String MODULE_BLUETOOTH = "bluetooth";
|
||||
field public static final String MODULE_SYSTEM = "system_server";
|
||||
field public static final String MODULE_TEST = "test";
|
||||
}
|
||||
|
||||
public abstract static class IpcDataCache.QueryHandler<Q, R> extends android.app.PropertyInvalidatedCache.QueryHandler<Q,R> {
|
||||
ctor public IpcDataCache.QueryHandler();
|
||||
}
|
||||
|
||||
public final class MessageQueue {
|
||||
method public int postSyncBarrier();
|
||||
method public void removeSyncBarrier(int);
|
||||
|
||||
@@ -18,7 +18,6 @@ package android.app;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.TestApi;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -137,6 +136,26 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
|
||||
* for the first time; on subsequent queries, we return the already-known Birthday object.
|
||||
*
|
||||
* The second parameter to the IpcDataCache constructor is a string that identifies the "module"
|
||||
* that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
|
||||
* string is permitted. The third parameters is the name of the API being cached; this, too, can
|
||||
* any value. The fourth is the name of the cache. The cache is usually named after th API.
|
||||
* Some things you must know about the three strings:
|
||||
* <list>
|
||||
* <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
|
||||
* Usually, the SELinux rules permit a process to write a system property (and therefore
|
||||
* invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
|
||||
* although the cache can be constructed with any module string, whatever string is chosen must be
|
||||
* consistent with the SELinux configuration.
|
||||
* <ul> The API name can be any string of alphanumeric characters. All caches with the same API
|
||||
* are invalidated at the same time. If a server supports several caches and all are invalidated
|
||||
* in common, then it is most efficient to assign the same API string to every cache.
|
||||
* <ul> The cache name can be any string. In debug output, the name is used to distiguish between
|
||||
* caches with the same API name. The cache name is also used when disabling caches in the
|
||||
* current process. So, invalidation is based on the module+api but disabling (which is generally
|
||||
* a once-per-process operation) is based on the cache name.
|
||||
* </list>
|
||||
*
|
||||
* User birthdays do occasionally change, so we have to modify the server to invalidate this
|
||||
* cache when necessary. That invalidation code looks like this:
|
||||
*
|
||||
@@ -192,25 +211,23 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* <pre>
|
||||
* public class ActivityThread {
|
||||
* ...
|
||||
* private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
|
||||
* private static final String BDAY_CACHE_KEY = "cache_key.birthdayd";
|
||||
* private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new
|
||||
* PropertyInvalidatedCache<Integer, Birthday%>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) {
|
||||
* {@literal @}Override
|
||||
* protected Birthday recompute(Integer userId) {
|
||||
* return GetService("birthdayd").getUserBirthday(userId);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* protected boolean bypass(Integer userId) {
|
||||
* return userId == NEXT_BIRTHDAY;
|
||||
* }
|
||||
* };
|
||||
* private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery =
|
||||
* new IpcDataCache.QueryHandler<Integer, Birthday>() {
|
||||
* {@literal @}Override
|
||||
* public Birthday apply(Integer) {
|
||||
* return GetService("birthdayd").getUserBirthday(userId);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean shouldBypassQuery(Integer userId) {
|
||||
* return userId == NEXT_BIRTHDAY;
|
||||
* }
|
||||
* };
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* If the {@code bypass()} method returns true then the cache is not used for that
|
||||
* particular query. The {@code bypass()} method is not abstract and the default
|
||||
* If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
|
||||
* particular query. The {@code shouldBypassQuery()} method is not abstract and the default
|
||||
* implementation returns false.
|
||||
*
|
||||
* For security, there is a allowlist of processes that are allowed to invalidate a cache.
|
||||
@@ -231,14 +248,12 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* @param <Result> The class holding cache entries; use a boxed primitive if possible
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public class PropertyInvalidatedCache<Query, Result> {
|
||||
/**
|
||||
* This is a configuration class that customizes a cache instance.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static abstract class QueryHandler<Q,R> {
|
||||
/**
|
||||
@@ -285,7 +300,6 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* The module used for bluetooth caches.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static final String MODULE_BLUETOOTH = "bluetooth";
|
||||
|
||||
@@ -533,7 +547,6 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* @param computer The code to compute values that are not in the cache.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api,
|
||||
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
|
||||
@@ -792,7 +805,7 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* TODO(216112648) Remove this in favor of disableForCurrentProcess().
|
||||
* @hide
|
||||
*/
|
||||
public final void disableLocal() {
|
||||
public void disableLocal() {
|
||||
disableForCurrentProcess();
|
||||
}
|
||||
|
||||
@@ -802,12 +815,17 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* property.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public final void disableForCurrentProcess() {
|
||||
public void disableForCurrentProcess() {
|
||||
disableLocal(mCacheName);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@TestApi
|
||||
public static void disableForCurrentProcess(@NonNull String cacheName) {
|
||||
disableLocal(cacheName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a cache instance is disabled.
|
||||
* @hide
|
||||
@@ -821,9 +839,8 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* Get a value from the cache or recompute it.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public final @Nullable Result query(@NonNull Query query) {
|
||||
public @Nullable Result query(@NonNull Query query) {
|
||||
// Let access to mDisabled race: it's atomic anyway.
|
||||
long currentNonce = (!isDisabled()) ? getCurrentNonce() : NONCE_DISABLED;
|
||||
if (bypass(query)) {
|
||||
@@ -964,9 +981,8 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* PropertyInvalidatedCache is keyed on a particular property value.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public final void invalidateCache() {
|
||||
public void invalidateCache() {
|
||||
invalidateCache(mPropertyName);
|
||||
}
|
||||
|
||||
@@ -974,7 +990,6 @@ public class PropertyInvalidatedCache<Query, Result> {
|
||||
* Invalidate caches in all processes that are keyed for the module and api.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static void invalidateCache(@NonNull String module, @NonNull String api) {
|
||||
invalidateCache(createPropertyName(module, api));
|
||||
|
||||
364
core/java/android/os/IpcDataCache.java
Normal file
364
core/java/android/os/IpcDataCache.java
Normal file
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 android.os;
|
||||
|
||||
import android.app.PropertyInvalidatedCache;
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.TestApi;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.util.FastPrintWriter;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
|
||||
* but doesn't hold a lock across data fetches on query misses.
|
||||
*
|
||||
* The intended use case is caching frequently-read, seldom-changed information normally retrieved
|
||||
* across interprocess communication. Imagine that you've written a user birthday information
|
||||
* daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over
|
||||
* binder. That binder interface looks something like this:
|
||||
*
|
||||
* <pre>
|
||||
* parcelable Birthday {
|
||||
* int month;
|
||||
* int day;
|
||||
* }
|
||||
* interface IUserBirthdayService {
|
||||
* Birthday getUserBirthday(int userId);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Suppose the service implementation itself looks like this...
|
||||
*
|
||||
* <pre>
|
||||
* public class UserBirthdayServiceImpl implements IUserBirthdayService {
|
||||
* private final HashMap<Integer, Birthday%> mUidToBirthday;
|
||||
* {@literal @}Override
|
||||
* public synchronized Birthday getUserBirthday(int userId) {
|
||||
* return mUidToBirthday.get(userId);
|
||||
* }
|
||||
* private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) {
|
||||
* mUidToBirthday.clear();
|
||||
* mUidToBirthday.putAll(uidToBirthday);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* ... and we have a client in frameworks (loaded into every app process) that looks like this:
|
||||
*
|
||||
* <pre>
|
||||
* public class ActivityThread {
|
||||
* ...
|
||||
* public Birthday getUserBirthday(int userId) {
|
||||
* return GetService("birthdayd").getUserBirthday(userId);
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call to
|
||||
* the birthdayd process and consult its database of birthdays. If we query user birthdays
|
||||
* frequently, we do a lot of work that we don't have to do, since user birthdays change
|
||||
* infrequently.
|
||||
*
|
||||
* IpcDataCache is part of a pattern for optimizing this kind of information-querying code. Using
|
||||
* {@code IpcDataCache}, you'd write the client this way:
|
||||
*
|
||||
* <pre>
|
||||
* public class ActivityThread {
|
||||
* ...
|
||||
* private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery =
|
||||
* new IpcDataCache.QueryHandler<Integer, Birthday>() {
|
||||
* {@literal @}Override
|
||||
* public Birthday apply(Integer) {
|
||||
* return GetService("birthdayd").getUserBirthday(userId);
|
||||
* }
|
||||
* };
|
||||
* private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
|
||||
* private static final String BDAY_API = "getUserBirthday";
|
||||
* private final IpcDataCache<Integer, Birthday%> mBirthdayCache = new
|
||||
* IpcDataCache<Integer, Birthday%>(
|
||||
* BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
|
||||
*
|
||||
* public void disableUserBirthdayCache() {
|
||||
* mBirthdayCache.disableForCurrentProcess();
|
||||
* }
|
||||
* public void invalidateUserBirthdayCache() {
|
||||
* mBirthdayCache.invalidateCache();
|
||||
* }
|
||||
* public Birthday getUserBirthday(int userId) {
|
||||
* return mBirthdayCache.query(userId);
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
|
||||
* for the first time; on subsequent queries, we return the already-known Birthday object.
|
||||
*
|
||||
* The second parameter to the IpcDataCache constructor is a string that identifies the "module"
|
||||
* that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
|
||||
* string is permitted. The third parameters is the name of the API being cached; this, too, can
|
||||
* any value. The fourth is the name of the cache. The cache is usually named after th API.
|
||||
* Some things you must know about the three strings:
|
||||
* <list>
|
||||
* <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
|
||||
* Usually, the SELinux rules permit a process to write a system property (and therefore
|
||||
* invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
|
||||
* although the cache can be constructed with any module string, whatever string is chosen must be
|
||||
* consistent with the SELinux configuration.
|
||||
* <ul> The API name can be any string of alphanumeric characters. All caches with the same API
|
||||
* are invalidated at the same time. If a server supports several caches and all are invalidated
|
||||
* in common, then it is most efficient to assign the same API string to every cache.
|
||||
* <ul> The cache name can be any string. In debug output, the name is used to distiguish between
|
||||
* caches with the same API name. The cache name is also used when disabling caches in the
|
||||
* current process. So, invalidation is based on the module+api but disabling (which is generally
|
||||
* a once-per-process operation) is based on the cache name.
|
||||
* </list>
|
||||
*
|
||||
* User birthdays do occasionally change, so we have to modify the server to invalidate this
|
||||
* cache when necessary. That invalidation code looks like this:
|
||||
*
|
||||
* <pre>
|
||||
* public class UserBirthdayServiceImpl {
|
||||
* ...
|
||||
* public UserBirthdayServiceImpl() {
|
||||
* ...
|
||||
* ActivityThread.currentActivityThread().disableUserBirthdayCache();
|
||||
* ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
|
||||
* }
|
||||
*
|
||||
* private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) {
|
||||
* mUidToBirthday.clear();
|
||||
* mUidToBirthday.putAll(uidToBirthday);
|
||||
* ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The call to {@code IpcDataCache.invalidateCache()} guarantees that all clients will re-fetch
|
||||
* birthdays from binder during consequent calls to
|
||||
* {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock
|
||||
* held, we maintain consistency between different client views of the birthday state. The use of
|
||||
* IpcDataCache in this idiomatic way introduces no new race conditions.
|
||||
*
|
||||
* IpcDataCache has a few other features for doing things like incremental enhancement of cached
|
||||
* values and invalidation of multiple caches (that all share the same property key) at once.
|
||||
*
|
||||
* {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each
|
||||
* time we update the cache. SELinux configuration must allow everyone to read this property
|
||||
* and it must allow any process that needs to invalidate the cache (here, birthdayd) to write
|
||||
* the property. (These properties conventionally begin with the "cache_key." prefix.)
|
||||
*
|
||||
* The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so
|
||||
* that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In this
|
||||
* local case, there's no IPC, so use of the cache is (depending on exact circumstance)
|
||||
* unnecessary.
|
||||
*
|
||||
* There may be queries for which it is more efficient to bypass the cache than to cache the
|
||||
* result. This would be true, for example, if some queries would require frequent cache
|
||||
* invalidation while other queries require infrequent invalidation. To expand on the birthday
|
||||
* example, suppose that there is a userId that signifies "the next birthday". When passed this
|
||||
* userId, the server returns the next birthday among all users - this value changes as time
|
||||
* advances. The userId value can be cached, but the cache must be invalidated whenever a
|
||||
* birthday occurs, and this invalidates all birthdays. If there is a large number of users,
|
||||
* invalidation will happen so often that the cache provides no value.
|
||||
*
|
||||
* The class provides a bypass mechanism to handle this situation.
|
||||
* <pre>
|
||||
* public class ActivityThread {
|
||||
* ...
|
||||
* private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery =
|
||||
* new IpcDataCache.QueryHandler<Integer, Birthday>() {
|
||||
* {@literal @}Override
|
||||
* public Birthday apply(Integer) {
|
||||
* return GetService("birthdayd").getUserBirthday(userId);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean shouldBypassQuery(Integer userId) {
|
||||
* return userId == NEXT_BIRTHDAY;
|
||||
* }
|
||||
* };
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
|
||||
* particular query. The {@code shouldBypassQuery()} method is not abstract and the default
|
||||
* implementation returns false.
|
||||
*
|
||||
* For security, there is a allowlist of processes that are allowed to invalidate a cache. The
|
||||
* allowlist includes normal runtime processes but does not include test processes. Test
|
||||
* processes must call {@code IpcDataCache.disableForTestMode()} to disable all cache activity in
|
||||
* that process.
|
||||
*
|
||||
* Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding.
|
||||
*
|
||||
* To test a binder cache, create one or more tests that exercise the binder method. This should
|
||||
* be done twice: once with production code and once with a special image that sets {@code DEBUG}
|
||||
* and {@code VERIFY} true. In the latter case, verify that no cache inconsistencies are
|
||||
* reported. If a cache inconsistency is reported, however, it might be a false positive. This
|
||||
* happens if the server side data can be read and written non-atomically with respect to cache
|
||||
* invalidation.
|
||||
*
|
||||
* @param <Query> The class used to index cache entries: must be hashable and comparable
|
||||
* @param <Result> The class holding cache entries; use a boxed primitive if possible
|
||||
* @hide
|
||||
*/
|
||||
@TestApi
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, Result> {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static abstract class QueryHandler<Q,R>
|
||||
extends PropertyInvalidatedCache.QueryHandler<Q,R> {
|
||||
/**
|
||||
* Compute a result given a query. The semantics are those of Functor.
|
||||
*/
|
||||
public abstract @Nullable R apply(@NonNull Q query);
|
||||
|
||||
/**
|
||||
* Return true if a query should not use the cache. The default implementation
|
||||
* always uses the cache.
|
||||
*/
|
||||
public boolean shouldBypassCache(@NonNull Q query) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The module used for unit tests and cts tests. It is expected that no process in
|
||||
* the system has permissions to write properties with this module.
|
||||
* @hide
|
||||
*/
|
||||
@TestApi
|
||||
public static final String MODULE_TEST = PropertyInvalidatedCache.MODULE_TEST;
|
||||
|
||||
/**
|
||||
* The module used for system server/framework caches. This is not visible outside
|
||||
* the system processes.
|
||||
* @hide
|
||||
*/
|
||||
@TestApi
|
||||
public static final String MODULE_SYSTEM = PropertyInvalidatedCache.MODULE_SYSTEM;
|
||||
|
||||
/**
|
||||
* The module used for bluetooth caches.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static final String MODULE_BLUETOOTH = PropertyInvalidatedCache.MODULE_BLUETOOTH;
|
||||
|
||||
/**
|
||||
* Make a new property invalidated cache. The key is computed from the module and api
|
||||
* parameters.
|
||||
*
|
||||
* @param maxEntries Maximum number of entries to cache; LRU discard
|
||||
* @param module The module under which the cache key should be placed.
|
||||
* @param api The api this cache front-ends. The api must be a Java identifier but
|
||||
* need not be an actual api.
|
||||
* @param cacheName Name of this cache in debug and dumpsys
|
||||
* @param computer The code to compute values that are not in the cache.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public IpcDataCache(int maxEntries, @NonNull String module, @NonNull String api,
|
||||
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
|
||||
super(maxEntries, module, api, cacheName, computer);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
@Override
|
||||
public void disableForCurrentProcess() {
|
||||
super.disableForCurrentProcess();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static void disableForCurrentProcess(@NonNull String cacheName) {
|
||||
PropertyInvalidatedCache.disableForCurrentProcess(cacheName);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
@Override
|
||||
public @Nullable Result query(@NonNull Query query) {
|
||||
return super.query(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
@Override
|
||||
public void invalidateCache() {
|
||||
super.invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate caches in all processes that are keyed for the module and api.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
|
||||
@TestApi
|
||||
public static void invalidateCache(@NonNull String module, @NonNull String api) {
|
||||
PropertyInvalidatedCache.invalidateCache(module, api);
|
||||
}
|
||||
}
|
||||
312
core/tests/coretests/src/android/os/IpcDataCacheTest.java
Normal file
312
core/tests/coretests/src/android/os/IpcDataCacheTest.java
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 android.os;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test for verifying the behavior of {@link IpcDataCache}. This test does
|
||||
* not use any actual binder calls - it is entirely self-contained. This test also relies
|
||||
* on the test mode of {@link IpcDataCache} because Android SELinux rules do
|
||||
* not grant test processes the permission to set system properties.
|
||||
* <p>
|
||||
* Build/Install/Run:
|
||||
* atest FrameworksCoreTests:IpcDataCacheTest
|
||||
*/
|
||||
@SmallTest
|
||||
public class IpcDataCacheTest {
|
||||
|
||||
// Configuration for creating caches
|
||||
private static final String MODULE = IpcDataCache.MODULE_TEST;
|
||||
private static final String API = "testApi";
|
||||
|
||||
// This class is a proxy for binder calls. It contains a counter that increments
|
||||
// every time the class is queried.
|
||||
private static class ServerProxy {
|
||||
// The number of times this class was queried.
|
||||
private int mCount = 0;
|
||||
|
||||
// A single query. The key behavior is that the query count is incremented.
|
||||
boolean query(int x) {
|
||||
mCount++;
|
||||
return value(x);
|
||||
}
|
||||
|
||||
// Return the expected value of an input, without incrementing the query count.
|
||||
boolean value(int x) {
|
||||
return x % 3 == 0;
|
||||
}
|
||||
|
||||
// Verify the count.
|
||||
void verify(int x) {
|
||||
assertEquals(x, mCount);
|
||||
}
|
||||
}
|
||||
|
||||
// The functions for querying the server.
|
||||
private static class ServerQuery
|
||||
extends IpcDataCache.QueryHandler<Integer, Boolean> {
|
||||
private final ServerProxy mServer;
|
||||
|
||||
ServerQuery(ServerProxy server) {
|
||||
mServer = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean apply(Integer x) {
|
||||
return mServer.query(x);
|
||||
}
|
||||
@Override
|
||||
public boolean shouldBypassCache(Integer x) {
|
||||
return x % 13 == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the test mode after every test, in case this process is used for other
|
||||
// tests. This also resets the test property map.
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
IpcDataCache.setTestMode(false);
|
||||
}
|
||||
|
||||
// This test is disabled pending an sepolicy change that allows any app to set the
|
||||
// test property.
|
||||
@Test
|
||||
public void testBasicCache() {
|
||||
|
||||
// A stand-in for the binder. The test verifies that calls are passed through to
|
||||
// this class properly.
|
||||
ServerProxy tester = new ServerProxy();
|
||||
|
||||
// Create a cache that uses simple arithmetic to computer its values.
|
||||
IpcDataCache<Integer, Boolean> testCache =
|
||||
new IpcDataCache<>(4, MODULE, API, "testCache1",
|
||||
new ServerQuery(tester));
|
||||
|
||||
IpcDataCache.setTestMode(true);
|
||||
testCache.testPropertyName();
|
||||
|
||||
tester.verify(0);
|
||||
assertEquals(tester.value(3), testCache.query(3));
|
||||
tester.verify(1);
|
||||
assertEquals(tester.value(3), testCache.query(3));
|
||||
tester.verify(2);
|
||||
testCache.invalidateCache();
|
||||
assertEquals(tester.value(3), testCache.query(3));
|
||||
tester.verify(3);
|
||||
assertEquals(tester.value(5), testCache.query(5));
|
||||
tester.verify(4);
|
||||
assertEquals(tester.value(5), testCache.query(5));
|
||||
tester.verify(4);
|
||||
assertEquals(tester.value(3), testCache.query(3));
|
||||
tester.verify(4);
|
||||
|
||||
// Invalidate the cache, and verify that the next read on 3 goes to the server.
|
||||
testCache.invalidateCache();
|
||||
assertEquals(tester.value(3), testCache.query(3));
|
||||
tester.verify(5);
|
||||
|
||||
// Test bypass. The query for 13 always bypasses the cache.
|
||||
assertEquals(tester.value(12), testCache.query(12));
|
||||
assertEquals(tester.value(13), testCache.query(13));
|
||||
assertEquals(tester.value(14), testCache.query(14));
|
||||
tester.verify(8);
|
||||
assertEquals(tester.value(12), testCache.query(12));
|
||||
assertEquals(tester.value(13), testCache.query(13));
|
||||
assertEquals(tester.value(14), testCache.query(14));
|
||||
tester.verify(9);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableCache() {
|
||||
|
||||
// A stand-in for the binder. The test verifies that calls are passed through to
|
||||
// this class properly.
|
||||
ServerProxy tester = new ServerProxy();
|
||||
|
||||
// Three caches, all using the same system property but one uses a different name.
|
||||
IpcDataCache<Integer, Boolean> cache1 =
|
||||
new IpcDataCache<>(4, MODULE, API, "cacheA",
|
||||
new ServerQuery(tester));
|
||||
IpcDataCache<Integer, Boolean> cache2 =
|
||||
new IpcDataCache<>(4, MODULE, API, "cacheA",
|
||||
new ServerQuery(tester));
|
||||
IpcDataCache<Integer, Boolean> cache3 =
|
||||
new IpcDataCache<>(4, MODULE, API, "cacheB",
|
||||
new ServerQuery(tester));
|
||||
|
||||
// Caches are enabled upon creation.
|
||||
assertEquals(false, cache1.getDisabledState());
|
||||
assertEquals(false, cache2.getDisabledState());
|
||||
assertEquals(false, cache3.getDisabledState());
|
||||
|
||||
// Disable the cache1 instance. Only cache1 is disabled
|
||||
cache1.disableInstance();
|
||||
assertEquals(true, cache1.getDisabledState());
|
||||
assertEquals(false, cache2.getDisabledState());
|
||||
assertEquals(false, cache3.getDisabledState());
|
||||
|
||||
// Disable cache1. This will disable cache1 and cache2 because they share the
|
||||
// same name. cache3 has a different name and will not be disabled.
|
||||
cache1.disableForCurrentProcess();
|
||||
assertEquals(true, cache1.getDisabledState());
|
||||
assertEquals(true, cache2.getDisabledState());
|
||||
assertEquals(false, cache3.getDisabledState());
|
||||
|
||||
// Create a new cache1. Verify that the new instance is disabled.
|
||||
cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA",
|
||||
new ServerQuery(tester));
|
||||
assertEquals(true, cache1.getDisabledState());
|
||||
|
||||
// Remove the record of caches being locally disabled. This is a clean-up step.
|
||||
cache1.forgetDisableLocal();
|
||||
assertEquals(true, cache1.getDisabledState());
|
||||
assertEquals(true, cache2.getDisabledState());
|
||||
assertEquals(false, cache3.getDisabledState());
|
||||
|
||||
// Create a new cache1. Verify that the new instance is not disabled.
|
||||
cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA",
|
||||
new ServerQuery(tester));
|
||||
assertEquals(false, cache1.getDisabledState());
|
||||
}
|
||||
|
||||
private static class TestQuery
|
||||
extends IpcDataCache.QueryHandler<Integer, String> {
|
||||
|
||||
private int mRecomputeCount = 0;
|
||||
|
||||
@Override
|
||||
public String apply(Integer qv) {
|
||||
mRecomputeCount += 1;
|
||||
return "foo" + qv.toString();
|
||||
}
|
||||
|
||||
int getRecomputeCount() {
|
||||
return mRecomputeCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestCache extends IpcDataCache<Integer, String> {
|
||||
private final TestQuery mQuery;
|
||||
|
||||
TestCache() {
|
||||
this(MODULE, API);
|
||||
}
|
||||
|
||||
TestCache(String module, String api) {
|
||||
this(module, api, new TestQuery());
|
||||
}
|
||||
|
||||
TestCache(String module, String api, TestQuery query) {
|
||||
super(4, module, api, "testCache7", query);
|
||||
mQuery = query;
|
||||
setTestMode(true);
|
||||
testPropertyName();
|
||||
}
|
||||
|
||||
int getRecomputeCount() {
|
||||
return mQuery.getRecomputeCount();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheRecompute() {
|
||||
TestCache cache = new TestCache();
|
||||
cache.invalidateCache();
|
||||
assertEquals(cache.isDisabled(), false);
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(1, cache.getRecomputeCount());
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(1, cache.getRecomputeCount());
|
||||
assertEquals("foo6", cache.query(6));
|
||||
assertEquals(2, cache.getRecomputeCount());
|
||||
cache.invalidateCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(3, cache.getRecomputeCount());
|
||||
// Invalidate the cache with a direct call to the property.
|
||||
IpcDataCache.invalidateCache(MODULE, API);
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(4, cache.getRecomputeCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheInitialState() {
|
||||
TestCache cache = new TestCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(2, cache.getRecomputeCount());
|
||||
cache.invalidateCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(3, cache.getRecomputeCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCachePropertyUnset() {
|
||||
final String UNSET_API = "otherApi";
|
||||
TestCache cache = new TestCache(MODULE, UNSET_API);
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(2, cache.getRecomputeCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCacheDisableState() {
|
||||
TestCache cache = new TestCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(2, cache.getRecomputeCount());
|
||||
cache.invalidateCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(3, cache.getRecomputeCount());
|
||||
cache.disableSystemWide();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(5, cache.getRecomputeCount());
|
||||
cache.invalidateCache(); // Should not reenable
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(7, cache.getRecomputeCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocalProcessDisable() {
|
||||
TestCache cache = new TestCache();
|
||||
assertEquals(cache.isDisabled(), false);
|
||||
cache.invalidateCache();
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(1, cache.getRecomputeCount());
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(1, cache.getRecomputeCount());
|
||||
assertEquals(cache.isDisabled(), false);
|
||||
cache.disableForCurrentProcess();
|
||||
assertEquals(cache.isDisabled(), true);
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals("foo5", cache.query(5));
|
||||
assertEquals(3, cache.getRecomputeCount());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user