Skip to content

Conversation

G8XSU
Copy link
Contributor

@G8XSU G8XSU commented Dec 2, 2023

  • Includes implementation for ExponentialBackoffRetryPolicy and JitteredRetryPolicy

@G8XSU G8XSU requested a review from jkczyz December 2, 2023 02:12
Copy link
Contributor

@tnull tnull left a 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?

/// 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>>,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@G8XSU G8XSU Dec 13, 2023

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.

Copy link

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.

Copy link
Contributor Author

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)

Copy link

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.

Copy link
Contributor Author

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)

@G8XSU
Copy link
Contributor Author

G8XSU commented Dec 4, 2023

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?

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?)
I also thought of having something similar in rust-lightning. but in vss-client we can't use any of that.

Copy link
Contributor

@tnull tnull left a 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).

/// 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>>,
Copy link

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.

@G8XSU
Copy link
Contributor Author

G8XSU commented Dec 7, 2023

@jkczyz @tnull
I am still bit divided b/w (decorator & functional-interface) and simple functions approach(without 6c824df).
My main concern is trouble for bindings if we need them later on. Fn/closure parameter for error discriminator and decorator functions such as with_* in RetryPolicy will work fine in rust but might have problems in other languages.

(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.

@jkczyz
Copy link

jkczyz commented Dec 7, 2023

@jkczyz @tnull I am still bit divided b/w (decorator & functional-interface) and simple functions approach(without 6c824df). My main concern is trouble for bindings if we need them later on. Fn/closure parameter for error discriminator and decorator functions such as with_* in RetryPolicy will work fine in rust but might have problems in other languages.

(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 RetryPolicy to the Node builder and we could provide individual Node builder methods for adding the decorators for bindings users. Thus, the decorator API would only need to be used internally. But rust users could still use the decorators if preferred.

@tnull
Copy link
Contributor

tnull commented Dec 8, 2023

@jkczyz @tnull I am still bit divided b/w (decorator & functional-interface) and simple functions approach(without 6c824df). My main concern is trouble for bindings if we need them later on. Fn/closure parameter for error discriminator and decorator functions such as with_* in RetryPolicy will work fine in rust but might have problems in other languages.

(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.

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:

  1. I don't expect this to be exposed in the LDK Node API somewhere, but just be set to a certain RetryPolicy and used internally, so exposing it via LDK Node bindings shouldn't become an issue.
  2. It should be noted however that Uniffi bindings don't support generics, i.e., we really can't let the type parameters bubble up to, say, the Node object. For context, this is already an issue for KVStore where we didn't find a good alternative and currently just 'hard code' SqliteStore via the LDKNode type alias. Note that this will mean for VSS we'll have to do the same, i.e., create a distinct LDKNodeWithVSS alias that, as far as bindings are concerned, will be an entirely separate object (with copy/pasted API definitions, that will need to be maintained every time we change something..).
  3. This also has to be kept in mind if the plan would be to use Uniffi for VssClient. However, so far my understanding was that it would be exposed via LDK's generator, which should be fine?

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.

@G8XSU
Copy link
Contributor Author

G8XSU commented Dec 13, 2023

In addition to ldk-node,
I was also considering if we ever need to create bindings for vss-client itself in future.

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.

@jkczyz
Copy link

jkczyz commented Dec 13, 2023

In addition to ldk-node, I was also considering if we ever need to create bindings for vss-client itself in future.

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.

Copy link

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Overall looks good!

@G8XSU
Copy link
Contributor Author

G8XSU commented Dec 13, 2023

@jkczyz Thanks for the review.
Yes some docs and stuff need cleaning up after the latest changes,
But looking to get alignment on broader stuff first, mainly this for now: #20 (comment)

@G8XSU G8XSU requested a review from jkczyz December 14, 2023 23:40
Copy link

@jkczyz jkczyz left a 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.

Comment on lines 21 to 23
/// # let max_attempts = 3;
/// # let max_total_delay = Duration::from_secs(60);
/// # let max_jitter = Duration::from_millis(5);
Copy link

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.

Copy link
Contributor Author

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.

Copy link

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.

@G8XSU G8XSU requested a review from jkczyz December 18, 2023 21:21
Copy link
Contributor

@tnull tnull left a 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.

@@ -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));
Copy link
Contributor

@tnull tnull Dec 19, 2023

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@G8XSU G8XSU merged commit c5564e7 into lightningdevkit:main Dec 19, 2023
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