question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Simplifying predicate configuration for resilience strategies in V8

See original GitHub issue

In Polly V8, resilience strategies are configured using options and delegates.

Here are some patterns you can use to accomplish this:

// Switch-based pattern (recommended)
builder.AddRetry(new RetryStrategyOptions
{
    ShouldHandle = args => args.Exception switch
    {
        InvalidOperationException => PredicateResult.True,
        _ => PredicateResult.False
    }
});

// Simplified pattern
builder.AddRetry(new RetryStrategyOptions
{
    ShouldHandle = args =>
    {
        if (args.Exception is InvalidOperationException)
        {
            return PredicateResult.True;
        }

        return PredicateResult.False;
    }
});

// Inline pattern
builder.AddRetry(new RetryStrategyOptions
{
    ShouldHandle = args => new ValueTask<bool>(args.Exception is InvalidOperationException)
});

For handling results, the pattern is slightly more complex:

builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = args => args switch
    {
        { Exception: InvalidOperationException } => PredicateResult.True,
        { Result: HttpResponseMessage response } when !response.IsSuccessStatusCode => PredicateResult.True,
        _ => PredicateResult.False
    }
});

I’m currently exploring ways to streamline the configuration of predicates. Here are some options I’ve considered:

Option 1: Introduction of new Constructors

This would involve adding new sets of constructors for every generic option:

public class RetryStrategyOptions<TResult> : ResilienceStrategyOptions
{
    public RetryStrategyOptions()
    {
        // ...
    }

    public RetryStrategyOptions(PredicateBuilder<TResult> shouldHandle)
    {
        // ...
    }

    public RetryStrategyOptions(Func<OutcomeArguments<TResult, RetryPredicateArguments>> shouldHandle)
    {
        // ...
    }

With these, you can now configure strategies like so:

// Simple
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>(args => args.Exception is InvalidOperationException));

// With results
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>(args =>
{
    if (args.Exception is InvalidOperationException)
    {
        return true;
    }

    if (args.Result != null && !args.Result.IsSuccessStatusCode)
    {
        return false;
    }

    return false;
}));

// Using predicate builder
var predicate = new PredicateBuilder<HttpResponseMessage>()
    .Handle<InvalidOperationException>()
    .HandleResult(r => !r.IsSuccessStatusCode);

builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>(predicate));

This option complements the pattern well but limits extensibility to constructors only.

Option 2: Use extension methods for options

In this scenario, the previous example would look like this:

// Simple
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>().WithShouldHandle(args => args.Exception is InvalidOperationException));

// With results
builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>().WithShouldHandle(args =>
{
    if (args.Exception is InvalidOperationException)
    {
        return true;
    }

    if (args.Result != null && !args.Result.IsSuccessStatusCode)
    {
        return false;
    }

    return false;
}));

// Using predicate builder
var predicate = new PredicateBuilder<HttpResponseMessage>()
    .Handle<InvalidOperationException>()
    .HandleResult(r => !r.IsSuccessStatusCode);

builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>(predicate).WithShouldHandle(builder));

While this method allows for greater future extensibility, the syntax is a bit more complex.

Option 3: Extensions for builders

Here, we’d offer extensions for the ResilienceStrategyBuilder to facilitate easier predicate configuration. For instance, these convenience extensions could take the handling configuration as the first parameter, followed by the options.

// Simple
builder.AddRetry(args => args.Exception is InvalidOperationException, new RetryStrategyOptions<HttpResponseMessage>());

// With results
builder.AddRetry(
   args =>
  {
      if (args.Exception is InvalidOperationException)
      {
          return true;
      }
  
      if (args.Result != null && !args.Result.IsSuccessStatusCode)
      {
          return false;
      }
  
      return false;
  }, 
  new RetryStrategyOptions<HttpResponseMessage>());

// Using predicate builder
var predicate = new PredicateBuilder<HttpResponseMessage>()
    .Handle<InvalidOperationException>()
    .HandleResult(r => !r.IsSuccessStatusCode);

builder.AddRetry(predicate , new RetryStrategyOptions<HttpResponseMessage>(predicate));

This approach could lead to the development of more extensions and even the complete omission of options. However, the number of extension types could potentially increase exponentially.

Option 4: Implicit conversion from PredicateBuilder to delegate

This allows the following usage:

new ResilienceStrategyBuilder<HttpResponseMessage>()
    .AddRetry(new()
    {
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(result => result.IsSuccessStatusCode)
    });

Or for non-generic options:

new ResilienceStrategyBuilder()
    .AddRetry(new()
    {
        ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
    });

The idea si to define built-in implicit conversion between PredicateBuilder and the delegate:

public static implicit operator Func<OutcomeArguments<TResult, RetryPredicateArguments>, ValueTask<bool>>(PredicateBuilder<TResult> builder)
{
    return builder.CreatePredicate<RetryPredicateArguments>();
}

We wouldn’t expose any additional extensions for individual strategies. Simple use-cases can be done with PredicateBuilder while the more complex (args access, async predicates) can be done by defining the full delegate.

Option 5: Embracing switch Expressions, no new API

This option involves accepting switch expressions for all predicate configurations. It keeps the API minimal, but the configuration could seem foreign to those unfamiliar with the latest C# features.

I welcome your thoughts on these options. @martincostello, @joelhulen, @geeknoid, what are your preferences? Are there any other alternatives you’d suggest?

cc @PeterCsalaHbo

Issue Analytics

  • State:closed
  • Created 3 months ago
  • Comments:15 (12 by maintainers)

github_iconTop GitHub Comments

1reaction
martintmkcommented, Jun 21, 2023

fyi, the perf difference between switch expressions and PredicateBuilder:

Method Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
Predicate_SwitchExpression 17.17 ns 0.028 ns 0.041 ns 1.00 0.00 - NA
Predicate_PredicateBuilder 29.64 ns 0.859 ns 1.232 ns 1.73 0.07 - NA
1reaction
martintmkcommented, Jun 20, 2023

Thanks for the examples!

Just for comparison, this is how those would look in the current V8 API:

// native C# syntax, max perf 
var options = new RetryStrategyOptions<HttpResponseMessage>()
{
    ShouldHandle = args => args switch
    {
        { Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True,
        { Exception: HttpRequestException } => PredicateResult.True,
        { Exception: IOException } => PredicateResult.True,
        { Exception: BusinessException } => PredicateResult.False,
        _ => PredicateResult.False
    }
};

// PredicateBuilder, fluent syntax
var options = new RetryStrategyOptions<HttpResponseMessage>
{
    ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
        .HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
        .Handle<HttpRequestException>()
        .Handle<BusinessException>(e => false)
};
Read more comments on GitHub >

github_iconTop Results From Across the Web

README.md
Predicates : These are essential when a resilience strategy needs to determine whether or not to handle the execution result. Events: These are...
Read more >
Detailed examples of tactics — Coq 8.9.1 documentation
Let's see how the technique works with induction on inductive predicates on a real example. We will develop an example application to the...
Read more >
Extended pattern matching
The pattern matching compilation strategy examines patterns from left to right. A match expression is generated only when there is at least one...
Read more >
Recent changes — Coq 8.17.1 documentation
Changed: A simplification of parsing rules could cause a slight change of parsing precedences for the very rare users who defined notations with...
Read more >
When Are Opaque Predicates Useful?
Resilience — This follows immediately, since we prove that our opaque predicates are indistin- guishable from obfuscations of already occurring predicates, cf.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found