Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Timeout implementation with interface, introduce StartTime interface #1194

Merged
merged 3 commits into from
Sep 22, 2023

Conversation

katcharov
Copy link
Contributor

@katcharov katcharov requested a review from rozza September 11, 2023 21:04
Copy link

@orca-security-us orca-security-us bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca

Comment on lines 1087 to 1096
}
long remainingNanos = timeout.remainingOrInfinite(NANOSECONDS);
while (permits == 0
// the absence of short-circuiting is of importance
& !stateAndGeneration.throwIfClosedOrPaused()
& (availableConnection = tryGetAvailable ? tryGetAvailableConnection() : null) == null) {
if (Timeout.expired(remainingNanos)) {
throw createTimeoutException(timeout);
if (timeout.hasExpired()) {
throw createTimeoutException(startTime);
}
remainingNanos = awaitNanos(permitAvailableOrHandedOverOrClosedOrPausedCondition, remainingNanos);
timeout.awaitOn(permitAvailableOrHandedOverOrClosedOrPausedCondition);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the timeout is now checked directly, rather than the value from await.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

* @return The remaining duration as per {@link Timeout#remainingOrInfinite(TimeUnit)} if waiting ended early either
* spuriously or because of receiving a signal.
*/
private long awaitNanos(final Condition condition, final long timeoutNanos) throws MongoInterruptedException {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been moved into the Deadline.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

/**
* @see TimePoint
*/
public interface Deadline {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the Deadline and StartPoint interfaces on TimePoint. These hide TimePoint methods that are not relevant or used, and also make it clear what "type of" TimePoint we are using.

Comment on lines +97 to +98
* The number of whole time units that remain until this TimePoint
* {@link #hasExpired()}. This should not be used to check for expiry,
* but can be used to supply a remaining value, in the finest-grained
* TimeUnit available, to some method that may time out.
* This method must not be used with infinite TimePoints.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the behaviour of the remaining method.

Previously, if there were 100ns remaining, and you asked how many hours were remaining, it would return 1 hour, because returning 0 might suggest that the deadline is expired. However, this does not match the behaviour of Duration, and I do not think rounding up is correct, for the expected usage. For example, I might get the remaining time in hours because that is the granularity of some method that I will invoke, and get back 1, and wait on the method for the next hour, which is not correct. As far as "hours" go, the deadline is indeed expired, and I do not think we should undertake operations that might (because of granularity differences) exceed the timeout. The same logic applies to milliseconds, though less intuitively.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spoke with @stIncMale about this, and he explained that the existing rounding-up behaviour is generally consistent with the timeout interpretation elsewhere in Java, of "try at least this long and then time out as fast as possible". I agree this is not an incorrect interpretation, but we also agree that the appropriate way to address this particular issue is to use nanoseconds (as Java SE does), rather than rounding the value up and down to some granularity. I won't address this in this PR (that is, refactoring our existing methods). I will leave this rounding-up interpretation, since it is consistent with how "elapsed" works, and the inconsistency with Java SE is not crucial (and should ideally be addressed in a different way regardless).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Set<Class<?>> encountered = new HashSet<>();
while (true) {
Object element;
if (timeout.isImmediate()) {
if (deadline.hasExpired()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the concept of "immediate" timepoints: they are just expired.

Comment on lines 36 to 51
/**
* Returns an {@linkplain Deadline#isInfinite() infinite} deadline if
* {@code timeoutValue} is negative, an expired deadline if
* {@code timeoutValue} is 0, otherwise a deadline in {@code durationNanos}.
* <p>
* Note that some code might ignore a deadline, and attempt to perform
* the operation in question at least once.</p>
* <p>
* 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.</p>
*
* @see TimePoint#fromNowOrInfiniteIfNegative(long, TimeUnit)
*/
Deadline fromNowOrInfiniteIfNegative(long timeoutValue, TimeUnit timeUnit);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think perhaps this method should not be added, but I have included it because otherwise, it seems I would need to change existing locations that take long/TimeUnit in this PR. I have at least made the name long and noticeable.

@katcharov katcharov force-pushed the JAVA-5112 branch 2 times, most recently from c0e5a15 to 65ceb29 Compare September 12, 2023 20:55
Copy link
Member

@rozza rozza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good - would be worthwhile having @stIncMale review as well as he authored the original Timeout class.

Comment on lines 1087 to 1096
}
long remainingNanos = timeout.remainingOrInfinite(NANOSECONDS);
while (permits == 0
// the absence of short-circuiting is of importance
& !stateAndGeneration.throwIfClosedOrPaused()
& (availableConnection = tryGetAvailable ? tryGetAvailableConnection() : null) == null) {
if (Timeout.expired(remainingNanos)) {
throw createTimeoutException(timeout);
if (timeout.hasExpired()) {
throw createTimeoutException(startTime);
}
remainingNanos = awaitNanos(permitAvailableOrHandedOverOrClosedOrPausedCondition, remainingNanos);
timeout.awaitOn(permitAvailableOrHandedOverOrClosedOrPausedCondition);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

* @return The remaining duration as per {@link Timeout#remainingOrInfinite(TimeUnit)} if waiting ended early either
* spuriously or because of receiving a signal.
*/
private long awaitNanos(final Condition condition, final long timeoutNanos) throws MongoInterruptedException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Comment on lines +97 to +98
* The number of whole time units that remain until this TimePoint
* {@link #hasExpired()}. This should not be used to check for expiry,
* but can be used to supply a remaining value, in the finest-grained
* TimeUnit available, to some method that may time out.
* This method must not be used with infinite TimePoints.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

} else {
// the deadline will track this remaining time
//noinspection ResultOfMethodCallIgnored
condition.awaitNanos(remaining(NANOSECONDS));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the documentation should be clarified to ensure that hasExpired is checked after calling awaitOn as the condition may not be satisfied with in the deadline.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added docs. It seems it is usually checked before calling await (though, this is generally in a loop).

I considered changing the method to perform the check, but there were some complications that I ran into elsewhere (that might be noted in a future PR).

@katcharov katcharov requested a review from stIncMale September 13, 2023 15:17
@stIncMale
Copy link
Member

stIncMale commented Sep 13, 2023

The idea behind JAVA-5112 was that instead of having two sets of APIs, Timeout and TimePoint, we only use one TimePoint API (a quote from the ticket: "If we do this, we won't need to have two APIs, Timeout and TimePoint, we will only need TimePoint."). That was supposed to simplify things. This PR does not do that, it still leaves two APIs (StartTime, Deadline), with a third program element TimePoint that implements them. Furthermore, the Deadline API is hardly different from the Timeout API.

As far as I can see, this PR does not do the improvement that was intended by JAVA-5112. What is the motivation behind this PR?

@katcharov katcharov changed the title Remove Timeout, introduce StartTime and Deadline Replace Timeout implementation with interface, introduce StartTime interface Sep 19, 2023
@katcharov katcharov changed the base branch from master to CSOT September 19, 2023 20:54
@katcharov
Copy link
Contributor Author

(From earlier discussion, to track responses to the points above.)

It seems beneficial, whatever we do, to have a single implementation (more maintainable and testable), and also a single abstraction (a plain point in time), and my opinion is that these are the primary benefits. The outstanding point is whether we should differentiate two "types of" TimePoint (how we decide what counts as a single API is tangential). I believe there is little harm in differentiating because there is no significant API duplication between the interfaces, and a benefit is that the compiler now prevents us from confusing the two, or using methods from one unsuitable in the other (that is, TimePoints are always strictly either Deadlines or StartTimes).

@katcharov
Copy link
Contributor Author

I renamed Deadlines to Timeouts because, while the term "deadline" is clearer, it would be burdensome to change existing usages, and would conflict with public-facing terminology. This is a naming change only - Timeouts are still trimmed down, no longer have start-times built in, etc.

Also changed target to the CSOT feature branch.

Copy link
Member

@rozza rozza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - main comment is to add a little more back ground to StartTime but I like it :)

Comment on lines +97 to +98
* The number of whole time units that remain until this TimePoint
* {@link #hasExpired()}. This should not be used to check for expiry,
* but can be used to supply a remaining value, in the finest-grained
* TimeUnit available, to some method that may time out.
* This method must not be used with infinite TimePoints.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

@@ -75,7 +75,7 @@ public boolean hasTimeoutMS() {
* @return true if the timeout has been set and it has expired
*/
public boolean expired() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change this to hasExpired at some point - just to keep the nomenclature consistent. Doesn't have to be done in this PR.

import java.util.concurrent.TimeUnit;

/**
* @see TimePoint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think perhaps there should be more docs - just explaining why this interface exists.

As TimePoint.now could be added and all functionality is covered by Timepoint.

Copy link
Contributor Author

@katcharov katcharov Sep 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. Both StartTime and Timeout are intended to remove functionality to provide the (now-documented) typesafe guarantees, and to clarify the purpose or intended use of any value of that type.

@katcharov katcharov requested a review from rozza September 21, 2023 22:09
@rozza
Copy link
Member

rozza commented Sep 22, 2023

@katcharov I think you need to push a commit?

Copy link
Member

@rozza rozza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@katcharov katcharov merged commit 8f710f3 into mongodb:CSOT Sep 22, 2023
@katcharov katcharov deleted the JAVA-5112 branch September 22, 2023 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants