Skip to content

Commit

Permalink
Make most util.concurrent Duration overloads available to Android…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
cpovirk authored and Google Java Core Libraries committed Oct 19, 2024
1 parent 89ca0ef commit cdc2254
Show file tree
Hide file tree
Showing 23 changed files with 866 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
Expand Down Expand Up @@ -57,7 +60,7 @@ public static TestSuite suite() {
}
}

assertEquals(548, suite.testCount());
assertEquals(980, suite.testCount());

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

/** Determines whether the given method takes a time and unit as its last two parameters. */
/** Determines whether the given method is time-based. */
private static boolean isTimed(Method method) {
return isLongTimeUnitBased(method) || isDurationBased(method);
}

/** Determines whether the given method takes a time and unit as its last two parameters. */
private static boolean isLongTimeUnitBased(Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
return parameterTypes.length >= 2
&& parameterTypes[parameterTypes.length - 2] == long.class
&& parameterTypes[parameterTypes.length - 1] == TimeUnit.class;
}

/** Determines whether the given method takes a Duration as its last parameter. */
private static boolean isDurationBased(Method method) {
Class<?>[] parameterTypes = method.getParameterTypes();
return parameterTypes.length >= 1
&& parameterTypes[parameterTypes.length - 1] == Duration.class;
}

/** Determines whether the given method returns a boolean value. */
private static boolean isBoolean(Method method) {
return method.getReturnType() == boolean.class;
Expand Down Expand Up @@ -232,11 +247,21 @@ private static void validateMethod(Method method) {
assertFalse(desc, isTimed(method));
break;
case 1:
assertTrue(desc, isGuarded(method));
assertFalse(desc, isTimed(method));
if (isDurationBased(method)) {
assertFalse(desc, isGuarded(method));
} else {
assertTrue(desc, isGuarded(method));
}
// we can't make an assumption about isTimed() because now we have single-parameter methods
// that accept a java.time.Duration
assertFalse(desc, isLongTimeUnitBased(method));
break;
case 2:
assertFalse(desc, isGuarded(method));
if (isDurationBased(method)) {
assertTrue(desc, isGuarded(method));
} else {
assertFalse(desc, isGuarded(method));
}
assertTrue(desc, isTimed(method));
break;
case 3:
Expand Down Expand Up @@ -622,21 +647,22 @@ private void doWaitScenarioSetUp() {
}

private Outcome doCall() {
boolean guarded = isGuarded(method);
boolean timed = isTimed(method);
Object[] arguments = new Object[(guarded ? 1 : 0) + (timed ? 2 : 0)];
if (guarded) {
arguments[0] = guard;
List<Object> arguments = new ArrayList<>();
if (isGuarded(method)) {
arguments.add(guard);
}
if (isLongTimeUnitBased(method)) {
arguments.add(timeout.millis);
arguments.add(TimeUnit.MILLISECONDS);
}
if (timed) {
arguments[arguments.length - 2] = timeout.millis;
arguments[arguments.length - 1] = TimeUnit.MILLISECONDS;
if (isDurationBased(method)) {
arguments.add(Duration.ofMillis(timeout.millis));
}
try {
Object result;
doingCallLatch.countDown();
try {
result = method.invoke(monitor, arguments);
result = method.invoke(monitor, arguments.toArray());
} finally {
callCompletedLatch.countDown();
}
Expand Down Expand Up @@ -721,16 +747,23 @@ protected void runTest() throws Throwable {
Monitor monitor1 = new Monitor(fair1);
Monitor monitor2 = new Monitor(fair2);
FlagGuard guard = new FlagGuard(monitor2);
Object[] arguments =
(timed ? new Object[] {guard, 0L, TimeUnit.MILLISECONDS} : new Object[] {guard});
List<Object> arguments = new ArrayList<>();
arguments.add(guard);
if (isDurationBased(method)) {
arguments.add(Duration.ZERO);
}
if (isLongTimeUnitBased(method)) {
arguments.add(0L);
arguments.add(TimeUnit.MILLISECONDS);
}
boolean occupyMonitor = isWaitFor(method);
if (occupyMonitor) {
// If we don't already occupy the monitor, we'll get an IMSE regardless of the guard (see
// generateWaitForWhenNotOccupyingTestCase).
monitor1.enter();
}
try {
method.invoke(monitor1, arguments);
method.invoke(monitor1, arguments.toArray());
fail("expected IllegalMonitorStateException");
} catch (InvocationTargetException e) {
assertEquals(IllegalMonitorStateException.class, e.getTargetException().getClass());
Expand Down Expand Up @@ -760,10 +793,17 @@ private static TestCase generateWaitForWhenNotOccupyingTestCase(
protected void runTest() throws Throwable {
Monitor monitor = new Monitor(fair);
FlagGuard guard = new FlagGuard(monitor);
Object[] arguments =
(timed ? new Object[] {guard, 0L, TimeUnit.MILLISECONDS} : new Object[] {guard});
List<Object> arguments = new ArrayList<>();
arguments.add(guard);
if (isDurationBased(method)) {
arguments.add(Duration.ZERO);
}
if (isLongTimeUnitBased(method)) {
arguments.add(0L);
arguments.add(TimeUnit.MILLISECONDS);
}
try {
method.invoke(monitor, arguments);
method.invoke(monitor, arguments.toArray());
fail("expected IllegalMonitorStateException");
} catch (InvocationTargetException e) {
assertEquals(IllegalMonitorStateException.class, e.getTargetException().getClass());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.common.testing.TestLogHandler;
import com.google.common.util.concurrent.Service.State;
import com.google.common.util.concurrent.ServiceManager.Listener;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -139,6 +140,21 @@ public void testServiceStartupTimes() {
assertThat(startupTimes.get(b)).isAtLeast(353);
}

public void testServiceStartupDurations() {
if (isWindows() && isJava8()) {
// Flaky there: https://github.com/google/guava/pull/6731#issuecomment-1736298607
return;
}
Service a = new NoOpDelayedService(150);
Service b = new NoOpDelayedService(353);
ServiceManager serviceManager = new ServiceManager(asList(a, b));
serviceManager.startAsync().awaitHealthy();
ImmutableMap<Service, Duration> startupTimes = serviceManager.startupDurations();
assertThat(startupTimes).hasSize(2);
assertThat(startupTimes.get(a)).isAtLeast(Duration.ofMillis(150));
assertThat(startupTimes.get(b)).isAtLeast(Duration.ofMillis(353));
}

public void testServiceStartupTimes_selfStartingServices() {
// This tests to ensure that:
// 1. service times are accurate when the service is started by the manager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.common.testing.TearDown;
import com.google.common.testing.TearDownStack;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
Expand Down Expand Up @@ -468,6 +469,26 @@ public void testTryAcquireTimeoutMultiInterruptExpiredMultiPermit() {
}

// executor.awaitTermination Testcases
public void testTryAwaitTerminationUninterruptiblyDuration_success() {
ExecutorService executor = newFixedThreadPool(1);
requestInterruptIn(500);
executor.execute(new SleepTask(1000));
executor.shutdown();
assertTrue(awaitTerminationUninterruptibly(executor, Duration.ofMillis(LONG_DELAY_MS)));
assertTrue(executor.isTerminated());
assertInterrupted();
}

public void testTryAwaitTerminationUninterruptiblyDuration_failure() {
ExecutorService executor = newFixedThreadPool(1);
requestInterruptIn(500);
executor.execute(new SleepTask(10000));
executor.shutdown();
assertFalse(awaitTerminationUninterruptibly(executor, Duration.ofSeconds(1)));
assertFalse(executor.isTerminated());
assertInterrupted();
}

public void testTryAwaitTerminationUninterruptiblyLongTimeUnit_success() {
ExecutorService executor = newFixedThreadPool(1);
requestInterruptIn(500);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
package com.google.common.util.concurrent;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Internal.toNanosSaturated;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.base.Function;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.DoNotMock;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
Expand Down Expand Up @@ -254,6 +256,25 @@ public final <X extends Throwable> FluentFuture<V> catchingAsync(
return (FluentFuture<V>) Futures.catchingAsync(this, exceptionType, fallback, executor);
}

/**
* Returns a future that delegates to this future but will finish early (via a {@link
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified timeout expires.
* If the timeout expires, not only will the output future finish, but also the input future
* ({@code this}) will be cancelled and interrupted.
*
* @param timeout when to time out the future
* @param scheduledExecutor The executor service to enforce the timeout.
* @since NEXT (but since 28.0 in the JRE flavor)
*/
@J2ktIncompatible
@GwtIncompatible // ScheduledExecutorService
@SuppressWarnings("Java7ApiChecker")
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
public final FluentFuture<V> withTimeout(
Duration timeout, ScheduledExecutorService scheduledExecutor) {
return withTimeout(toNanosSaturated(timeout), TimeUnit.NANOSECONDS, scheduledExecutor);
}

/**
* Returns a future that delegates to this future but will finish early (via a {@link
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified timeout expires.
Expand Down
93 changes: 93 additions & 0 deletions android/guava/src/com/google/common/util/concurrent/Futures.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.util.concurrent.Internal.toNanosSaturated;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly;
import static java.util.Objects.requireNonNull;
Expand All @@ -34,6 +35,7 @@
import com.google.common.util.concurrent.internal.InternalFutures;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
Expand Down Expand Up @@ -219,6 +221,22 @@ private Futures() {}
return task;
}

/**
* Schedules {@code callable} on the specified {@code executor}, returning a {@code Future}.
*
* @throws RejectedExecutionException if the task cannot be scheduled for execution
* @since NEXT (but since 28.0 in the JRE flavor)
*/
@J2ktIncompatible
@GwtIncompatible // java.util.concurrent.ScheduledExecutorService
@SuppressWarnings("Java7ApiChecker")
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
// TODO(cpovirk): Return ListenableScheduledFuture?
public static <O extends @Nullable Object> ListenableFuture<O> scheduleAsync(
AsyncCallable<O> callable, Duration delay, ScheduledExecutorService executorService) {
return scheduleAsync(callable, toNanosSaturated(delay), TimeUnit.NANOSECONDS, executorService);
}

/**
* Schedules {@code callable} on the specified {@code executor}, returning a {@code Future}.
*
Expand Down Expand Up @@ -362,6 +380,27 @@ private Futures() {}
* <p>The delegate future is interrupted and cancelled if it times out.
*
* @param delegate The future to delegate to.
* @param time when to time out the future
* @param scheduledExecutor The executor service to enforce the timeout.
* @since NEXT (but since 28.0 in the JRE flavor)
*/
@J2ktIncompatible
@GwtIncompatible // java.util.concurrent.ScheduledExecutorService
@SuppressWarnings("Java7ApiChecker")
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
public static <V extends @Nullable Object> ListenableFuture<V> withTimeout(
ListenableFuture<V> delegate, Duration time, ScheduledExecutorService scheduledExecutor) {
return withTimeout(delegate, toNanosSaturated(time), TimeUnit.NANOSECONDS, scheduledExecutor);
}

/**
* Returns a future that delegates to another but will finish early (via a {@link
* TimeoutException} wrapped in an {@link ExecutionException}) if the specified duration expires.
*
* <p>The delegate future is interrupted and cancelled if it times out.
*
* @param delegate The future to delegate to.
* @param time when to time out the future
* @param unit the time unit of the time parameter
* @param scheduledExecutor The executor service to enforce the timeout.
* @since 19.0
Expand Down Expand Up @@ -1238,6 +1277,60 @@ public String toString() {
* @throws CancellationException if {@code get} throws a {@code CancellationException}
* @throws IllegalArgumentException if {@code exceptionClass} extends {@code RuntimeException} or
* does not have a suitable constructor
* @since NEXT (but since 28.0 in the JRE flavor)
*/
@CanIgnoreReturnValue
@J2ktIncompatible
@GwtIncompatible // reflection
@ParametricNullness
@SuppressWarnings("Java7ApiChecker")
@IgnoreJRERequirement // Users will use this only if they're already using Duration.
public static <V extends @Nullable Object, X extends Exception> V getChecked(
Future<V> future, Class<X> exceptionClass, Duration timeout) throws X {
return getChecked(future, exceptionClass, toNanosSaturated(timeout), TimeUnit.NANOSECONDS);
}

/**
* Returns the result of {@link Future#get(long, TimeUnit)}, converting most exceptions to a new
* instance of the given checked exception type. This reduces boilerplate for a common use of
* {@code Future} in which it is unnecessary to programmatically distinguish between exception
* types or to extract other information from the exception instance.
*
* <p>Exceptions from {@code Future.get} are treated as follows:
*
* <ul>
* <li>Any {@link ExecutionException} has its <i>cause</i> wrapped in an {@code X} if the cause
* is a checked exception, an {@link UncheckedExecutionException} if the cause is a {@code
* RuntimeException}, or an {@link ExecutionError} if the cause is an {@code Error}.
* <li>Any {@link InterruptedException} is wrapped in an {@code X} (after restoring the
* interrupt).
* <li>Any {@link TimeoutException} is wrapped in an {@code X}.
* <li>Any {@link CancellationException} is propagated untouched, as is any other {@link
* RuntimeException} (though {@code get} implementations are discouraged from throwing such
* exceptions).
* </ul>
*
* <p>The overall principle is to continue to treat every checked exception as a checked
* exception, every unchecked exception as an unchecked exception, and every error as an error. In
* addition, the cause of any {@code ExecutionException} is wrapped in order to ensure that the
* new stack trace matches that of the current thread.
*
* <p>Instances of {@code exceptionClass} are created by choosing an arbitrary public constructor
* that accepts zero or more arguments, all of type {@code String} or {@code Throwable}
* (preferring constructors with at least one {@code String}) and calling the constructor via
* reflection. If the exception did not already have a cause, one is set by calling {@link
* Throwable#initCause(Throwable)} on it. If no such constructor exists, an {@code
* IllegalArgumentException} is thrown.
*
* @throws X if {@code get} throws any checked exception except for an {@code ExecutionException}
* whose cause is not itself a checked exception
* @throws UncheckedExecutionException if {@code get} throws an {@code ExecutionException} with a
* {@code RuntimeException} as its cause
* @throws ExecutionError if {@code get} throws an {@code ExecutionException} with an {@code
* Error} as its cause
* @throws CancellationException if {@code get} throws a {@code CancellationException}
* @throws IllegalArgumentException if {@code exceptionClass} extends {@code RuntimeException} or
* does not have a suitable constructor
* @since 19.0 (in 10.0 as {@code get} and with different parameter order)
*/
@CanIgnoreReturnValue
Expand Down
Loading

0 comments on commit cdc2254

Please sign in to comment.