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+ * {@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}: - *
- * 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