diff --git a/api/src/main/java/io/grpc/CallOptions.java b/api/src/main/java/io/grpc/CallOptions.java index 25c4df386a1..a1b8984c48b 100644 --- a/api/src/main/java/io/grpc/CallOptions.java +++ b/api/src/main/java/io/grpc/CallOptions.java @@ -17,9 +17,11 @@ package io.grpc; import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.TimeUtils.convertToNanos; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -176,6 +178,11 @@ public CallOptions withDeadlineAfter(long duration, TimeUnit unit) { return withDeadline(Deadline.after(duration, unit)); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public CallOptions withDeadlineAfter(Duration duration) { + return withDeadlineAfter(convertToNanos(duration), TimeUnit.NANOSECONDS); + } + /** * Returns the deadline or {@code null} if the deadline is not set. */ diff --git a/api/src/main/java/io/grpc/InternalTimeUtils.java b/api/src/main/java/io/grpc/InternalTimeUtils.java new file mode 100644 index 00000000000..ef8022f53c5 --- /dev/null +++ b/api/src/main/java/io/grpc/InternalTimeUtils.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.time.Duration; + +@Internal +public final class InternalTimeUtils { + public static long convert(Duration duration) { + return TimeUtils.convertToNanos(duration); + } +} diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 6d74006b396..52d37f8677d 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -212,7 +212,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { * * @since 1.21.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771") + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") public static final class ResolvedAddresses { private final List addresses; @NameResolver.ResolutionResultAttr diff --git a/api/src/main/java/io/grpc/SynchronizationContext.java b/api/src/main/java/io/grpc/SynchronizationContext.java index 5a7677ac15f..94916a1b473 100644 --- a/api/src/main/java/io/grpc/SynchronizationContext.java +++ b/api/src/main/java/io/grpc/SynchronizationContext.java @@ -18,8 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static io.grpc.TimeUtils.convertToNanos; import java.lang.Thread.UncaughtExceptionHandler; +import java.time.Duration; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; @@ -162,6 +164,12 @@ public String toString() { return new ScheduledHandle(runnable, future); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final ScheduledHandle schedule( + final Runnable task, Duration delay, ScheduledExecutorService timerService) { + return schedule(task, convertToNanos(delay), TimeUnit.NANOSECONDS, timerService); + } + /** * Schedules a task to be added and run via {@link #execute} after an initial delay and then * repeated after the delay until cancelled. @@ -193,6 +201,14 @@ public String toString() { return new ScheduledHandle(runnable, future); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final ScheduledHandle scheduleWithFixedDelay( + final Runnable task, Duration initialDelay, Duration delay, + ScheduledExecutorService timerService) { + return scheduleWithFixedDelay(task, convertToNanos(initialDelay), convertToNanos(delay), + TimeUnit.NANOSECONDS, timerService); + } + private static class ManagedRunnable implements Runnable { final Runnable task; @@ -246,4 +262,4 @@ public boolean isPending() { return !(runnable.hasStarted || runnable.isCancelled); } } -} +} \ No newline at end of file diff --git a/api/src/main/java/io/grpc/TimeUtils.java b/api/src/main/java/io/grpc/TimeUtils.java new file mode 100644 index 00000000000..c3031f13d94 --- /dev/null +++ b/api/src/main/java/io/grpc/TimeUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.time.Duration; + +final class TimeUtils { + private TimeUtils() {} + + static long convertToNanos(Duration duration) { + try { + return duration.toNanos(); + } catch (ArithmeticException tooBig) { + return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE; + } + } +} diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java index cc90a9799d7..d74c74ccd66 100644 --- a/api/src/test/java/io/grpc/CallOptionsTest.java +++ b/api/src/test/java/io/grpc/CallOptionsTest.java @@ -32,6 +32,7 @@ import com.google.common.base.Objects; import io.grpc.ClientStreamTracer.StreamInfo; import io.grpc.internal.SerializingExecutor; +import java.time.Duration; import java.util.concurrent.Executor; import org.junit.Test; import org.junit.runner.RunWith; @@ -150,6 +151,14 @@ public void withDeadlineAfter() { assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); } + @Test + public void withDeadlineAfterDuration() { + Deadline actual = CallOptions.DEFAULT.withDeadlineAfter(Duration.ofMinutes(1L)).getDeadline(); + Deadline expected = Deadline.after(1, MINUTES); + + assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); + } + @Test public void toStringMatches_noDeadline_default() { String actual = allSet diff --git a/api/src/test/java/io/grpc/SynchronizationContextTest.java b/api/src/test/java/io/grpc/SynchronizationContextTest.java index 3d5e7fa42b9..f0797df227e 100644 --- a/api/src/test/java/io/grpc/SynchronizationContextTest.java +++ b/api/src/test/java/io/grpc/SynchronizationContextTest.java @@ -27,6 +27,7 @@ import com.google.common.util.concurrent.testing.TestingExecutors; import io.grpc.SynchronizationContext.ScheduledHandle; +import java.time.Duration; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; @@ -72,7 +73,7 @@ public void uncaughtException(Thread t, Throwable e) { @Mock private Runnable task3; - + @After public void tearDown() { assertThat(uncaughtErrors).isEmpty(); } @@ -246,6 +247,41 @@ public void schedule() { verify(task1).run(); } + @Test + public void scheduleDuration() { + MockScheduledExecutorService executorService = new MockScheduledExecutorService(); + ScheduledHandle handle = + syncContext.schedule(task1, Duration.ofSeconds(10), executorService); + + assertThat(executorService.delay) + .isEqualTo(executorService.unit.convert(10, TimeUnit.SECONDS)); + assertThat(handle.isPending()).isTrue(); + verify(task1, never()).run(); + + executorService.command.run(); + + assertThat(handle.isPending()).isFalse(); + verify(task1).run(); + } + + @Test + public void scheduleWithFixedDelayDuration() { + MockScheduledExecutorService executorService = new MockScheduledExecutorService(); + ScheduledHandle handle = + syncContext.scheduleWithFixedDelay(task1, Duration.ofSeconds(10), + Duration.ofSeconds(10), executorService); + + assertThat(executorService.delay) + .isEqualTo(executorService.unit.convert(10, TimeUnit.SECONDS)); + assertThat(handle.isPending()).isTrue(); + verify(task1, never()).run(); + + executorService.command.run(); + + assertThat(handle.isPending()).isFalse(); + verify(task1).run(); + } + @Test public void scheduleDueImmediately() { MockScheduledExecutorService executorService = new MockScheduledExecutorService(); @@ -357,5 +393,13 @@ static class MockScheduledExecutorService extends ForwardingScheduledExecutorSer this.unit = unit; return future = super.schedule(command, delay, unit); } + + @Override public ScheduledFuture scheduleWithFixedDelay(Runnable command, long intialDelay, + long delay, TimeUnit unit) { + this.command = command; + this.delay = delay; + this.unit = unit; + return future = super.scheduleWithFixedDelay(command, intialDelay, delay, unit); + } } -} +} \ No newline at end of file diff --git a/api/src/test/java/io/grpc/TimeUtilsTest.java b/api/src/test/java/io/grpc/TimeUtilsTest.java new file mode 100644 index 00000000000..4faaa9cbf6d --- /dev/null +++ b/api/src/test/java/io/grpc/TimeUtilsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import static org.junit.Assert.assertEquals; + +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TimeUtils}. */ +@RunWith(JUnit4.class) +public class TimeUtilsTest { + + @Test + public void testConvertNormalDuration() { + Duration duration = Duration.ofSeconds(10); + long expected = 10 * 1_000_000_000L; + + assertEquals(expected, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertNegativeDuration() { + Duration duration = Duration.ofSeconds(-3); + long expected = -3 * 1_000_000_000L; + + assertEquals(expected, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertTooLargeDuration() { + Duration duration = Duration.ofSeconds(Long.MAX_VALUE / 1_000_000_000L + 1); + + assertEquals(Long.MAX_VALUE, TimeUtils.convertToNanos(duration)); + } + + @Test + public void testConvertTooLargeNegativeDuration() { + Duration duration = Duration.ofSeconds(Long.MIN_VALUE / 1_000_000_000L - 1); + + assertEquals(Long.MIN_VALUE, TimeUtils.convertToNanos(duration)); + } +} \ No newline at end of file diff --git a/stub/src/main/java/io/grpc/stub/AbstractStub.java b/stub/src/main/java/io/grpc/stub/AbstractStub.java index 0b6f86f2acf..06dd55ff466 100644 --- a/stub/src/main/java/io/grpc/stub/AbstractStub.java +++ b/stub/src/main/java/io/grpc/stub/AbstractStub.java @@ -17,6 +17,7 @@ package io.grpc.stub; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.InternalTimeUtils.convert; import io.grpc.CallCredentials; import io.grpc.CallOptions; @@ -26,6 +27,7 @@ import io.grpc.Deadline; import io.grpc.ExperimentalApi; import io.grpc.ManagedChannelBuilder; +import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.annotation.CheckReturnValue; @@ -149,6 +151,11 @@ public final S withDeadlineAfter(long duration, TimeUnit unit) { return build(channel, callOptions.withDeadlineAfter(duration, unit)); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657") + public final S withDeadlineAfter(Duration duration) { + return withDeadlineAfter(convert(duration), TimeUnit.NANOSECONDS); + } + /** * Returns a new stub with the given executor that is to be used instead of the default one * specified with {@link ManagedChannelBuilder#executor}. Note that setting this option may not diff --git a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java index 9006b8679e4..a167c735160 100644 --- a/stub/src/test/java/io/grpc/stub/AbstractStubTest.java +++ b/stub/src/test/java/io/grpc/stub/AbstractStubTest.java @@ -16,12 +16,18 @@ package io.grpc.stub; +import static com.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.testing.DeadlineSubject.deadline; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; import io.grpc.CallOptions; import io.grpc.Channel; +import io.grpc.Deadline; import io.grpc.stub.AbstractStub.StubFactory; import io.grpc.stub.AbstractStubTest.NoopStub; +import java.time.Duration; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,8 +53,22 @@ public NoopStub newStub(Channel channel, CallOptions callOptions) { .isNull(); } - class NoopStub extends AbstractStub { + @Test + public void testDuration() { + NoopStub stub = NoopStub.newStub(new StubFactory() { + @Override + public NoopStub newStub(Channel channel, CallOptions callOptions) { + return create(channel, callOptions); + } + }, channel, CallOptions.DEFAULT); + NoopStub stubInstance = stub.withDeadlineAfter(Duration.ofMinutes(1L)); + Deadline actual = stubInstance.getCallOptions().getDeadline(); + Deadline expected = Deadline.after(1, MINUTES); + assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected); + } + + class NoopStub extends AbstractStub { NoopStub(Channel channel, CallOptions options) { super(channel, options); }