diff --git a/driver-core/src/main/com/mongodb/internal/connection/ConcurrentPool.java b/driver-core/src/main/com/mongodb/internal/connection/ConcurrentPool.java index 0158c2d9637..35661561704 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ConcurrentPool.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ConcurrentPool.java @@ -23,6 +23,8 @@ import com.mongodb.MongoTimeoutException; import com.mongodb.annotations.ThreadSafe; import com.mongodb.internal.VisibleForTesting; +import com.mongodb.internal.time.TimePoint; +import com.mongodb.internal.time.Timeout; import com.mongodb.lang.Nullable; import java.util.Deque; @@ -142,7 +144,7 @@ public T get() { * Gets an object from the pool. Blocks until an object is available, or the specified {@code timeout} expires, * or the pool is {@linkplain #close() closed}/{@linkplain #pause(Supplier) paused}. * - * @param timeout See {@link com.mongodb.internal.Timeout#startNow(long, TimeUnit)}. + * @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}. * @param timeUnit the time unit of the timeout * @return An object from the pool, or null if can't get one in the given waitTime * @throws MongoTimeoutException if the timeout has been exceeded @@ -226,7 +228,7 @@ private T createNewAndReleasePermitIfFailure() { } /** - * @param timeout See {@link com.mongodb.internal.Timeout#startNow(long, TimeUnit)}. + * @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}. */ @VisibleForTesting(otherwise = PRIVATE) boolean acquirePermit(final long timeout, final TimeUnit timeUnit) { @@ -386,7 +388,7 @@ boolean acquirePermitImmediateUnfair() { * This method also emulates the eager {@link InterruptedException} behavior of * {@link java.util.concurrent.Semaphore#tryAcquire(long, TimeUnit)}. * - * @param timeout See {@link com.mongodb.internal.Timeout#startNow(long, TimeUnit)}. + * @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}. */ boolean acquirePermit(final long timeout, final TimeUnit unit) throws MongoInterruptedException { long remainingNanos = unit.toNanos(timeout); diff --git a/driver-core/src/main/com/mongodb/internal/connection/ConnectionPool.java b/driver-core/src/main/com/mongodb/internal/connection/ConnectionPool.java index da2b0dcc1a0..5b7849f5a88 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ConnectionPool.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ConnectionPool.java @@ -20,6 +20,8 @@ import com.mongodb.annotations.ThreadSafe; import com.mongodb.connection.ConnectionPoolSettings; import com.mongodb.internal.async.SingleResultCallback; +import com.mongodb.internal.time.Timeout; +import com.mongodb.internal.time.TimePoint; import org.bson.types.ObjectId; import com.mongodb.lang.Nullable; @@ -38,7 +40,7 @@ interface ConnectionPool extends Closeable { /** * @param operationContext operation context - * @param timeout See {@link com.mongodb.internal.Timeout#startNow(long, TimeUnit)}. + * @param timeout See {@link Timeout#started(long, TimeUnit, TimePoint)}. * @throws MongoConnectionPoolClearedException If detects that the pool is {@linkplain #invalidate(Throwable) paused}. */ InternalConnection get(OperationContext operationContext, long timeout, TimeUnit timeUnit) throws MongoConnectionPoolClearedException; diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java index b20b1e7c57e..2a6d29b0b08 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java @@ -43,7 +43,8 @@ import com.mongodb.event.ConnectionPoolListener; import com.mongodb.event.ConnectionPoolReadyEvent; import com.mongodb.event.ConnectionReadyEvent; -import com.mongodb.internal.Timeout; +import com.mongodb.internal.time.TimePoint; +import com.mongodb.internal.time.Timeout; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.connection.SdamServerDescriptionManager.SdamIssue; @@ -1123,7 +1124,7 @@ void signalClosedOrPaused() { } /** - * @param timeoutNanos See {@link Timeout#startNow(long)}. + * @param timeoutNanos See {@link Timeout#started(long, TimePoint)}. * @return The remaining duration as per {@link Timeout#remainingOrInfinite(TimeUnit)} if waiting ended early either * spuriously or because of receiving a signal. */ diff --git a/driver-core/src/main/com/mongodb/internal/time/TimePoint.java b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java new file mode 100644 index 00000000000..78859802150 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java @@ -0,0 +1,123 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.internal.time; + +import com.mongodb.annotations.Immutable; +import com.mongodb.internal.VisibleForTesting; + +import java.time.Clock; +import java.time.Duration; + +import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; + +/** + * A value-based class + * representing a point on a timeline. The origin of this timeline has no known relation to the + * {@linkplain Clock#systemUTC() system clock}. The same timeline is used by all {@link TimePoint}s within the same process. + *

+ * Methods operating on a pair of {@link TimePoint}s, + * for example, {@link #durationSince(TimePoint)}, {@link #compareTo(TimePoint)}, + * or producing a point from another one, for example, {@link #add(Duration)}, + * work correctly only if the duration between the points is not greater than {@link Long#MAX_VALUE} nanoseconds, + * which is more than 292 years.

+ *

+ * This class is not part of the public API and may be removed or changed at any time.

+ */ +@Immutable +public final class TimePoint implements Comparable { + private final long nanos; + + private TimePoint(final long nanos) { + this.nanos = nanos; + } + + /** + * Returns the current {@link TimePoint}. + */ + public static TimePoint now() { + return at(System.nanoTime()); + } + + @VisibleForTesting(otherwise = PRIVATE) + static TimePoint at(final long nanos) { + return new TimePoint(nanos); + } + + /** + * The {@link Duration} between this {@link TimePoint} and {@code t}. + * A {@linkplain Duration#isNegative() negative} {@link Duration} means that + * this {@link TimePoint} is {@linkplain #compareTo(TimePoint) before} {@code t}. + * + * @see #elapsed() + */ + public Duration durationSince(final TimePoint t) { + return Duration.ofNanos(nanos - t.nanos); + } + + /** + * The {@link Duration} between {@link TimePoint#now()} and this {@link TimePoint}. + * This method is functionally equivalent to {@code TimePoint.now().durationSince(this)}. + * + * @see #durationSince(TimePoint) + */ + public Duration elapsed() { + return Duration.ofNanos(System.nanoTime() - nanos); + } + + /** + * Returns a {@link TimePoint} that is {@code duration} away from this one. + * + * @param duration A duration that may also be {@linkplain Duration#isNegative() negative}. + */ + public TimePoint add(final Duration duration) { + long durationNanos = duration.toNanos(); + return TimePoint.at(nanos + durationNanos); + } + + /** + * If this {@link TimePoint} is less/greater than {@code t}, then it is before/after {@code t}. + *

+ * {@inheritDoc}

+ */ + @Override + public int compareTo(final TimePoint t) { + return Long.signum(nanos - t.nanos); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TimePoint timePoint = (TimePoint) o; + return nanos == timePoint.nanos; + } + + @Override + public int hashCode() { + return Long.hashCode(nanos); + } + + @Override + public String toString() { + return "TimePoint{" + + "nanos=" + nanos + + '}'; + } +} diff --git a/driver-core/src/main/com/mongodb/internal/Timeout.java b/driver-core/src/main/com/mongodb/internal/time/Timeout.java similarity index 58% rename from driver-core/src/main/com/mongodb/internal/Timeout.java rename to driver-core/src/main/com/mongodb/internal/time/Timeout.java index f36eedd1d64..f0d4bbf3ea1 100644 --- a/driver-core/src/main/com/mongodb/internal/Timeout.java +++ b/driver-core/src/main/com/mongodb/internal/time/Timeout.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.mongodb.internal; +package com.mongodb.internal.time; import com.mongodb.annotations.Immutable; +import com.mongodb.internal.VisibleForTesting; +import com.mongodb.lang.Nullable; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -29,125 +31,98 @@ /** * A value-based class - * useful for tracking timeouts. - * - *

This class is not part of the public API and may be removed or changed at any time

+ * for tracking timeouts. + *

+ * This class is not part of the public API and may be removed or changed at any time.

*/ @Immutable public final class Timeout { - private static final Timeout INFINITE = new Timeout(-1, 0); - private static final Timeout IMMEDIATE = new Timeout(0, 0); + private static final Timeout INFINITE = new Timeout(-1, null); + private static final Timeout IMMEDIATE = new Timeout(0, null); private final long durationNanos; - private final long startNanos; + /** + * {@code null} iff {@code this} is {@linkplain #isInfinite() infinite} or {@linkplain #isImmediate() immediate}. + */ + @Nullable + private final TimePoint start; - private Timeout(final long durationNanos, final long startNanos) { + private Timeout(final long durationNanos, @Nullable final TimePoint start) { this.durationNanos = durationNanos; - this.startNanos = startNanos; + this.start = start; } /** - * Converts the specified {@code duration} from {@code unit}s to {@link TimeUnit#NANOSECONDS} via {@link TimeUnit#toNanos(long)} - * and then acts identically to {@link #startNow(long)}. + * Converts the specified {@code duration} from {@code unit}s to {@link TimeUnit#NANOSECONDS} + * as specified by {@link TimeUnit#toNanos(long)} and then acts identically to {@link #started(long, TimePoint)}. *

* Note that the contract of this method is also used in some places to specify the behavior of methods that accept * {@code (long timeout, TimeUnit unit)}, e.g., {@link com.mongodb.internal.connection.ConcurrentPool#get(long, TimeUnit)}, - * so it cannot be changed without updating those methods. - * @see #startNow(long) + * so it cannot be changed without updating those methods.

*/ - public static Timeout startNow(final long duration, final TimeUnit unit) { - assertNotNull(unit); - return startNow(unit.toNanos(duration)); + public static Timeout started(final long duration, final TimeUnit unit, final TimePoint at) { + return started(unit.toNanos(duration), assertNotNull(at)); } /** * Returns an {@linkplain #isInfinite() infinite} timeout if {@code durationNanos} is either negative * or is equal to {@link Long#MAX_VALUE}, * an {@linkplain #isImmediate() immediate} timeout if {@code durationNanos} is 0, - * otherwise an object that represents the specified {@code durationNanos}. + * otherwise a timeout of {@code durationNanos}. *

* Note that the contract of this method is also used in some places to specify the behavior of methods that accept * {@code (long timeout, TimeUnit unit)}, e.g., {@link com.mongodb.internal.connection.ConcurrentPool#get(long, TimeUnit)}, - * so it cannot be changed without updating those methods. + * so it cannot be changed without updating those methods.

*/ - public static Timeout startNow(final long durationNanos) { + public static Timeout started(final long durationNanos, final TimePoint at) { if (durationNanos < 0 || durationNanos == Long.MAX_VALUE) { return infinite(); } else if (durationNanos == 0) { return immediate(); } else { - return new Timeout(durationNanos, System.nanoTime()); + return new Timeout(durationNanos, assertNotNull(at)); } } /** - * @see #startNow(long) + * This method acts identically to {@link #started(long, TimeUnit, TimePoint)} + * with the {@linkplain TimePoint#now() current} {@link TimePoint} passed to it. */ - public static Timeout infinite() { - return INFINITE; + public static Timeout startNow(final long duration, final TimeUnit unit) { + return started(duration, unit, TimePoint.now()); } /** - * @see #startNow(long) + * This method acts identically to {@link #started(long, TimePoint)} + * with the {@linkplain TimePoint#now() current} {@link TimePoint} passed to it. */ - public static Timeout immediate() { - return IMMEDIATE; + public static Timeout startNow(final long durationNanos) { + return started(durationNanos, TimePoint.now()); } /** - * Must not be called on {@linkplain #isInfinite() infinite} or {@linkplain #isImmediate() immediate} timeouts. - *

- * Returns {@code currentNanos} - {@link #startNanos}: - *

- *
- * (*) n is positive and odd. + * @see #started(long, TimePoint) */ - private long elapsedNanos(final long currentNanos) { - assertFalse(isInfinite() || isImmediate()); - return currentNanos - startNanos; + public static Timeout infinite() { + return INFINITE; + } + + /** + * @see #started(long, TimePoint) + */ + public static Timeout immediate() { + return IMMEDIATE; } /** * Returns 0 or a positive value. * 0 means that the timeout has expired. - *

- * Must not be called on {@linkplain #isInfinite() infinite} timeouts. + * + * @throws AssertionError If the timeout is {@linkplain #isInfinite() infinite} or {@linkplain #isImmediate() immediate}. */ @VisibleForTesting(otherwise = PRIVATE) - long remainingNanos(final long currentNanos) { - assertFalse(isInfinite() || isImmediate()); - long elapsedNanos = elapsedNanos(currentNanos); - return elapsedNanos < 0 ? 0 : Math.max(0, durationNanos - elapsedNanos); + long remainingNanos(final TimePoint now) { + return Math.max(0, durationNanos - now.durationSince(assertNotNull(start)).toNanos()); } /** @@ -155,22 +130,21 @@ long remainingNanos(final long currentNanos) { * Use {@link #expired(long)} to check if the returned value signifies that a timeout is expired. * * @param unit If not {@link TimeUnit#NANOSECONDS}, then coarsening conversion is done that may result in returning a value - * that represents a longer time duration than is actually remaining (this is done to prevent treating a timeout as - * {@linkplain #expired(long) expired} when it is not). Consequently, one should specify {@code unit} as small as - * practically possible. Such rounding up happens if and only if the remaining time cannot be - * represented exactly as an integral number of the {@code unit}s specified. It may result in - * {@link #expired()} returning {@code true} and after that (in the happens-before order) - * {@link #expired(long) expired}{@code (}{@link #remaining(TimeUnit) remaining(...)}{@code )} - * returning {@code false}. If such a discrepancy is observed, - * the result of the {@link #expired()} method should be preferred. + * that represents a longer time duration than is actually remaining (this is done to prevent treating a timeout as + * {@linkplain #expired(long) expired} when it is not). Consequently, one should specify {@code unit} as small as + * practically possible. Such rounding up happens if and only if the remaining time cannot be + * represented exactly as an integral number of the {@code unit}s specified. It may result in + * {@link #expired()} returning {@code true} and after that (in the happens-before order) + * {@link #expired(long) expired}{@code (}{@link #remaining(TimeUnit) remaining(...)}{@code )} + * returning {@code false}. If such a discrepancy is observed, + * the result of the {@link #expired()} method should be preferred. * * @throws AssertionError If the timeout is {@linkplain #isInfinite() infinite}. * @see #remainingOrInfinite(TimeUnit) */ public long remaining(final TimeUnit unit) { - assertNotNull(unit); assertFalse(isInfinite()); - return isImmediate() ? 0 : convertRoundUp(remainingNanos(System.nanoTime()), unit); + return isImmediate() ? 0 : convertRoundUp(remainingNanos(TimePoint.now()), unit); } /** @@ -181,7 +155,6 @@ public long remaining(final TimeUnit unit) { * @see #remaining(TimeUnit) */ public long remainingOrInfinite(final TimeUnit unit) { - assertNotNull(unit); return isInfinite() ? -1 : remaining(unit); } @@ -226,12 +199,12 @@ public boolean equals(final Object o) { return false; } Timeout other = (Timeout) o; - return durationNanos == other.durationNanos && startNanos == other.startNanos; + return durationNanos == other.durationNanos && Objects.equals(start, other.start()); } @Override public int hashCode() { - return Objects.hash(durationNanos, startNanos); + return Objects.hash(durationNanos, start); } /** @@ -243,7 +216,7 @@ public int hashCode() { public String toString() { return "Timeout{" + "durationNanos=" + durationNanos - + ", startNanos=" + startNanos + + ", start=" + start + '}'; } @@ -268,8 +241,9 @@ long durationNanos() { } @VisibleForTesting(otherwise = PRIVATE) - long startNanos() { - return startNanos; + @Nullable + TimePoint start() { + return start; } @VisibleForTesting(otherwise = PRIVATE) diff --git a/driver-core/src/main/com/mongodb/internal/time/package-info.java b/driver-core/src/main/com/mongodb/internal/time/package-info.java new file mode 100644 index 00000000000..3b3ee457517 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/time/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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. + */ + +/** + * This package contains program elements for working with time. + */ +@NonNullApi +package com.mongodb.internal.time; + +import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/DefaultConnectionPoolTest.java b/driver-core/src/test/functional/com/mongodb/internal/connection/DefaultConnectionPoolTest.java index 91a08f63b22..919e0b130a8 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/connection/DefaultConnectionPoolTest.java +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/DefaultConnectionPoolTest.java @@ -26,7 +26,7 @@ import com.mongodb.connection.ConnectionPoolSettings; import com.mongodb.connection.ServerId; import com.mongodb.event.ConnectionCreatedEvent; -import com.mongodb.internal.Timeout; +import com.mongodb.internal.time.Timeout; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.inject.EmptyProvider; import com.mongodb.internal.inject.OptionalProvider; diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/TimePointTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/TimePointTest.java new file mode 100644 index 00000000000..4f331d208a2 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/time/TimePointTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.internal.time; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.util.Collection; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +final class TimePointTest { + @Test + void now() { + TimePoint timePointLowerBound = TimePoint.at(System.nanoTime()); + TimePoint timePoint = TimePoint.now(); + TimePoint timePointUpperBound = TimePoint.at(System.nanoTime()); + assertTrue(timePoint.compareTo(timePointLowerBound) >= 0, "the point is too early"); + assertTrue(timePoint.compareTo(timePointUpperBound) <= 0, "the point is too late"); + } + + @Test + void elapsed() { + TimePoint timePoint = TimePoint.now(); + Duration elapsedLowerBound = TimePoint.now().durationSince(timePoint); + Duration elapsed = timePoint.elapsed(); + Duration elapsedUpperBound = TimePoint.now().durationSince(timePoint); + assertTrue(elapsed.compareTo(elapsedLowerBound) >= 0, "the elapsed is too low"); + assertTrue(elapsed.compareTo(elapsedUpperBound) <= 0, "the elapsed is too high"); + } + + @ParameterizedTest + @MethodSource("earlierNanosAndNanosArguments") + void durationSince(final long earlierNanos, final long nanos) { + Duration expectedDuration = Duration.ofNanos(nanos - earlierNanos); + TimePoint earlierTimePoint = TimePoint.at(earlierNanos); + TimePoint timePoint = TimePoint.at(nanos); + assertFalse(expectedDuration.isNegative()); + assertEquals(expectedDuration, timePoint.durationSince(earlierTimePoint)); + assertEquals(expectedDuration.negated(), earlierTimePoint.durationSince(timePoint)); + } + + @ParameterizedTest + @MethodSource("earlierNanosAndNanosArguments") + void compareTo(final long earlierNanos, final long nanos) { + TimePoint earlierTimePoint = TimePoint.at(earlierNanos); + TimePoint timePoint = TimePoint.at(nanos); + if (earlierNanos == nanos) { + assertEquals(0, earlierTimePoint.compareTo(timePoint)); + assertEquals(0, timePoint.compareTo(earlierTimePoint)); + assertEquals(earlierTimePoint, timePoint); + assertEquals(timePoint, earlierTimePoint); + } else { + assertTrue(earlierTimePoint.compareTo(timePoint) < 0); + assertTrue(timePoint.compareTo(earlierTimePoint) > 0); + assertNotEquals(earlierTimePoint, timePoint); + assertNotEquals(timePoint, earlierTimePoint); + } + } + + private static Stream earlierNanosAndNanosArguments() { + Collection earlierNanos = asList(Long.MIN_VALUE, Long.MIN_VALUE / 2, 0L, Long.MAX_VALUE / 2, Long.MAX_VALUE); + Collection durationsInNanos = asList(0L, 1L, Long.MAX_VALUE / 2, Long.MAX_VALUE); + return earlierNanos.stream() + .flatMap(earlier -> durationsInNanos.stream() + .map(durationNanos -> arguments(earlier, earlier + durationNanos))); + } + + @ParameterizedTest + @MethodSource("nanosAndDurationsArguments") + void add(final long nanos, final Duration duration) { + TimePoint timePoint = TimePoint.at(nanos); + assertEquals(duration, timePoint.add(duration).durationSince(timePoint)); + } + + private static Stream nanosAndDurationsArguments() { + Collection nanos = asList(Long.MIN_VALUE, Long.MIN_VALUE / 2, 0L, Long.MAX_VALUE / 2, Long.MAX_VALUE); + Collection durationsInNanos = asList( + // Using `-Long.MAX_VALUE` results in `ArithmeticException` in OpenJDK JDK 8 because of https://bugs.openjdk.org/browse/JDK-8146747. + // This was fixed in OpenJDK JDK 9. + -Long.MAX_VALUE / 2, 0L, Long.MAX_VALUE / 2, Long.MAX_VALUE); + return nanos.stream() + .flatMap(nano -> durationsInNanos.stream() + .map(durationNanos -> arguments(nano, Duration.ofNanos(durationNanos)))); + } + + private TimePointTest() { + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/TimeoutTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/TimeoutTest.java similarity index 66% rename from driver-core/src/test/unit/com/mongodb/internal/TimeoutTest.java rename to driver-core/src/test/unit/com/mongodb/internal/time/TimeoutTest.java index ad96096a0dc..03df92771ac 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/TimeoutTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/time/TimeoutTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.mongodb.internal; +package com.mongodb.internal.time; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -21,16 +21,17 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; final class TimeoutTest { @Test @@ -38,7 +39,8 @@ void isInfinite() { assertAll( () -> assertTrue(Timeout.infinite().isInfinite()), () -> assertFalse(Timeout.immediate().isInfinite()), - () -> assertFalse(Timeout.startNow(1).isInfinite())); + () -> assertFalse(Timeout.startNow(1).isInfinite()), + () -> assertFalse(Timeout.started(1, TimePoint.now()).isInfinite())); } @Test @@ -46,7 +48,24 @@ void isImmediate() { assertAll( () -> assertTrue(Timeout.immediate().isImmediate()), () -> assertFalse(Timeout.infinite().isImmediate()), - () -> assertFalse(Timeout.startNow(1).isImmediate())); + () -> assertFalse(Timeout.startNow(1).isImmediate()), + () -> assertFalse(Timeout.started(1, TimePoint.now()).isImmediate())); + } + + @Test + void started() { + TimePoint timePoint = TimePoint.now(); + assertAll( + () -> assertEquals(Timeout.infinite(), Timeout.started(-1, timePoint)), + () -> assertEquals(Timeout.immediate(), Timeout.started(0, timePoint)), + () -> assertNotEquals(Timeout.infinite(), Timeout.started(1, timePoint)), + () -> assertNotEquals(Timeout.immediate(), Timeout.started(1, timePoint)), + () -> assertEquals(1, Timeout.started(1, timePoint).durationNanos()), + () -> assertEquals(timePoint, Timeout.started(1, timePoint).start()), + () -> assertNotEquals(Timeout.infinite(), Timeout.started(Long.MAX_VALUE - 1, timePoint)), + () -> assertEquals(Long.MAX_VALUE - 1, Timeout.started(Long.MAX_VALUE - 1, timePoint).durationNanos()), + () -> assertEquals(timePoint, Timeout.started(Long.MAX_VALUE - 1, timePoint).start()), + () -> assertEquals(Timeout.infinite(), Timeout.started(Long.MAX_VALUE, timePoint))); } @Test @@ -56,10 +75,25 @@ void startNow() { () -> assertEquals(Timeout.immediate(), Timeout.startNow(0)), () -> assertNotEquals(Timeout.infinite(), Timeout.startNow(1)), () -> assertNotEquals(Timeout.immediate(), Timeout.startNow(1)), + () -> assertEquals(1, Timeout.startNow(1).durationNanos()), () -> assertNotEquals(Timeout.infinite(), Timeout.startNow(Long.MAX_VALUE - 1)), + () -> assertEquals(Long.MAX_VALUE - 1, Timeout.startNow(Long.MAX_VALUE - 1).durationNanos()), () -> assertEquals(Timeout.infinite(), Timeout.startNow(Long.MAX_VALUE))); } + @ParameterizedTest + @MethodSource("durationArguments") + void startedConvertsUnits(final long duration, final TimeUnit unit) { + TimePoint timePoint = TimePoint.now(); + if (duration < 0) { + assertTrue(Timeout.started(duration, unit, timePoint).isInfinite()); + } else if (duration == 0) { + assertTrue(Timeout.started(duration, unit, timePoint).isImmediate()); + } else { + assertEquals(unit.toNanos(duration), Timeout.started(duration, unit, timePoint).durationNanos()); + } + } + @ParameterizedTest @MethodSource("durationArguments") void startNowConvertsUnits(final long duration, final TimeUnit unit) { @@ -81,7 +115,7 @@ private static Stream durationArguments() { } @Test - void remainingNanosTrivialCases() { + void remainingTrivialCases() { assertAll( () -> assertThrows(AssertionError.class, () -> Timeout.infinite().remaining(NANOSECONDS)), () -> assertTrue(Timeout.infinite().remainingOrInfinite(NANOSECONDS) < 0), @@ -92,16 +126,12 @@ void remainingNanosTrivialCases() { @ParameterizedTest @ValueSource(longs = {1, 7, Long.MAX_VALUE / 2, Long.MAX_VALUE - 1}) void remainingNanos(final long durationNanos) { - Timeout timeout = Timeout.startNow(durationNanos); - long startNanos = timeout.startNanos(); - assertEquals(durationNanos, timeout.remainingNanos(startNanos)); - assertEquals(Math.max(0, durationNanos - 1), timeout.remainingNanos(startNanos + 1)); - assertEquals(0, timeout.remainingNanos(startNanos + durationNanos)); - assertEquals(0, timeout.remainingNanos(startNanos + durationNanos + 1)); - assertEquals(0, timeout.remainingNanos(startNanos + Long.MAX_VALUE)); - assertEquals(0, timeout.remainingNanos(startNanos + Long.MAX_VALUE + 1)); - assertEquals(0, timeout.remainingNanos(startNanos + Long.MAX_VALUE + Long.MAX_VALUE)); - + TimePoint start = TimePoint.now(); + Timeout timeout = Timeout.started(durationNanos, start); + assertEquals(durationNanos, timeout.remainingNanos(start)); + assertEquals(Math.max(0, durationNanos - 1), timeout.remainingNanos(start.add(Duration.ofNanos(1)))); + assertEquals(0, timeout.remainingNanos(start.add(Duration.ofNanos(durationNanos)))); + assertEquals(0, timeout.remainingNanos(start.add(Duration.ofNanos(durationNanos + 1)))); } @Test @@ -130,21 +160,18 @@ void convertRoundUp() { @ParameterizedTest @ValueSource(longs = {1, 7, 10, 100, 1000}) - void usesRealClock(final long durationNanos) { - long startNanosLowerBound = System.nanoTime(); - Timeout timeout = Timeout.startNow(durationNanos); - long startNanosUpperBound = System.nanoTime(); - assertTrue(timeout.startNanos() - startNanosLowerBound >= 0, "started too early"); - assertTrue(timeout.startNanos() - startNanosUpperBound <= 0, "started too late"); + void remaining(final long durationNanos) { + TimePoint start = TimePoint.now(); + Timeout timeout = Timeout.started(durationNanos, start); while (!timeout.expired()) { - long remainingNanosUpperBound = Math.max(0, durationNanos - (System.nanoTime() - startNanosUpperBound)); + long remainingNanosUpperBound = Math.max(0, durationNanos - TimePoint.now().durationSince(start).toNanos()); long remainingNanos = timeout.remaining(NANOSECONDS); - long remainingNanosLowerBound = Math.max(0, durationNanos - (System.nanoTime() - startNanosLowerBound)); + long remainingNanosLowerBound = Math.max(0, durationNanos - TimePoint.now().durationSince(start).toNanos()); assertTrue(remainingNanos >= remainingNanosLowerBound, "remaining nanos is too low"); assertTrue(remainingNanos <= remainingNanosUpperBound, "remaining nanos is too high"); Thread.yield(); } - assertTrue(System.nanoTime() - startNanosLowerBound >= durationNanos, "expired too early"); + assertTrue(TimePoint.now().durationSince(start).toNanos() >= durationNanos, "expired too early"); } private TimeoutTest() { diff --git a/driver-sync/src/test/functional/com/mongodb/client/ServerDiscoveryAndMonitoringProseTests.java b/driver-sync/src/test/functional/com/mongodb/client/ServerDiscoveryAndMonitoringProseTests.java index 0915486dc41..1dba06b38df 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/ServerDiscoveryAndMonitoringProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/client/ServerDiscoveryAndMonitoringProseTests.java @@ -27,7 +27,7 @@ import com.mongodb.event.ServerHeartbeatSucceededEvent; import com.mongodb.event.ServerListener; import com.mongodb.event.ServerMonitorListener; -import com.mongodb.internal.Timeout; +import com.mongodb.internal.time.Timeout; import com.mongodb.internal.diagnostics.logging.Logger; import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.lang.Nullable;