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