Skip to content

Replace Timeout methods with branching "run" methods #1349

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

Merged
merged 25 commits into from
Apr 15, 2024

Conversation

katcharov
Copy link
Collaborator

JAVA-5376

This removes all production usages of the methods. Remaining work pending feedback regarding approach. Please look for TODO-CSOT when reviewing, and treat these as PR comments - some are notes for work remaining to be done, some indicate places where the approach runs into issues, and some are questions/confirmations regarding CSOT.

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.

This its looking good and its going to be a good improvement.

Added some comments and feedback.

(ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
() -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
() -> socket.getInputStream());
}
Copy link
Member

@vbabanin vbabanin Mar 23, 2024

Choose a reason for hiding this comment

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

The Timeout API addresses the issue of race conditions, enhancing reliability. However, the cognitive load required to understand the current implementation seems to be high, particularly due to the ambiguity surrounding the roles of the first and last lambda expressions. Without directly navigating to the method definitions in an IDE, discerning the distinctions between these lambda expressions is challenging.

    return Timeout.checkedRun(operationTimeout, NANOSECONDS,
                    () -> socket.getInputStream(),
                    (ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
                    () -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
                    () -> socket.getInputStream());
        }

To enhance readability, I suggest making the code more descriptive and self-explanatory. Introducing named methods that clearly convey the actions being performed can simplify comprehension for anyone reviewing the code.

For instance, consider the following revised implementation:

return Timeout.checkedRun(operationTimeout, NANOSECONDS,
                onInfinite(socket::getInputStream),
                onNull(socket::getInputStream),
                onRemaining((ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout))),
                onExpired(() -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)));

This approach could involve a set of static methods that return specific Supplier/Consumer types, such as InfiniteSupplier or NullableSupplier. Alternatively, we could design a TimeoutStep class that accumulates lambdas until an execute method is called, providing a fluent interface for configuring timeout behavior:

return Timeout.of(operationTimeout, NANOSECONDS)
              .onInfinite(socket::getInputStream)
              .onNull(socket::getInputStream)
              .onRemaining((ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)))
              .onExpired(() -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)))
              .execute();

It might make the code more more intuitive and also facilitate easier modifications and debugging in the future. @rozza @katcharov what do you think?

P.S.: I don't have a strong preference for the specific approach to making the code more descriptive; my primary concern is that the current state can be confusing.

Copy link
Collaborator Author

@katcharov katcharov Mar 25, 2024

Choose a reason for hiding this comment

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

Perhaps the underlying problem that you identify, of the first and last params, is solved by adding nullAsInfinite, which I have pushed up:

    return Timeout.nullAsInfinite(operationTimeout).checkedRun(NANOSECONDS,
                    () -> socket.getInputStream(),
                    (ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
                    () -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)));
    }

This makes it explicit why the first and last are the same, and removes redundant code. It also facilitates extracting that wrapping, which I think belongs outside of the method. It also means we can get rid of static variants.

I found it helpful when implementing to think of params as going from "least expired to most expired" (infinite never expires, then remaining, then expired). I am not sure how helpful this is for others, but I do not recall needing to check param order when making PR changes. Perhaps this "order mnemonic" addresses remaining readability issues? (Early on, @stIncMale and I briefly discussed the fluent approach. I think it would take more work and design, and would have its own downsides.)

Copy link
Member

@rozza rozza Mar 26, 2024

Choose a reason for hiding this comment

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

I agree with @vbabanin using named handlers over unnamed parameters makes the code self explaining / readable. That said this pattern is similar to how a reactive stream Subscriber has named events, but then libraries (Reactor) often add helpers eg: subscribe(Subscriber) gets extended out to subscribe(OnNext), subscribe(OnNext, onError), subscribe(OnNext, OnError, OnComplete) . Once you start using it and get familiar with it you often use the convience methods. 🤷

Copy link
Member

Choose a reason for hiding this comment

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

To clarify my point - I think its fine to leave as is as its an internal API.

Copy link
Member

@vbabanin vbabanin Apr 10, 2024

Choose a reason for hiding this comment

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

I understand the rationale behind the "least expired to most expired" parameter organization. However, concerns arise regarding its long-term clarity and potential for error as this implicit understanding might not be as apparent in the future.

The approach with fluent API, while initially demanding more effort, promises maintainability in the future, making it easier to understand, modify, and debug.

Given that @stIncMale and @rozza are comfortable with the current implementation and the existing approvals on the PR, I'm willing to support the consensus.

@katcharov katcharov mentioned this pull request Mar 25, 2024
# Conflicts:
#	driver-core/src/main/com/mongodb/internal/TimeoutContext.java
#	driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/AsyncCommandBatchCursor.java
#	driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursor.java
#	driver-core/src/main/com/mongodb/internal/operation/CommandBatchCursorHelper.java
#	driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java
#	driver-core/src/main/com/mongodb/internal/operation/CountOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/CreateIndexesOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/DistinctOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/DropIndexOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/EstimatedDocumentCountOperation.java
#	driver-core/src/main/com/mongodb/internal/operation/OperationHelper.java
import com.mongodb.internal.time.StartTime;
import com.mongodb.internal.time.Timeout;
import com.mongodb.lang.Nullable;
import com.mongodb.session.ClientSession;

import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.concurrent.TimeUnit;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if (timeoutMS == null) {
return alternativeTimeoutMS;
} else if (timeoutMS == 0) {
if (timeout == null) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This change assumes that it is impossible for timeout to be null when timeoutSettings.getTimeoutMS() returns a non-null value. Please confirm.

Copy link
Member

Choose a reason for hiding this comment

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

The API doesn't guarantee this as the Timeout can be passed in as well as TimeOutSettings to a constructor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It appears that in practice, based on usages, timeout is never null in such cases. The constructors are always invoked where the timeout is derived via startTimeout, or have a non-null value. Also, if this were not the case, then I think resetTimeout has a bug, because it derives the timeout based on timeoutSettings.getTimeoutMS.

Refactored a bit.

@@ -166,29 +161,15 @@ private boolean hasTimedOutForCommandExecution() {
* @return timeout to use.
*/
public long timeoutOrAlternative(final long alternativeTimeoutMS) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This timeoutOrAlternative method should ideally be inlined, with getMaxCommitTimeMS, getReadTimeoutMS, and getWriteTimeoutMS encapsulating the result using lambdas, as was done in runMaxTimeMSTimeout. This requires more associated refactoring than seems appropriate for this PR.

@@ -48,16 +48,21 @@ public class TimeoutContext {
private Timeout computedServerSelectionTimeout;
private long minRoundTripTimeMS = 0;

private MaxTimeSupplier maxTimeSupplier = this::calculateMaxTimeMS;
@Nullable
private MaxTimeSupplier maxTimeSupplier = null;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Resetting to null to designate a default seemed clearer, and made the default call more obvious in the context of the logic where this was used (which is one place).

return notNull("Should never be null", maxTimeSupplier.get());
}
public void runMaxTimeMSTimeout(final Runnable onInfinite, final LongConsumer onRemaining,
final Runnable onExpired) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Considered and tried various alternatives:

  1. Returning the remaining time leaks internals (which has led to bugs).
  2. Returning a live Timeout causes unified tests to fail, because a timeout of 100ms will be sent to the server as 99ms as soon as any nanoseconds have elapsed.
  3. Could return a fixed value under a new type of timeout, which would pass that fixed value to its run method (that is, it would not be a timeout, but a configuration value provider); but this is a stretch, can't be compared for "earlier" against other timeouts.

This chosen approach follows the pattern of existing timeout runs, and without leaking internals, breaking tests, or abusing the Timeout type.

@katcharov katcharov requested a review from stIncMale March 28, 2024 22:51
@katcharov katcharov requested review from vbabanin and rozza March 28, 2024 22:51
@katcharov katcharov marked this pull request as ready for review March 28, 2024 22:51
@@ -280,17 +280,14 @@
<!-- Ignoring await return; intended to be used in a loop -->
<Match>
<Class name="com.mongodb.internal.time.Timeout"/>
Copy link
Member

Choose a reason for hiding this comment

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

Should these blocks be removed as there is no method attached?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Refactoring moved the issue into a lambda, and IIRC there is no way to match on a lambda due to tool limitations. This "match" applies to all methods in the class.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, good to know

(ns) -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
() -> new OperationTimeoutAwareInputStream(socket, assertNotNull(operationTimeout)),
() -> socket.getInputStream());
}
Copy link
Member

Choose a reason for hiding this comment

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

To clarify my point - I think its fine to leave as is as its an internal API.

Copy link
Member

@stIncMale stIncMale left a comment

Choose a reason for hiding this comment

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

I have not reviewed everything (mostly the new API). Hopefully, the combination of all reviews covers the changes in full.

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 - any other items can be iterated on

# Conflicts:
#	driver-core/src/main/com/mongodb/internal/TimeoutContext.java
@katcharov
Copy link
Collaborator Author

Merged CSOT, 1 conflict: removed timeoutRemainingMS and its temp fix (see 4e97f21, "added temp fix for timeoutRemainingMS (scheduled to be removed)").

@mongodb mongodb deleted a comment from ross Apr 10, 2024
@katcharov katcharov merged commit 3b6ba32 into mongodb:CSOT Apr 15, 2024
56 of 58 checks passed
@katcharov katcharov deleted the JAVA-5376 branch April 15, 2024 14:31
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.

4 participants