Skip to content

Allow clients to request the transactor to retry queries#261

Merged
Omkar Kulkarni (okulkarni) merged 6 commits into
masterfrom
retry_queries
Oct 1, 2019
Merged

Allow clients to request the transactor to retry queries#261
Omkar Kulkarni (okulkarni) merged 6 commits into
masterfrom
retry_queries

Conversation

@okulkarni

@okulkarni Omkar Kulkarni (okulkarni) commented Sep 24, 2019

Copy link
Copy Markdown
Contributor

This patch is inspired by #210 - It introduces ExecutionContext that allows clients to specify options for every query/update. Currently the two options supported are to run as a transaction (deprecates queryAsTransaction and executeAsTransaction methods) and set a retry policy on failure. The default behavior remains unchanged, namely, no retries on failure. However, clients can now use the new exponential backoff policy with tunable number of retries and sleep intervals. Users could potentially create their own advanced retry policies by implementing the ITransactor.RetryPolicy interface. The policy is executed on failure only after the connection has been invalidated or returned to the pool manager.

Example:

transactor
  .asTransaction()
  .allowRetries(new ExponentialBackoffRetryPolicy(numRetries, sleepInterval))
  .query(myQuery);

https://liveramp.atlassian.net/browse/DIST-2702

@davinchia Davin Chia (davinchia) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice PR! Good tests! Spoke offline - one change before I think we are good to go.

private int retryInterval = 0;
private boolean succeeded = false;

public ExponentialBackoffRetryPolicy() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As per https://en.wikipedia.org/wiki/Principle_of_least_astonishment, i'd expect the default constructor of a class named ExponentialBackoffRetryPolicy to have a default non-null value. What about having default static constants for these instead?

I also see we are using this class to implement the NoOp retry policy - what about having a ConfigurableRetryPolicy class with static methods that return a no-op policy and a default exponential policy?

Comment thread jack-core/src/com/rapleaf/jack/transaction/TransactorImpl.java Outdated
}
}

class NoRetryPolicy extends ExponentialBackoffRetryPolicy {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If we go with the above implementation of base retry class + static builder method, NoRetryPolicy no longer needs to exists.

}

public ExponentialBackoffRetryPolicy(int maxRetries) {
this(maxRetries, new Random().nextInt(5000) + 10000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What do you think about letting others configure this backoff? A default definitely makes sense, but since we are enabling this, what about letting others configure the intial backoff period + an exponential multiplier for each successive backoff?

We'd have an additional field named backoffMultiplier the constructor would take in. This would also allow someone to configure a linear backoff without any code changes at all (for whatever reason one would want one instead of exponential).

if (shouldRetry()) {
LOG.warn("Retry #" + numFailures + ": Going to sleep for " + retryInterval + " milliseconds.");
sleep(retryInterval);
retryInterval <<= 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Whoa been a while since i've seen bitshift code - fancy C stuff here!

public void testWaitForConnectionTimeout() throws Exception {
TransactorImpl<IDatabase1> transactor = transactorBuilder.setMaxTotalConnections(1).setMaxWaitTime(Duration.ofMillis(500)).get();
TransactorImpl<IDatabase1> transactor =
transactorBuilder.setMaxTotalConnections(1).setMaxWaitTime(Duration.ofMillis(500)).get();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🙏 for the reformatting

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The IDE did it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree with most comments. However, decided to keep ExponentialBackoffRetryPolicy as is while removing the "no retry" case (and eliminating astonishment), and instead of introducing more complexity with a new base class and builder methods, decided to simply refactor NoRetryPolicy as essentially a no-op policy which always returns false for shouldRetry.

public void testWaitForConnectionTimeout() throws Exception {
TransactorImpl<IDatabase1> transactor = transactorBuilder.setMaxTotalConnections(1).setMaxWaitTime(Duration.ofMillis(500)).get();
TransactorImpl<IDatabase1> transactor =
transactorBuilder.setMaxTotalConnections(1).setMaxWaitTime(Duration.ofMillis(500)).get();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The IDE did it.

@davinchia Davin Chia (davinchia) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm apart from 1 nit :shipit:

} else {
dbManager.invalidateConnection(connection);
}
context.retryPolicy.execute();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: comment explaining why we want to retry after returning/invalidating connections

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will do!

@briancecker Brian Ecker (briancecker) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice PR. I like the way you approached this with the ExecutionContext.


boolean shouldRetry();

void updateOnFailure();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you document the purpose of this method or make the name more clear? I had a hard time understanding what it was used for until I saw the exponential implementation. I feel like just onFailure would be a better name. The update part really confused me, and onFailure/onSuccess are relatively common names for callback type methods.

Does it make sense to provide some sort of argument to these methods? For instance, onFailure could take an Exception as an argument to allow the implementing class to modify retry behavior based on the type of failure. You could then differentiate between retrying on a transient exception and not retrying on a logical exception.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree completely on the first part. I had originally named it onFailure and onSuccess, I am not sure why I changed that. :-/ I realize that on{EventName} is common for handling events (My Java is still a bit rusty after having worked on C all this while).

Probably a good idea to allow onFailure to handle the exception -- which makes me think that the shouldRetry method is redundant since onFailure could simply throw/propagate the exception to exit the loop. Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm, that's also another way of looking at it. So the usage would be like the following, correct?

  • try to execute a query
  • catch an exception
  • in the catch block, call onFailure(e)
  • if the number of retries is below a certain number, then you increment the number and execute what is now in the execute method
  • If the number of retries is above the threshold, then you rethrow the exception allowing the method to fail normally

Maybe this whole thing could just be onFailure? What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was the original thought; However, the reason I have onSuccess and execute is to allow the policy to sleep (or do some other blocking/long running task) after returning the connection to the connection pool, which is located in the finally block. Since the finally block executes no matter what, there must be some way of knowing when not to sleep -- either the number of retries have been exhausted or the query was successful.

Omkar Kulkarni added 2 commits October 1, 2019 09:03
1. Removed `shouldRetry` (redundant).
2. Renamed to `onSuccess` and `onFailure` for clarity.
3. `onFailure` now takes an exception as an argument.
4. `execute` returns boolean indicating whether policy was executed.
Comment to explain why policy is executed after returning/invalidating connection.
@okulkarni
Omkar Kulkarni (okulkarni) merged commit 7216a66 into master Oct 1, 2019
@delete-merged-branch
delete-merged-branch Bot deleted the retry_queries branch October 1, 2019 23:24
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