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.bedstead.nene.utils; 18 19 import android.util.Log; 20 21 import com.android.bedstead.nene.exceptions.NeneException; 22 import com.android.bedstead.nene.exceptions.PollValueFailedException; 23 24 import java.time.Duration; 25 import java.time.Instant; 26 import java.util.Objects; 27 import java.util.function.Function; 28 import java.util.function.Supplier; 29 30 /** 31 * Utility class for polling for some state to be reached. 32 * 33 * <p>To use, you first use {@link #forValue(String, ValueSupplier)} to supply the value to be 34 * polled on. It is recommended you provide a descriptive name of the source of the value to improve 35 * failure messages. 36 * 37 * <p>Then you specify the criteria you are polling for, simple criteria are provided 38 * (e.g. {@link #toBeNull()}, {@link #toBeEqualTo(Object)}, etc.) and these should be preferred when 39 * possible as they provide good failure messages by default. If your state cannot be queried using 40 * a simple matcher, you can use {@link #toMeet(ValueChecker)} and pass in an arbitrary function to 41 * check the value. 42 * 43 * <p>By default, this will poll up to {@link #timeout(Duration)} (defaulting to 30 seconds), and 44 * will return after the timeout whatever the value is at that time. If you'd rather a 45 * {@link NeneException} is thrown, you can use {@link #errorOnFail()}. 46 * 47 * <p>You can add more context to failures using the overloaded versions of {@link #errorOnFail()}. 48 * In particular, you should do this if you're using {@link #toMeet(ValueChecker)} as otherwise the 49 * failure message is not helpful. 50 * 51 * <p>Any exceptions thrown when getting the value or when checking it will result in that check 52 * failing and a retry happening. If this is the final iteration the exception will be thrown 53 * wrapped in a {@link NeneException}. 54 * 55 * <p>You should not use this class to retry some state changing logic until it succeeds - it should 56 * only be used for polling a value until it reaches the value you want. 57 */ 58 public final class Poll<E> { 59 60 private static final String LOG_TAG = Poll.class.getName(); 61 62 private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); 63 private static final long SLEEP_MILLIS = 200; 64 private final String mValueName; 65 private final ValueSupplier<E> mSupplier; 66 private ValueChecker<E> mChecker = (v) -> true; 67 private Function<E, Boolean> mTerminalValueChecker; 68 private Function<Throwable, Boolean> mTerminalExceptionChecker; 69 private Function2<String, E, String> mErrorSupplier = 70 (valueName, value) -> "Expected " 71 + valueName + " to meet checker function. Was " + value; 72 private Duration mTimeout = DEFAULT_TIMEOUT; 73 private boolean mErrorOnFail = false; 74 Poll(String valueName, ValueSupplier<E> supplier)75 private Poll(String valueName, ValueSupplier<E> supplier) { 76 mValueName = valueName; 77 mSupplier = supplier; 78 } 79 80 /** 81 * Begin polling for the given value. 82 * 83 * <p>In general, this method should only be used when you're using the 84 * {@link #errorOnFail(Function)} method, otherwise {@link #forValue(String, ValueSupplier)} 85 * will mean better error messages. 86 */ forValue(ValueSupplier<E> supplier)87 public static <E> Poll<E> forValue(ValueSupplier<E> supplier) { 88 return forValue("value", supplier); 89 } 90 91 /** 92 * Begin polling for the given value. 93 * 94 * <p>The {@code valueName} will be used in error messages. 95 */ forValue(String valueName, ValueSupplier<E> supplier)96 public static <E> Poll<E> forValue(String valueName, ValueSupplier<E> supplier) { 97 return new Poll<>(valueName, supplier); 98 } 99 100 /** Expect the value to be null. */ toBeNull()101 public Poll<E> toBeNull() { 102 toMeet(Objects::isNull); 103 softErrorOnFail((valueName, value) -> 104 "Expected " + valueName + " to be null. Was " + value); 105 return this; 106 } 107 108 /** Expect the value to not be null. */ toNotBeNull()109 public Poll<E> toNotBeNull() { 110 toMeet(Objects::nonNull); 111 softErrorOnFail((valueName, value) -> 112 "Expected " + valueName + " to not be null. Was " + value); 113 return this; 114 } 115 116 /** Expect the value to be equal to {@code other}. */ toBeEqualTo(E other)117 public Poll<E> toBeEqualTo(E other) { 118 toMeet(v -> Objects.equals(v, other)); 119 softErrorOnFail((valueName, value) -> 120 "Expected " + valueName + " to be equal to " + other + ". Was " + value); 121 return this; 122 } 123 124 /** Expect the value to not be equal to {@code other}. */ toNotBeEqualTo(E other)125 public Poll<E> toNotBeEqualTo(E other) { 126 toMeet(v -> !Objects.equals(v, other)); 127 softErrorOnFail((valueName, value) -> 128 "Expected " + valueName + " to not be equal to " + other + ". Was " + value); 129 return this; 130 } 131 132 /** 133 * Expect the value to meet the requirements specified by {@code checker}. 134 * 135 * <p>If this method throws an exception, or returns false, then the value will be considered 136 * to not have met the requirements. If true is returned then the value will be considered to 137 * have met the requirements. 138 */ toMeet(ValueChecker<E> checker)139 public Poll<E> toMeet(ValueChecker<E> checker) { 140 mChecker = checker; 141 return this; 142 } 143 144 /** Throw an exception on failure instead of returning the incorrect value. */ errorOnFail()145 public Poll<E> errorOnFail() { 146 mErrorOnFail = true; 147 return this; 148 } 149 150 /** 151 * Throw an exception on failure instead of returning the incorrect value. 152 * 153 * <p>The {@code errorSupplier} will be passed the latest value. If you do not want to include 154 * the latest value in the error message (and have it auto-provided) use 155 * {@link #errorOnFail(String)}. 156 */ errorOnFail(Function<E, String> errorSupplier)157 public Poll<E> errorOnFail(Function<E, String> errorSupplier) { 158 softErrorOnFail((vn, v) -> errorSupplier.apply(v)); 159 mErrorOnFail = true; 160 return this; 161 } 162 163 /** 164 * Throw an exception on failure instead of returning the incorrect value. 165 * 166 * <p>The {@code error} will be used as the failure message, with the latest value added. 167 */ errorOnFail(String error)168 public Poll<E> errorOnFail(String error) { 169 softErrorOnFail((vn, v) -> error + ". " + vn + " was " + v); 170 mErrorOnFail = true; 171 return this; 172 } 173 softErrorOnFail(Function2<String, E, String> errorSupplier)174 private void softErrorOnFail(Function2<String, E, String> errorSupplier) { 175 mErrorSupplier = errorSupplier; 176 } 177 178 /** Change the default timeout before the check is considered failed (default 30 seconds). */ timeout(Duration timeout)179 public Poll<E> timeout(Duration timeout) { 180 mTimeout = timeout; 181 return this; 182 } 183 184 /** 185 * Await the value meeting the requirements. 186 * 187 * <p>This will retry fetching and checking the value until it meets the requirements or the 188 * timeout expires. 189 * 190 * <p>By default, the most recent value will be returned even after timeout. 191 * See {@link #errorOnFail()} to change this behavior. 192 */ await()193 public E await() { 194 Instant startTime = Instant.now(); 195 Instant endTime = startTime.plus(mTimeout); 196 197 E value = null; 198 int tries = 0; 199 200 while (!Duration.between(Instant.now(), endTime).isNegative()) { 201 tries++; 202 try { 203 value = mSupplier.get(); 204 if (mChecker.apply(value)) { 205 return value; 206 } 207 if (mTerminalValueChecker != null && mTerminalValueChecker.apply(value)) { 208 break; 209 } 210 } catch (Throwable e) { 211 // Eat the exception until the timeout 212 Log.e(LOG_TAG, "Exception during retries", e); 213 if (mTerminalExceptionChecker != null && mTerminalExceptionChecker.apply(e)) { 214 break; 215 } 216 } 217 218 try { 219 Thread.sleep(SLEEP_MILLIS); 220 } catch (InterruptedException e) { 221 throw new PollValueFailedException("Interrupted while awaiting", e); 222 } 223 } 224 225 if (!mErrorOnFail) { 226 return value; 227 } 228 229 // We call again to allow exceptions to be thrown - if it passes here we can still return 230 try { 231 value = mSupplier.get(); 232 } catch (Throwable e) { 233 long seconds = Duration.between(startTime, Instant.now()).toMillis() / 1000; 234 throw new PollValueFailedException(mErrorSupplier.apply(mValueName, value) 235 + " - Exception when getting value (checked " + tries + " times in " 236 + seconds + " seconds)", e); 237 } 238 239 try { 240 if (mChecker.apply(value)) { 241 return value; 242 } 243 244 long seconds = Duration.between(startTime, Instant.now()).toMillis() / 1000; 245 throw new PollValueFailedException( 246 mErrorSupplier.apply(mValueName, value) + " (checked " + tries + " times in " 247 + seconds + " seconds)"); 248 } catch (Throwable e) { 249 long seconds = Duration.between(startTime, Instant.now()).toMillis() / 1000; 250 throw new PollValueFailedException( 251 mErrorSupplier.apply(mValueName, value) + " (checked " + tries + " times in " 252 + seconds + " seconds)", e); 253 254 } 255 } 256 257 /** 258 * Set a method which, after a value fails the check, can tell if the failure is terminal. 259 * 260 * <p>This method will only be called after the value check fails. It will be passed the most 261 * recent value and should return true if this value is terminal. 262 * 263 * <p>If true is returned, then no more retries will be attempted, otherwise retries will 264 * continue until timeout. 265 */ terminalValue(Function<E, Boolean> terminalChecker)266 public Poll<E> terminalValue(Function<E, Boolean> terminalChecker) { 267 mTerminalValueChecker = terminalChecker; 268 return this; 269 } 270 271 /** 272 * Set a method which, after a value fails the check, can tell if the failure is terminal. 273 * 274 * <p>This method will only be called after the value check fails with an exception. It will be 275 * passed the exception return true if this exception is terminal. 276 * 277 * <p>If true is returned, then no more retries will be attempted, otherwise retries will 278 * continue until timeout. 279 */ terminalException(Function<Throwable, Boolean> terminalChecker)280 public Poll<E> terminalException(Function<Throwable, Boolean> terminalChecker) { 281 mTerminalExceptionChecker = terminalChecker; 282 return this; 283 } 284 285 /** 286 * Set a method which, after a value fails the check, can tell if the failure is terminal. 287 * 288 * <p>This method will only be called after the value check fails. It should return true if this 289 * state is terminal. 290 * 291 * <p>If true is returned, then no more retries will be attempted, otherwise retries will 292 * continue until timeout. 293 */ terminal(Supplier<Boolean> terminalChecker)294 public Poll<E> terminal(Supplier<Boolean> terminalChecker) { 295 terminalValue((e) -> terminalChecker.get()); 296 terminalException((e) -> terminalChecker.get()); 297 return this; 298 } 299 300 /** Interface for supplying values to {@link Poll}. */ 301 public interface ValueSupplier<E> { get()302 E get() throws Throwable; 303 } 304 305 /** Interface for checking values for {@link Poll}. */ 306 public interface ValueChecker<E> { apply(E e)307 boolean apply(E e) throws Throwable; 308 } 309 310 /** Interface for supplying errors for {@link Poll}. */ 311 public interface Function2<E, F, G> { apply(E e, F f)312 G apply(E e, F f); 313 } 314 } 315