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