1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm.utils; 18 19 import android.annotation.NonNull; 20 import android.os.Handler; 21 22 import com.android.server.IoThread; 23 24 import java.util.concurrent.atomic.AtomicInteger; 25 import java.util.function.Supplier; 26 27 /** 28 * Loose throttle latest behavior for success/fail requests, with options to schedule or force a 29 * request through. Throttling is implicit and not configurable. This means requests are dispatched 30 * to the {@link Handler} immediately when received, and only batched while waiting on the next 31 * message execution or running request. 32 * 33 * This also means there is no explicit debouncing. Implicit debouncing is available through the 34 * same runtime delays in the {@link Handler} instance and the request execution, where multiple 35 * requests prior to the execution point are collapsed. 36 * 37 * Callers provide a {@link Handler} with which to schedule tasks on. This may be a highly 38 * contentious thread like {@link IoThread#getHandler()}, but note that there are no guarantees 39 * that the request will be handled before the system server dies. Ideally callers should handle 40 * re-initialization from stale state with no consequences to the user. 41 * 42 * This class will retry requests if they don't succeed, as provided by a true/false response from 43 * the block provided to run the request. This uses an exponential backoff mechanism, assuming that 44 * state write should be attempted immediately, but not retried so heavily as to potentially block 45 * other system server callers. Exceptions are not considered and will not result in a retry if 46 * thrown from inside the block. Caller should wrap with try-catch and rollback and transaction 47 * state before returning false to signal a retry. 48 * 49 * The caller is strictly responsible for data synchronization, as this class will not synchronize 50 * the request block, potentially running it multiple times or on multiple threads simultaneously 51 * if requests come in asynchronously. 52 */ 53 public class RequestThrottle { 54 55 private static final int DEFAULT_RETRY_MAX_ATTEMPTS = 5; 56 private static final int DEFAULT_DELAY_MS = 1000; 57 private static final int DEFAULT_BACKOFF_BASE = 2; 58 59 private final AtomicInteger mLastRequest = new AtomicInteger(0); 60 private final AtomicInteger mLastCommitted = new AtomicInteger(-1); 61 62 private final int mMaxAttempts; 63 private final int mFirstDelay; 64 private final int mBackoffBase; 65 66 private final AtomicInteger mCurrentRetry = new AtomicInteger(0); 67 68 @NonNull 69 private final Handler mHandler; 70 71 @NonNull 72 private final Supplier<Boolean> mBlock; 73 74 @NonNull 75 private final Runnable mRunnable; 76 77 /** 78 * @see #RequestThrottle(Handler, int, int, int, Supplier) 79 */ RequestThrottle(@onNull Handler handler, @NonNull Supplier<Boolean> block)80 public RequestThrottle(@NonNull Handler handler, @NonNull Supplier<Boolean> block) { 81 this(handler, DEFAULT_RETRY_MAX_ATTEMPTS, DEFAULT_DELAY_MS, DEFAULT_BACKOFF_BASE, 82 block); 83 } 84 85 /** 86 * Backoff timing is calculated as firstDelay * (backoffBase ^ retryAttempt). 87 * 88 * @param handler Representing the thread to run the provided block. 89 * @param block The action to run when scheduled, returning whether or not the request was 90 * successful. Note that any thrown exceptions will be ignored and not 91 * retried, since it's not easy to tell how destructive or retry-able an 92 * exception is. 93 * @param maxAttempts Number of times to re-attempt any single request. 94 * @param firstDelay The first delay used after the initial attempt. 95 * @param backoffBase The base of the backoff calculation, where retry attempt count is the 96 * exponent. 97 */ RequestThrottle(@onNull Handler handler, int maxAttempts, int firstDelay, int backoffBase, @NonNull Supplier<Boolean> block)98 public RequestThrottle(@NonNull Handler handler, int maxAttempts, int firstDelay, 99 int backoffBase, @NonNull Supplier<Boolean> block) { 100 mHandler = handler; 101 mBlock = block; 102 mMaxAttempts = maxAttempts; 103 mFirstDelay = firstDelay; 104 mBackoffBase = backoffBase; 105 mRunnable = this::runInternal; 106 } 107 108 /** 109 * Schedule the intended action on the provided {@link Handler}. 110 */ schedule()111 public void schedule() { 112 // To avoid locking the Handler twice by pre-checking hasCallbacks, instead just queue 113 // the Runnable again. It will no-op if the request has already been written to disk. 114 mLastRequest.incrementAndGet(); 115 mHandler.post(mRunnable); 116 } 117 118 /** 119 * Run the intended action immediately on the calling thread. Note that synchronization and 120 * deadlock between threads is not handled. This will immediately call the request block, and 121 * also potentially schedule a retry. The caller must not block itself. 122 * 123 * @return true if the write succeeded or the last request was already written 124 */ runNow()125 public boolean runNow() { 126 mLastRequest.incrementAndGet(); 127 return runInternal(); 128 } 129 runInternal()130 private boolean runInternal() { 131 int lastRequest = mLastRequest.get(); 132 int lastCommitted = mLastCommitted.get(); 133 if (lastRequest == lastCommitted) { 134 return true; 135 } 136 137 if (mBlock.get()) { 138 mCurrentRetry.set(0); 139 mLastCommitted.set(lastRequest); 140 return true; 141 } else { 142 int currentRetry = mCurrentRetry.getAndIncrement(); 143 if (currentRetry < mMaxAttempts) { 144 long nextDelay = 145 (long) (mFirstDelay * Math.pow(mBackoffBase, currentRetry)); 146 mHandler.postDelayed(mRunnable, nextDelay); 147 } else { 148 mCurrentRetry.set(0); 149 } 150 151 return false; 152 } 153 } 154 } 155