blob: a94167eb9fa70184a9aff777904e971c5fef480a [file] [log] [blame]
/*
* 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.android.collect.Sets;
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;
/**
* 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.
*/
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, Sets.newHashSet(), 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);
Set<String> packagesWithTokens = 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#numOperations()}. */
@Override
public int numOperations() {
synchronized (mOperationsLock) {
return mOperations.size();
}
}
/** See {@link OperationStorage#isBackupOperationInProgress()}. */
@Override
public boolean isBackupOperationInProgress() {
synchronized (mOperationsLock) {
for (int i = 0; i < mOperations.size(); i++) {
Operation op = mOperations.valueAt(i);
if (op.type == OpType.BACKUP && op.state == OpState.PENDING) {
return true;
}
}
return false;
}
}
/** See {@link OperationStorage#operationTokensForPackage()} */
@Override
public Set<Integer> operationTokensForPackage(String packageName) {
synchronized (mOperationsLock) {
final Set<Integer> tokens = mOpTokensByPackage.get(packageName);
Set<Integer> result = Sets.newHashSet();
if (tokens != null) {
result.addAll(tokens);
}
return result;
}
}
/** See {@link OperationStorage#operationTokensForOpType()} */
@Override
public Set<Integer> operationTokensForOpType(@OpType int type) {
Set<Integer> tokens = Sets.newHashSet();
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;
}
}
/** See {@link OperationStorage#operationTokensForOpState()} */
@Override
public Set<Integer> operationTokensForOpState(@OpState int state) {
Set<Integer> tokens = Sets.newHashSet();
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;
}
}
/**
* 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);
}
}
}