-
Notifications
You must be signed in to change notification settings - Fork 7
Add Retry utility with RetryPolicy definition #20
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
Conversation
G8XSU
commented
Dec 2, 2023
- Includes implementation for ExponentialBackoffRetryPolicy and JitteredRetryPolicy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, generally LGTM!
While I think it also makes sense to have these utilities live in this crate, I want to note that LDK Node would also probably benefit from having access to them as we already implement (much more simple/crude) retrying in a few places there. Do you have an idea how we could make it happen to have them reusable?
src/util/retry.rs
Outdated
/// A function that performs and retries the given operation, acc. to a retry policy and a set of | ||
/// retriable errors. | ||
pub async fn retry<R, F, Fut, T, E>( | ||
mut operation: F, retry_policy: &R, retriable_errors: Option<HashSet<E>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, this this could be made a bit more simple by just leaving out the Option
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but we need the functionality to retry on all errors instead of having to specify all errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, makes sense! Could you add a few more words in the fn retry
docs that describes the intended usage of this API, i.e., also that the user may specify a set of errors that if only these variants should be retried? Given the discussion around the Hash
impl above it might also be good to specify how these errors are matched exactly, i.e., if they really need to be idenentical or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we just pass a Fn
? Feels a little more rust idiomatic, at least. Callers would likely just hardcode a few error checks without using a container.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Functional interface for this seems like an overkill.
Callers would normally just retry on fixed set of errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am just worried if clients trip up on it and end up with an infinite loop in production. (and our new api enables that)
Often enough, retry policies don't get properly tested in client code.
Whereas with previous api, client was forced to input some(max_attempts), they could still input none
explicitly and get infinite retries.
(a doc warning might help but it is still not as good as interface.)
one way would be to remove with_max_attempts
from RetryPolicy and have ExponentialBackoffRetryPolicy
like
pub fn new<E: Error>(base_delay: Duration, max_attempts: Option<u32>) -> MaxAttemptsRetryPolicy<Self, E> {
MaxAttemptsRetryPolicy { inner_policy: ExponentialBackoffRetryPolicy { base_delay }, max_attempts, phantom: PhantomData }
}
But it is not ideal because if we have to introduce another decorator in constructor, it is statically linked to MaxAttemptsRetryPolicy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could have the retry
interface take a MaxAttemptsRetryPolicy
instead of a generic RetryPolicy
if you want to enforce it at the interface level, I suppose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Option-1 Remove with_max_attempts
and MaxAttempts decorator, have this implementation inside each concrete policy such as ExponentialBackoff. take max_attempts as arg in constructor.
Option-2 Keep interface as it is now, using with_max_attempts
. (Ack risk that client could misuse it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm...
Option 3: Change retry
to take &MaxAttemptsRetryPolicy<R>
instead of &R
.
Also, could add a without_attempt_limit
provided function to RetryPolicy
returning a MaxAttemptsRetryPolicy
, changing the max_attempts
field to an Option
where it is set to None
. That way, you can still enforce making a choice even if the user doesn't want a limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change retry to take &MaxAttemptsRetryPolicy instead of &R.
This is limiting, it forces MaxAttempts to be the last decorator that is applied.
could add a without_attempt_limit provided function to RetryPolicy returning a MaxAttemptsRetryPolicy, changing the max_attempts field to an Option where it is set to None.
I am not sure how this would work, in current impl, if we use the same decorator twice, both of them are applied. (previous one doesn't get overridden)
It is pub and exposed from this crate. (But I am not sure if we would want to use them as it is in ldk-node, but maybe we can since it is internal repo?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, mod outstanding feedback (drop Hash
impl and use Vec
, add a sentence more to explain usage of retriable_errors
).
src/util/retry.rs
Outdated
/// A function that performs and retries the given operation, acc. to a retry policy and a set of | ||
/// retriable errors. | ||
pub async fn retry<R, F, Fut, T, E>( | ||
mut operation: F, retry_policy: &R, retriable_errors: Option<HashSet<E>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we just pass a Fn
? Feels a little more rust idiomatic, at least. Callers would likely just hardcode a few error checks without using a container.
@jkczyz @tnull (I wouldn't want to do some separate thing if that's too complicated, just to make bindings work) Would also like to hear from @tnull on this. |
If the use case is to use this in LDK Node, the user could give the base |
I think I have no strong opinion which way to go. Decorators can be nice in Rust, but also happy to go another way. Regarding bindings compatibility:
So TLDR: no strong opinion, shouldn't be an issue for LDK Node as long as it's not exposed in API and there is no requirement to bubble up the generics. |
In addition to ldk-node, Where certain parts of it are easy to generate bindings such as retry-helper, but if i complicate this with use of above mentioned features such as functional interface and decorator-builder pattern, then it might be difficult to expose them as it is. |
Mentioned earlier, but you can always expose a different interface for bindings that uses this interface internally. We shouldn't necessarily let bindings restrictions affect how we define our abstractions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good!
@jkczyz Thanks for the review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code itself LGTM. Comments primarily on docs and tests.
src/util/retry.rs
Outdated
/// # let max_attempts = 3; | ||
/// # let max_total_delay = Duration::from_secs(60); | ||
/// # let max_jitter = Duration::from_millis(5); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to inline these since they are just repeated in the method names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they don't repeat in docs, since in definition is hidden from user. (using #
).
I prefer this and find it more readable than in-line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that the method and variable names are essentially the same. It would better to inline the values because the example would then demonstrate the types that the methods take.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, feel free to land as is.
src/util/retry.rs
Outdated
@@ -20,7 +20,8 @@ use std::time::Duration; | |||
/// # } | |||
/// # | |||
/// let retry_policy = ExponentialBackoffRetryPolicy::new(Duration::from_millis(100)) | |||
/// .with_max_attempts(5); | |||
/// .with_max_attempts(5) | |||
/// .with_max_total_delay(Duration::from_secs(2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Could be indented one space more to fix alignment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since it is an intermediate commit and it gets fixed in next commit, will ignore it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lol, sorry, error on my part. Should always check if stuff is still present in the final changeset when reviewing commit-by-commit.