Merge "Add LifecycleOperationStorage and tests"

This commit is contained in:
Paul Hadfield
2021-12-06 14:41:54 +00:00
committed by Android (Google) Code Review
5 changed files with 659 additions and 3 deletions

View File

@@ -19,5 +19,9 @@ java_library_static {
defaults: ["platform_service_defaults"],
srcs: [":services.backup-sources"],
libs: ["services.core"],
static_libs: ["backuplib", "app-compat-annotations"],
static_libs: [
"backuplib",
"app-compat-annotations",
"guava",
],
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2021 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.server.backup;
import android.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Set;
/**
* OperationStorage is an abstraction around a set of active operations.
*
* Operations are registered with a token that must first be obtained from
* {@link UserBackupManagerService#generateRandomIntegerToken()}. When
* registering, the caller may also associate a set of package names with
* the operation.
*
* TODO(b/208442527): have the token be generated within and returned by
* registerOperation, as it should be an internal detail.
*
* Operations have a type and a state. Although ints, the values that can
* be used are defined in {@link UserBackupManagerService}. If the type of
* an operation is OP_BACKUP, then it represents a task running backups. The
* task is provided when registering the operation because it provides a
* handle to cancel the backup.
*/
public interface OperationStorage {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
OpState.PENDING,
OpState.ACKNOWLEDGED,
OpState.TIMEOUT
})
public @interface OpState {
// The operation is in progress.
int PENDING = 0;
// The operation has been acknowledged.
int ACKNOWLEDGED = 1;
// The operation has timed out.
int TIMEOUT = -1;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
OpType.BACKUP_WAIT,
OpType.RESTORE_WAIT,
OpType.BACKUP,
})
public @interface OpType {
// Waiting for backup agent to respond during backup operation.
int BACKUP_WAIT = 0;
// Waiting for backup agent to respond during restore operation.
int RESTORE_WAIT = 1;
// An entire backup operation spanning multiple packages.
int BACKUP = 2;
}
/**
* Record an ongoing operation of given type and in the given initial
* state. The associated task is used as a callback.
*
* @param token an operation token issued by
* {@link UserBackupManagerService#generateRandomIntegerToken()}
* @param initialState the state that the operation starts in
* @param task the {@link BackupRestoreTask} that is expected to
* remove the operation on completion, and which may
* be notified if the operation requires cancelling.
* @param type the type of the operation.
*/
void registerOperation(int token, @OpState int initialState,
BackupRestoreTask task, @OpType int type);
/**
* See {@link #registerOperation()}. In addition this method accepts a set
* of package names which are associated with the operation.
*
* @param token See {@link #registerOperation()}
* @param initialState See {@link #registerOperation()}
* @param packageNames the package names to associate with the operation.
* @param task See {@link #registerOperation()}
* @param type See {@link #registerOperation()}
*/
void registerOperationForPackages(int token, @OpState int initialState,
Set<String> packageNames, BackupRestoreTask task, @OpType int type);
/**
* Remove the operation identified by token. This is called when the
* operation is no longer in progress and should be dropped. Any association
* with package names provided in {@link #registerOperation()} is dropped as
* well.
*
* @param token the operation token specified when registering the operation.
*/
void removeOperation(int token);
/**
* Obtain a set of operation tokens for all pending operations that were
* registered with an association to the specified package name.
*
* @param packageName the name of the package used at registration time
*
* @return a set of operation tokens associated to package name.
*/
Set<Integer> operationTokensForPackage(String packageName);
/**
* Obtain a set of operation tokens for all pending operations that are
* of the specified operation type.
*
* @param type the type of the operation provided at registration time.
*
* @return a set of operation tokens for operations of that type.
*/
Set<Integer> operationTokensForOpType(@OpType int type);
/**
* Obtain a set of operation tokens for all pending operations that are
* currently in the specified operation state.
*
* @param state the state of the operation.
*
* @return a set of operation tokens for operations in that state.
*/
Set<Integer> operationTokensForOpState(@OpState int state);
};

View File

@@ -290,8 +290,8 @@ public class UserBackupManagerService {
// Bookkeeping of in-flight operations. The operation token is the index of the entry in the
// pending operations list.
public static final int OP_PENDING = 0;
private static final int OP_ACKNOWLEDGED = 1;
private static final int OP_TIMEOUT = -1;
public static final int OP_ACKNOWLEDGED = 1;
public static final int OP_TIMEOUT = -1;
// Waiting for backup agent to respond during backup operation.
public static final int OP_TYPE_BACKUP_WAIT = 0;

View File

@@ -0,0 +1,337 @@
/*
* Copyright (C) 2021 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.server.backup.internal;
import static com.android.server.backup.BackupManagerService.DEBUG;
import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
import android.annotation.UserIdInt;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.OperationStorage;
import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import javax.annotation.concurrent.ThreadSafe;
/**
* LifecycleOperationStorage is responsible for maintaining a set of currently
* active operations. Each operation has a type and state, and a callback that
* can receive events upon operation completion or cancellation. It may also
* be associated with one or more package names.
*
* An operation wraps a {@link BackupRestoreTask} within it.
* It's the responsibility of this task to remove the operation from this array.
*
* If type of operation is {@code OP_TYPE_WAIT}, it is waiting for an ACK or
* timeout.
*
* A BackupRestore task gets notified of AVK/timeout for the operation via
* {@link BackupRestoreTask#handleCancel()},
* {@link BackupRestoreTask#operationComplete()} and {@code notifyAll} called
* on the {@code mCurrentOpLock}.
*
* {@link LifecycleOperationStorage#waitUntilOperationComplete(int)} is used in
* various places to 'wait' for notifyAll and detect change of pending state of
* an operation. So typically, an operation will be removed from this array by:
* - {@link BackupRestoreTask#handleCancel()} and
* - {@link BackupRestoreTask#operationComplete()} OR
* {@link BackupRestoreTask#waitUntilOperationComplete()}.
* Do not remove at both these places because {@code waitUntilOperationComplete}
* relies on the operation being present to determine its completion status.
*
* If type of operation is {@code OP_BACKUP}, it is a task running backups. It
* provides a handle to cancel backup tasks.
*/
@ThreadSafe
public class LifecycleOperationStorage implements OperationStorage {
private static final String TAG = "LifecycleOperationStorage";
private final int mUserId;
private final Object mOperationsLock = new Object();
// Bookkeeping of in-flight operations. The operation token is the index of
// the entry in the pending operations list.
@GuardedBy("mOperationsLock")
private final SparseArray<Operation> mOperations = new SparseArray<>();
// Association from package name to one or more operations relating to that
// package.
@GuardedBy("mOperationsLock")
private final Map<String, Set<Integer>> mOpTokensByPackage = new HashMap<>();
public LifecycleOperationStorage(@UserIdInt int userId) {
this.mUserId = userId;
}
/** See {@link OperationStorage#registerOperation()} */
@Override
public void registerOperation(int token, @OpState int initialState,
BackupRestoreTask task, @OpType int type) {
registerOperationForPackages(token, initialState, ImmutableSet.of(), task, type);
}
/** See {@link OperationStorage#registerOperationForPackages()} */
@Override
public void registerOperationForPackages(int token, @OpState int initialState,
Set<String> packageNames, BackupRestoreTask task, @OpType int type) {
synchronized (mOperationsLock) {
mOperations.put(token, new Operation(initialState, task, type));
for (String packageName : packageNames) {
Set<Integer> tokens = mOpTokensByPackage.get(packageName);
if (tokens == null) {
tokens = new HashSet<Integer>();
}
tokens.add(token);
mOpTokensByPackage.put(packageName, tokens);
}
}
}
/** See {@link OperationStorage#removeOperation()} */
@Override
public void removeOperation(int token) {
synchronized (mOperationsLock) {
mOperations.remove(token);
ImmutableSet<String> packagesWithTokens =
ImmutableSet.copyOf(mOpTokensByPackage.keySet());
for (String packageName : packagesWithTokens) {
Set<Integer> tokens = mOpTokensByPackage.get(packageName);
if (tokens == null) {
continue;
}
tokens.remove(token);
mOpTokensByPackage.put(packageName, tokens);
}
}
}
/** See {@link OperationStorage#operationTokensForPackage()} */
@Override
public Set<Integer> operationTokensForPackage(String packageName) {
synchronized (mOperationsLock) {
Set<Integer> tokens = mOpTokensByPackage.get(packageName);
if (tokens == null) {
return ImmutableSet.of();
}
return ImmutableSet.copyOf(tokens);
}
}
/** See {@link OperationStorage#operationTokensForOpType()} */
@Override
public Set<Integer> operationTokensForOpType(@OpType int type) {
ImmutableSet.Builder<Integer> tokens = ImmutableSet.builder();
synchronized (mOperationsLock) {
for (int i = 0; i < mOperations.size(); i++) {
final Operation op = mOperations.valueAt(i);
final int token = mOperations.keyAt(i);
if (op.type == type) {
tokens.add(token);
}
}
return tokens.build();
}
}
/** See {@link OperationStorage#operationTokensForOpState()} */
@Override
public Set<Integer> operationTokensForOpState(@OpState int state) {
ImmutableSet.Builder<Integer> tokens = ImmutableSet.builder();
synchronized (mOperationsLock) {
for (int i = 0; i < mOperations.size(); i++) {
final Operation op = mOperations.valueAt(i);
final int token = mOperations.keyAt(i);
if (op.state == state) {
tokens.add(token);
}
}
return tokens.build();
}
}
/**
* A blocking function that blocks the caller until the operation identified
* by {@code token} is complete - either via a message from the backup,
* agent or through cancellation.
*
* @param token the operation token specified when registering the operation
* @param callback a lambda which is invoked once only when the operation
* completes - ie. if this method is called twice for the
* same token, the lambda is not invoked the second time.
* @return true if the operation was ACKed prior to or during this call.
*/
public boolean waitUntilOperationComplete(int token, IntConsumer callback) {
if (MORE_DEBUG) {
Slog.i(TAG, "[UserID:" + mUserId + "] Blocking until operation complete for "
+ Integer.toHexString(token));
}
@OpState int finalState = OpState.PENDING;
Operation op = null;
synchronized (mOperationsLock) {
while (true) {
op = mOperations.get(token);
if (op == null) {
// mysterious disappearance: treat as success with no callback
break;
} else {
if (op.state == OpState.PENDING) {
try {
mOperationsLock.wait();
} catch (InterruptedException e) {
Slog.w(TAG, "Waiting on mOperationsLock: ", e);
}
// When the wait is notified we loop around and recheck the current state
} else {
if (MORE_DEBUG) {
Slog.d(TAG, "[UserID:" + mUserId
+ "] Unblocked waiting for operation token="
+ Integer.toHexString(token));
}
// No longer pending; we're done
finalState = op.state;
break;
}
}
}
}
removeOperation(token);
if (op != null) {
callback.accept(op.type);
}
if (MORE_DEBUG) {
Slog.v(TAG, "[UserID:" + mUserId + "] operation " + Integer.toHexString(token)
+ " complete: finalState=" + finalState);
}
return finalState == OpState.ACKNOWLEDGED;
}
/**
* Signals that an ongoing operation is complete: after a currently-active
* backup agent has notified us that it has completed the outstanding
* asynchronous backup/restore operation identified by the supplied
* {@code} token.
*
* @param token the operation token specified when registering the operation
* @param result a result code or error code for the completed operation
* @param callback a lambda that is invoked if the completion moves the
* operation from PENDING to ACKNOWLEDGED state.
*/
public void onOperationComplete(int token, long result, Consumer<BackupRestoreTask> callback) {
if (MORE_DEBUG) {
Slog.v(TAG, "[UserID:" + mUserId + "] onOperationComplete: "
+ Integer.toHexString(token) + " result=" + result);
}
Operation op = null;
synchronized (mOperationsLock) {
op = mOperations.get(token);
if (op != null) {
if (op.state == OpState.TIMEOUT) {
// The operation already timed out, and this is a late response. Tidy up
// and ignore it; we've already dealt with the timeout.
op = null;
mOperations.remove(token);
} else if (op.state == OpState.ACKNOWLEDGED) {
if (DEBUG) {
Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token="
+ Integer.toHexString(token));
}
op = null;
mOperations.remove(token);
} else if (op.state == OpState.PENDING) {
// Can't delete op from mOperations. waitUntilOperationComplete can be
// called after we we receive this call.
op.state = OpState.ACKNOWLEDGED;
}
}
mOperationsLock.notifyAll();
}
// Invoke the operation's completion callback, if there is one.
if (op != null && op.callback != null) {
callback.accept(op.callback);
}
}
/**
* Cancel the operation associated with {@code token}. Cancellation may be
* propagated to the operation's callback (a {@link BackupRestoreTask}) if
* the operation has one, and the cancellation is due to the operation
* timing out.
*
* @param token the operation token specified when registering the operation
* @param cancelAll this is passed on when propagating the cancellation
* @param operationTimedOutCallback a lambda that is invoked with the
* operation type where the operation is
* cancelled due to timeout, allowing the
* caller to do type-specific clean-ups.
*/
public void cancelOperation(
int token, boolean cancelAll, IntConsumer operationTimedOutCallback) {
// Notify any synchronous waiters
Operation op = null;
synchronized (mOperationsLock) {
op = mOperations.get(token);
if (MORE_DEBUG) {
if (op == null) {
Slog.w(TAG, "[UserID:" + mUserId + "] Cancel of token "
+ Integer.toHexString(token) + " but no op found");
}
}
int state = (op != null) ? op.state : OpState.TIMEOUT;
if (state == OpState.ACKNOWLEDGED) {
// The operation finished cleanly, so we have nothing more to do.
if (DEBUG) {
Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack."
+ "Should have been removed from mCurrentOperations.");
}
op = null;
mOperations.delete(token);
} else if (state == OpState.PENDING) {
if (DEBUG) {
Slog.v(TAG, "[UserID:" + mUserId + "] Cancel: token="
+ Integer.toHexString(token));
}
op.state = OpState.TIMEOUT;
// Can't delete op from mOperations here. waitUntilOperationComplete may be
// called after we receive cancel here. We need this op's state there.
operationTimedOutCallback.accept(op.type);
}
mOperationsLock.notifyAll();
}
// If there's a TimeoutHandler for this event, call it
if (op != null && op.callback != null) {
if (MORE_DEBUG) {
Slog.v(TAG, "[UserID:" + mUserId + " Invoking cancel on " + op.callback);
}
op.callback.handleCancel(cancelAll);
}
}
};

View File

@@ -0,0 +1,174 @@
/*
* Copyright (C) 2021 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.server.backup.internal;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import android.platform.test.annotations.Presubmit;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.backup.BackupRestoreTask;
import com.android.server.backup.OperationStorage.OpState;
import com.android.server.backup.OperationStorage.OpType;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Set;
@Presubmit
@RunWith(AndroidJUnit4.class)
public class LifecycleOperationStorageTest {
private static final int USER_ID = 0;
private static final int TOKEN_1 = 1;
private static final int TOKEN_2 = 2;
private static final long RESULT = 123L;
private static final String PKG_FOO = "com.android.foo";
private static final String PKG_BAR = "com.android.bar";
private static final String PKG_BAZ = "com.android.baz";
private static final ImmutableSet<String> MULTIPLE_PKG = ImmutableSet.of(PKG_FOO);
private static final ImmutableSet<String> MULTIPLE_PKGS_1 = ImmutableSet.of(PKG_FOO, PKG_BAR);
private static final ImmutableSet<String> MULTIPLE_PKGS_2 = ImmutableSet.of(PKG_BAR, PKG_BAZ);
@Mock private BackupRestoreTask mCallback;
private LifecycleOperationStorage mOpStorage;
@Before
public void setUp() {
MockitoAnnotations.initMocks(/* testClass */ this);
mOpStorage = new LifecycleOperationStorage(USER_ID);
}
@After
public void tearDown() {}
@Test
public void testRegisterOperation_singleOperation() throws Exception {
mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
Set<Integer> tokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
assertThat(tokens).isEqualTo(only(TOKEN_1));
}
@Test
public void testRegisterOperation_multipleOperations() throws Exception {
mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
mOpStorage.registerOperation(TOKEN_2, OpState.ACKNOWLEDGED, mCallback, OpType.BACKUP_WAIT);
Set<Integer> typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
Set<Integer> statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
Set<Integer> stateAcknowledgedTokens =
mOpStorage.operationTokensForOpState(OpState.ACKNOWLEDGED);
assertThat(typeWaitTokens).isEqualTo(ImmutableSet.of(TOKEN_1, TOKEN_2));
assertThat(statePendingTokens).isEqualTo(only(TOKEN_1));
assertThat(stateAcknowledgedTokens).isEqualTo(only(TOKEN_2));
}
@Test
public void testRegisterOperationForPackages_singlePackage() throws Exception {
mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING,
MULTIPLE_PKG, mCallback, OpType.BACKUP_WAIT);
Set<Integer> tokens = mOpStorage.operationTokensForPackage(PKG_FOO);
assertThat(tokens).isEqualTo(only(TOKEN_1));
}
@Test
public void testRegisterOperationForPackages_multiplePackage() throws Exception {
mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING,
MULTIPLE_PKGS_1, mCallback, OpType.BACKUP);
mOpStorage.registerOperationForPackages(TOKEN_2, OpState.PENDING,
MULTIPLE_PKGS_2, mCallback, OpType.BACKUP);
Set<Integer> tokensFoo = mOpStorage.operationTokensForPackage(PKG_FOO);
Set<Integer> tokensBar = mOpStorage.operationTokensForPackage(PKG_BAR);
Set<Integer> tokensBaz = mOpStorage.operationTokensForPackage(PKG_BAZ);
assertThat(tokensFoo).isEqualTo(only(TOKEN_1));
assertThat(tokensBar).isEqualTo(ImmutableSet.of(TOKEN_1, TOKEN_2));
assertThat(tokensBaz).isEqualTo(only(TOKEN_2));
}
@Test
public void testRemoveOperation() throws Exception {
mOpStorage.registerOperation(TOKEN_2, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
Set<Integer> typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
Set<Integer> statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
assertThat(typeWaitTokens).isEqualTo(only(TOKEN_2));
assertThat(statePendingTokens).isEqualTo(only(TOKEN_2));
mOpStorage.removeOperation(TOKEN_2);
typeWaitTokens = mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT);
statePendingTokens = mOpStorage.operationTokensForOpState(OpState.PENDING);
assertThat(typeWaitTokens).isEmpty();
assertThat(statePendingTokens).isEmpty();
}
@Test
public void testRemoveOperation_removesPackageMappings() throws Exception {
mOpStorage.registerOperationForPackages(TOKEN_1, OpState.PENDING, MULTIPLE_PKGS_1,
mCallback, OpType.BACKUP);
mOpStorage.registerOperationForPackages(TOKEN_2, OpState.PENDING, MULTIPLE_PKGS_2,
mCallback, OpType.BACKUP);
mOpStorage.removeOperation(TOKEN_2);
Set<Integer> tokensFoo = mOpStorage.operationTokensForPackage(PKG_FOO);
Set<Integer> tokensBar = mOpStorage.operationTokensForPackage(PKG_BAR);
Set<Integer> tokensBaz = mOpStorage.operationTokensForPackage(PKG_BAZ);
assertThat(tokensFoo).isEqualTo(only(TOKEN_1));
assertThat(tokensBar).isEqualTo(only(TOKEN_1));
assertThat(tokensBaz).isEmpty();
}
@Test
public void testOnOperationComplete_pendingAdvancesState_invokesCallback() throws Exception {
mOpStorage.registerOperation(TOKEN_1, OpState.PENDING, mCallback, OpType.BACKUP_WAIT);
mOpStorage.onOperationComplete(TOKEN_1, RESULT, callback -> {
mCallback.operationComplete(RESULT);
});
assertThat(mOpStorage.operationTokensForOpType(OpType.BACKUP_WAIT))
.isEqualTo(only(TOKEN_1));
assertThat(mOpStorage.operationTokensForOpState(OpState.PENDING)).isEmpty();
assertThat(mOpStorage.operationTokensForOpState(OpState.ACKNOWLEDGED)).isNotEmpty();
verify(mCallback).operationComplete(RESULT);
}
private Set<Integer> only(Integer val) {
return ImmutableSet.of(val);
}
}