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

[Feature request]: Simpler way to do basic ShouldHandle delegates for retry #2258

Open
martincostello opened this issue Aug 8, 2024 · 3 comments

Comments

@martincostello
Copy link
Member

martincostello commented Aug 8, 2024

Is your feature request related to a specific problem? Or an existing feature?

Doing some changes to an application of my own, it occurred to me that for basic use cases for retries, the syntax is more verbose than it could be:

// From https:/martincostello/costellobot/blob/e3ef21ed1db09c65e876a3d2b2650184bde69c62/src/Costellobot/Handlers/DeploymentStatusHandler.cs#L106-L115
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new()
    {
        Delay = TimeSpan.FromSeconds(2),
        ShouldHandle = new PredicateBuilder().Handle<ApiValidationException>(),
    })
    .Build();

The PredicateBuilder class is a good way to express complex conditions, but needing to allocate the object (and the underlying array) to then create the delegate to assign to ShouldHandle seems a bit over-the-top.

It feels like we could add something that's a bit simpler (and then implement PredicateBuilder in terms of it).

This is just a rough sketch of an idea to see what people think for now, I haven't tried to actually prototype it yet or anything.

Describe the solution you'd like

Something like this, where the existing instance methods in the PredicateBuilder would just call new static methods, but simple cases can use them directly:

public static class ResiliencePredicates
{
    public static Predicate<Outcome<TResult>> Handle<TException>()
        where TException : Exception
    {
        // Implementation ...
    }

    public static Predicate<Outcome<TResult>> Handle<TException>(Func<TException, bool> predicate)
        where TException : Exception
    {
        // Implementation ...
    }

    // etc.
}

That would then simplify the original example to something like this:

var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new()
    {
        Delay = TimeSpan.FromSeconds(2),
-       ShouldHandle = new PredicateBuilder().Handle<ApiValidationException>(),
+       ShouldHandle = ResiliencePredicates.Handle<ApiValidationException>,
    })
    .Build();

Additional context

No response

@peter-csala
Copy link
Contributor

Your proposed solution (as far as I understand) works with single exception type only. You can't chain them.

IMHO having different classes for simple and advanced scenarios does not help the usage of the API.

If we want to rewrite the predicate builder then we should look at how others have address this problem.

Like resilience4j or guava-retrying.

@martincostello
Copy link
Member Author

Your proposed solution (as far as I understand) works with single exception type only. You can't chain them.

Exactly - this suggestion is about making the simplest cases easier where you only want to check for one thing, not many.

It's just on reflection seems a bit overly-verbose and wasteful to allocate an object and then immediately throw it away. I guess if you cared enough about that you could just write a delegate directly instead, but then you need to account for the ValueTask and Outcome machinery so then it ends up being more complicated.

We have all the pieces, they're just hidden away in the internals.

@martintmk
Copy link
Contributor

Hey @martincostello

I think making this work will be more difficult, even not possible.

The thing is that the predicates are more complex than Predicate<Outcome<TResult>> and assigning it to RetryOptions.ShouldHandle is not possible. For example, this is the signature of RetryOptions.ShouldHandle predicate:

Func<RetryPredicateArguments<object>, ValueTask<bool>>

Assigning PredicateBuilder to this value works because we define a custom converter to this delegate:

public static implicit operator Func<RetryPredicateArguments<TResult>, ValueTask<bool>>(PredicateBuilder<TResult> builder)

Now, we cannot do the same for Predicate<Outcome<TResult>>, I have actually tried before :)

It's just on reflection seems a bit overly-verbose and wasteful to allocate an object and then immediately throw it away.
FYI, this is "do once and never again" as it happens only when initializing the pipeline. There are no allocations on hot path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants