Skip to content

Commit cdc2254

Browse files
cpovirkGoogle Java Core Libraries
authored and
Google Java Core Libraries
committed
Make most util.concurrent Duration overloads available to Android users.
(and also the `BooleanSupplier`-based `newGuard` method in `Monitor`) I skipped the `default` methods, as we're not yet sure how safe those are. RELNOTES=`util.concurrent`: Made most `Duration` overloads available to Android users. PiperOrigin-RevId: 687476839
1 parent 89ca0ef commit cdc2254

23 files changed

+866
-55
lines changed

android/guava-tests/test/com/google/common/util/concurrent/GeneratedMonitorTest.java

+60-20
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2424
import java.lang.reflect.InvocationTargetException;
2525
import java.lang.reflect.Method;
26+
import java.time.Duration;
27+
import java.util.ArrayList;
2628
import java.util.Arrays;
2729
import java.util.Comparator;
30+
import java.util.List;
2831
import java.util.Locale;
2932
import java.util.concurrent.CountDownLatch;
3033
import java.util.concurrent.FutureTask;
@@ -57,7 +60,7 @@ public static TestSuite suite() {
5760
}
5861
}
5962

60-
assertEquals(548, suite.testCount());
63+
assertEquals(980, suite.testCount());
6164

6265
return suite;
6366
}
@@ -185,14 +188,26 @@ private static boolean isGuarded(Method method) {
185188
return parameterTypes.length >= 1 && parameterTypes[0] == Monitor.Guard.class;
186189
}
187190

188-
/** Determines whether the given method takes a time and unit as its last two parameters. */
191+
/** Determines whether the given method is time-based. */
189192
private static boolean isTimed(Method method) {
193+
return isLongTimeUnitBased(method) || isDurationBased(method);
194+
}
195+
196+
/** Determines whether the given method takes a time and unit as its last two parameters. */
197+
private static boolean isLongTimeUnitBased(Method method) {
190198
Class<?>[] parameterTypes = method.getParameterTypes();
191199
return parameterTypes.length >= 2
192200
&& parameterTypes[parameterTypes.length - 2] == long.class
193201
&& parameterTypes[parameterTypes.length - 1] == TimeUnit.class;
194202
}
195203

204+
/** Determines whether the given method takes a Duration as its last parameter. */
205+
private static boolean isDurationBased(Method method) {
206+
Class<?>[] parameterTypes = method.getParameterTypes();
207+
return parameterTypes.length >= 1
208+
&& parameterTypes[parameterTypes.length - 1] == Duration.class;
209+
}
210+
196211
/** Determines whether the given method returns a boolean value. */
197212
private static boolean isBoolean(Method method) {
198213
return method.getReturnType() == boolean.class;
@@ -232,11 +247,21 @@ private static void validateMethod(Method method) {
232247
assertFalse(desc, isTimed(method));
233248
break;
234249
case 1:
235-
assertTrue(desc, isGuarded(method));
236-
assertFalse(desc, isTimed(method));
250+
if (isDurationBased(method)) {
251+
assertFalse(desc, isGuarded(method));
252+
} else {
253+
assertTrue(desc, isGuarded(method));
254+
}
255+
// we can't make an assumption about isTimed() because now we have single-parameter methods
256+
// that accept a java.time.Duration
257+
assertFalse(desc, isLongTimeUnitBased(method));
237258
break;
238259
case 2:
239-
assertFalse(desc, isGuarded(method));
260+
if (isDurationBased(method)) {
261+
assertTrue(desc, isGuarded(method));
262+
} else {
263+
assertFalse(desc, isGuarded(method));
264+
}
240265
assertTrue(desc, isTimed(method));
241266
break;
242267
case 3:
@@ -622,21 +647,22 @@ private void doWaitScenarioSetUp() {
622647
}
623648

624649
private Outcome doCall() {
625-
boolean guarded = isGuarded(method);
626-
boolean timed = isTimed(method);
627-
Object[] arguments = new Object[(guarded ? 1 : 0) + (timed ? 2 : 0)];
628-
if (guarded) {
629-
arguments[0] = guard;
650+
List<Object> arguments = new ArrayList<>();
651+
if (isGuarded(method)) {
652+
arguments.add(guard);
653+
}
654+
if (isLongTimeUnitBased(method)) {
655+
arguments.add(timeout.millis);
656+
arguments.add(TimeUnit.MILLISECONDS);
630657
}
631-
if (timed) {
632-
arguments[arguments.length - 2] = timeout.millis;
633-
arguments[arguments.length - 1] = TimeUnit.MILLISECONDS;
658+
if (isDurationBased(method)) {
659+
arguments.add(Duration.ofMillis(timeout.millis));
634660
}
635661
try {
636662
Object result;
637663
doingCallLatch.countDown();
638664
try {
639-
result = method.invoke(monitor, arguments);
665+
result = method.invoke(monitor, arguments.toArray());
640666
} finally {
641667
callCompletedLatch.countDown();
642668
}
@@ -721,16 +747,23 @@ protected void runTest() throws Throwable {
721747
Monitor monitor1 = new Monitor(fair1);
722748
Monitor monitor2 = new Monitor(fair2);
723749
FlagGuard guard = new FlagGuard(monitor2);
724-
Object[] arguments =
725-
(timed ? new Object[] {guard, 0L, TimeUnit.MILLISECONDS} : new Object[] {guard});
750+
List<Object> arguments = new ArrayList<>();
751+
arguments.add(guard);
752+
if (isDurationBased(method)) {
753+
arguments.add(Duration.ZERO);
754+
}
755+
if (isLongTimeUnitBased(method)) {
756+
arguments.add(0L);
757+
arguments.add(TimeUnit.MILLISECONDS);
758+
}
726759
boolean occupyMonitor = isWaitFor(method);
727760
if (occupyMonitor) {
728761
// If we don't already occupy the monitor, we'll get an IMSE regardless of the guard (see
729762
// generateWaitForWhenNotOccupyingTestCase).
730763
monitor1.enter();
731764
}
732765
try {
733-
method.invoke(monitor1, arguments);
766+
method.invoke(monitor1, arguments.toArray());
734767
fail("expected IllegalMonitorStateException");
735768
} catch (InvocationTargetException e) {
736769
assertEquals(IllegalMonitorStateException.class, e.getTargetException().getClass());
@@ -760,10 +793,17 @@ private static TestCase generateWaitForWhenNotOccupyingTestCase(
760793
protected void runTest() throws Throwable {
761794
Monitor monitor = new Monitor(fair);
762795
FlagGuard guard = new FlagGuard(monitor);
763-
Object[] arguments =
764-
(timed ? new Object[] {guard, 0L, TimeUnit.MILLISECONDS} : new Object[] {guard});
796+
List<Object> arguments = new ArrayList<>();
797+
arguments.add(guard);
798+
if (isDurationBased(method)) {
799+
arguments.add(Duration.ZERO);
800+
}
801+
if (isLongTimeUnitBased(method)) {
802+
arguments.add(0L);
803+
arguments.add(TimeUnit.MILLISECONDS);
804+
}
765805
try {
766-
method.invoke(monitor, arguments);
806+
method.invoke(monitor, arguments.toArray());
767807
fail("expected IllegalMonitorStateException");
768808
} catch (InvocationTargetException e) {
769809
assertEquals(IllegalMonitorStateException.class, e.getTargetException().getClass());

android/guava-tests/test/com/google/common/util/concurrent/ServiceManagerTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.common.testing.TestLogHandler;
3535
import com.google.common.util.concurrent.Service.State;
3636
import com.google.common.util.concurrent.ServiceManager.Listener;
37+
import java.time.Duration;
3738
import java.util.Arrays;
3839
import java.util.Collection;
3940
import java.util.List;
@@ -139,6 +140,21 @@ public void testServiceStartupTimes() {
139140
assertThat(startupTimes.get(b)).isAtLeast(353);
140141
}
141142

143+
public void testServiceStartupDurations() {
144+
if (isWindows() && isJava8()) {
145+
// Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607
146+
return;
147+
}
148+
Service a = new NoOpDelayedService(150);
149+
Service b = new NoOpDelayedService(353);
150+
ServiceManager serviceManager = new ServiceManager(asList(a, b));
151+
serviceManager.startAsync().awaitHealthy();
152+
ImmutableMap<Service, Duration> startupTimes = serviceManager.startupDurations();
153+
assertThat(startupTimes).hasSize(2);
154+
assertThat(startupTimes.get(a)).isAtLeast(Duration.ofMillis(150));
155+
assertThat(startupTimes.get(b)).isAtLeast(Duration.ofMillis(353));
156+
}
157+
142158
public void testServiceStartupTimes_selfStartingServices() {
143159
// This tests to ensure that:
144160
// 1. service times are accurate when the service is started by the manager

android/guava-tests/test/com/google/common/util/concurrent/UninterruptiblesTest.java

+21
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.common.testing.TearDown;
3535
import com.google.common.testing.TearDownStack;
3636
import com.google.errorprone.annotations.CanIgnoreReturnValue;
37+
import java.time.Duration;
3738
import java.util.Date;
3839
import java.util.concurrent.ArrayBlockingQueue;
3940
import java.util.concurrent.BlockingQueue;
@@ -468,6 +469,26 @@ public void testTryAcquireTimeoutMultiInterruptExpiredMultiPermit() {
468469
}
469470

470471
// executor.awaitTermination Testcases
472+
public void testTryAwaitTerminationUninterruptiblyDuration_success() {
473+
ExecutorService executor = newFixedThreadPool(1);
474+
requestInterruptIn(500);
475+
executor.execute(new SleepTask(1000));
476+
executor.shutdown();
477+
assertTrue(awaitTerminationUninterruptibly(executor, Duration.ofMillis(LONG_DELAY_MS)));
478+
assertTrue(executor.isTerminated());
479+
assertInterrupted();
480+
}
481+
482+
public void testTryAwaitTerminationUninterruptiblyDuration_failure() {
483+
ExecutorService executor = newFixedThreadPool(1);
484+
requestInterruptIn(500);
485+
executor.execute(new SleepTask(10000));
486+
executor.shutdown();
487+
assertFalse(awaitTerminationUninterruptibly(executor, Duration.ofSeconds(1)));
488+
assertFalse(executor.isTerminated());
489+
assertInterrupted();
490+
}
491+
471492
public void testTryAwaitTerminationUninterruptiblyLongTimeUnit_success() {
472493
ExecutorService executor = newFixedThreadPool(1);
473494
requestInterruptIn(500);

android/guava/src/com/google/common/util/concurrent/FluentFuture.java

+21
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
package com.google.common.util.concurrent;
1616

1717
import static com.google.common.base.Preconditions.checkNotNull;
18+
import static com.google.common.util.concurrent.Internal.toNanosSaturated;
1819

1920
import com.google.common.annotations.GwtCompatible;
2021
import com.google.common.annotations.GwtIncompatible;
2122
import com.google.common.annotations.J2ktIncompatible;
2223
import com.google.common.base.Function;
2324
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2425
import com.google.errorprone.annotations.DoNotMock;
26+
import java.time.Duration;
2527
import java.util.concurrent.ExecutionException;
2628
import java.util.concurrent.Executor;
2729
import java.util.concurrent.ScheduledExecutorService;
@@ -254,6 +256,25 @@ public final <X extends Throwable> FluentFuture<V> catchingAsync(
254256
return (FluentFuture<V>) Futures.catchingAsync(this, exceptionType, fallback, executor);
255257
}
256258

259+
/**
260+
* Returns a future that delegates to this future but will finish early (via a {@link
261+
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified timeout expires.
262+
* If the timeout expires, not only will the output future finish, but also the input future
263+
* ({@code this}) will be cancelled and interrupted.
264+
*
265+
* @param timeout when to time out the future
266+
* @param scheduledExecutor The executor service to enforce the timeout.
267+
* @since NEXT (but since 28.0 in the JRE flavor)
268+
*/
269+
@J2ktIncompatible
270+
@GwtIncompatible // ScheduledExecutorService
271+
@SuppressWarnings("Java7ApiChecker")
272+
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
273+
public final FluentFuture<V> withTimeout(
274+
Duration timeout, ScheduledExecutorService scheduledExecutor) {
275+
return withTimeout(toNanosSaturated(timeout), TimeUnit.NANOSECONDS, scheduledExecutor);
276+
}
277+
257278
/**
258279
* Returns a future that delegates to this future but will finish early (via a {@link
259280
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified timeout expires.

android/guava/src/com/google/common/util/concurrent/Futures.java

+93
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.common.base.Preconditions.checkNotNull;
1818
import static com.google.common.base.Preconditions.checkState;
19+
import static com.google.common.util.concurrent.Internal.toNanosSaturated;
1920
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
2021
import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly;
2122
import static java.util.Objects.requireNonNull;
@@ -34,6 +35,7 @@
3435
import com.google.common.util.concurrent.internal.InternalFutures;
3536
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3637
import com.google.errorprone.annotations.concurrent.LazyInit;
38+
import java.time.Duration;
3739
import java.util.Collection;
3840
import java.util.List;
3941
import java.util.concurrent.Callable;
@@ -219,6 +221,22 @@ private Futures() {}
219221
return task;
220222
}
221223

224+
/**
225+
* Schedules {@code callable} on the specified {@code executor}, returning a {@code Future}.
226+
*
227+
* @throws RejectedExecutionException if the task cannot be scheduled for execution
228+
* @since NEXT (but since 28.0 in the JRE flavor)
229+
*/
230+
@J2ktIncompatible
231+
@GwtIncompatible // java.util.concurrent.ScheduledExecutorService
232+
@SuppressWarnings("Java7ApiChecker")
233+
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
234+
// TODO(cpovirk): Return ListenableScheduledFuture?
235+
public static <O extends @Nullable Object> ListenableFuture<O> scheduleAsync(
236+
AsyncCallable<O> callable, Duration delay, ScheduledExecutorService executorService) {
237+
return scheduleAsync(callable, toNanosSaturated(delay), TimeUnit.NANOSECONDS, executorService);
238+
}
239+
222240
/**
223241
* Schedules {@code callable} on the specified {@code executor}, returning a {@code Future}.
224242
*
@@ -362,6 +380,27 @@ private Futures() {}
362380
* <p>The delegate future is interrupted and cancelled if it times out.
363381
*
364382
* @param delegate The future to delegate to.
383+
* @param time when to time out the future
384+
* @param scheduledExecutor The executor service to enforce the timeout.
385+
* @since NEXT (but since 28.0 in the JRE flavor)
386+
*/
387+
@J2ktIncompatible
388+
@GwtIncompatible // java.util.concurrent.ScheduledExecutorService
389+
@SuppressWarnings("Java7ApiChecker")
390+
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
391+
public static <V extends @Nullable Object> ListenableFuture<V> withTimeout(
392+
ListenableFuture<V> delegate, Duration time, ScheduledExecutorService scheduledExecutor) {
393+
return withTimeout(delegate, toNanosSaturated(time), TimeUnit.NANOSECONDS, scheduledExecutor);
394+
}
395+
396+
/**
397+
* Returns a future that delegates to another but will finish early (via a {@link
398+
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified duration expires.
399+
*
400+
* <p>The delegate future is interrupted and cancelled if it times out.
401+
*
402+
* @param delegate The future to delegate to.
403+
* @param time when to time out the future
365404
* @param unit the time unit of the time parameter
366405
* @param scheduledExecutor The executor service to enforce the timeout.
367406
* @since 19.0
@@ -1238,6 +1277,60 @@ public String toString() {
12381277
* @throws CancellationException if {@code get} throws a {@code CancellationException}
12391278
* @throws IllegalArgumentException if {@code exceptionClass} extends {@code RuntimeException} or
12401279
* does not have a suitable constructor
1280+
* @since NEXT (but since 28.0 in the JRE flavor)
1281+
*/
1282+
@CanIgnoreReturnValue
1283+
@J2ktIncompatible
1284+
@GwtIncompatible // reflection
1285+
@ParametricNullness
1286+
@SuppressWarnings("Java7ApiChecker")
1287+
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
1288+
public static <V extends @Nullable Object, X extends Exception> V getChecked(
1289+
Future<V> future, Class<X> exceptionClass, Duration timeout) throws X {
1290+
return getChecked(future, exceptionClass, toNanosSaturated(timeout), TimeUnit.NANOSECONDS);
1291+
}
1292+
1293+
/**
1294+
* Returns the result of {@link Future#get(long, TimeUnit)}, converting most exceptions to a new
1295+
* instance of the given checked exception type. This reduces boilerplate for a common use of
1296+
* {@code Future} in which it is unnecessary to programmatically distinguish between exception
1297+
* types or to extract other information from the exception instance.
1298+
*
1299+
* <p>Exceptions from {@code Future.get} are treated as follows:
1300+
*
1301+
* <ul>
1302+
* <li>Any {@link ExecutionException} has its <i>cause</i> wrapped in an {@code X} if the cause
1303+
* is a checked exception, an {@link UncheckedExecutionException} if the cause is a {@code
1304+
* RuntimeException}, or an {@link ExecutionError} if the cause is an {@code Error}.
1305+
* <li>Any {@link InterruptedException} is wrapped in an {@code X} (after restoring the
1306+
* interrupt).
1307+
* <li>Any {@link TimeoutException} is wrapped in an {@code X}.
1308+
* <li>Any {@link CancellationException} is propagated untouched, as is any other {@link
1309+
* RuntimeException} (though {@code get} implementations are discouraged from throwing such
1310+
* exceptions).
1311+
* </ul>
1312+
*
1313+
* <p>The overall principle is to continue to treat every checked exception as a checked
1314+
* exception, every unchecked exception as an unchecked exception, and every error as an error. In
1315+
* addition, the cause of any {@code ExecutionException} is wrapped in order to ensure that the
1316+
* new stack trace matches that of the current thread.
1317+
*
1318+
* <p>Instances of {@code exceptionClass} are created by choosing an arbitrary public constructor
1319+
* that accepts zero or more arguments, all of type {@code String} or {@code Throwable}
1320+
* (preferring constructors with at least one {@code String}) and calling the constructor via
1321+
* reflection. If the exception did not already have a cause, one is set by calling {@link
1322+
* Throwable#initCause(Throwable)} on it. If no such constructor exists, an {@code
1323+
* IllegalArgumentException} is thrown.
1324+
*
1325+
* @throws X if {@code get} throws any checked exception except for an {@code ExecutionException}
1326+
* whose cause is not itself a checked exception
1327+
* @throws UncheckedExecutionException if {@code get} throws an {@code ExecutionException} with a
1328+
* {@code RuntimeException} as its cause
1329+
* @throws ExecutionError if {@code get} throws an {@code ExecutionException} with an {@code
1330+
* Error} as its cause
1331+
* @throws CancellationException if {@code get} throws a {@code CancellationException}
1332+
* @throws IllegalArgumentException if {@code exceptionClass} extends {@code RuntimeException} or
1333+
* does not have a suitable constructor
12411334
* @since 19.0 (in 10.0 as {@code get} and with different parameter order)
12421335
*/
12431336
@CanIgnoreReturnValue

0 commit comments

Comments
 (0)