App-vNext / Polly

Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. From version 6.0.1, Polly targets .NET Standard 1.1 and 2.0+.
https://www.thepollyproject.org
BSD 3-Clause "New" or "Revised" License
13.43k stars 1.23k forks source link

[Question]: Polly v8.2.0 how to use AddRetry and AddFallback methods #1896

Closed JiaLinYou closed 9 months ago

JiaLinYou commented 10 months ago

How should I make the AddRetry‘s OnRetry method print out statements ?

How to solve the problem of "ResiliencePipelineBuilder does not include the definition of AddFallback" ?

Polly v8.2.0 Q1: Why AddRetry method not effective when use?

The logic I expect is that when the TheadSleep method times out, the statements in OnTimeout will be printed out, and then retry will begin. At the same time, the statements in OnRetry will also be printed out. Statements in the current OnTimeout will be printed, but those in the OnRetry will not be printed, indicating that no retry has been triggered.

image image

Q2: Why prompt does not include the definition of AddFallback method when use the AddFallback method ?

As you can see, there is no prompt for the AddFallback method. Is this because I am missing something?

image

Thanks in advance for your help!

What code or approach do you have so far?

        [HttpGet]
        [Route("TestTimeoutSyncV8")]
        public string TestTimeoutSyncV8()
        {           
            try
            {
                CancellationTokenSource source = new CancellationTokenSource();
                CancellationToken cancellationToken = source.Token;

                ResiliencePipeline pipeline = new ResiliencePipelineBuilder()                   
                    .AddTimeout(new TimeoutStrategyOptions
                    {
                        TimeoutGenerator = static args =>
                        {
                            return new ValueTask<TimeSpan>(TimeSpan.FromSeconds(1));
                        },
                        OnTimeout = static args =>
                        {
                            Console.WriteLine($"TestTimeoutSync Execution timed out after {args.Timeout.TotalSeconds} seconds.");

                            return default;
                        }
                    })
                    .AddRetry(new RetryStrategyOptions
                    {
                        ShouldHandle = new PredicateBuilder().Handle<Exception>(),
                        MaxRetryAttempts = 1,
                        Delay = TimeSpan.Zero,
                        OnRetry = static args =>
                        {
                            Console.WriteLine($"OnRetry, Attempt: {args.AttemptNumber}.");
                            return default;
                        }
                    })
                    .Build();

                var result = pipeline.Execute(static token =>
                {
                    var res = TheadSleep(token);
                    Console.WriteLine(res);
                    return res;
                }, cancellationToken);

                return result;
            }
            catch (TimeoutRejectedException ex)
            {
                Console.WriteLine($"TimeoutRejectedException");
            }

            return "TestTimeoutSyncV8 Execution Timeout!";
        }

        public static string TheadSleep(CancellationToken token)
        {
            Thread.Sleep(2000);
            token.ThrowIfCancellationRequested(); // If the request is cancelled, trigger OperationCanceledException
            return "This is TheadSleep";
        }``

Additional context

No response

martincostello commented 10 months ago

For question 1, your strategies are defined in the wrong order. If you swap retry and timeout, like shown in our Getting Started documentation, you'll get the desired behaviour:

TestTimeoutSync Execution timed out after 1 seconds.
OnRetry, Attempt: 0.
TestTimeoutSync Execution timed out after 1 seconds.
TimeoutRejectedException
TestTimeoutSyncV8 Execution Timeout!

For question 2, the fallback strategy requires the pipeline to be of the type of the fallback result. Your code doesn't use the generic type parameter, so the fallback isn't available. The following code will return a fallback:

var pipeline = new ResiliencePipelineBuilder<string>()
    .AddFallback(new FallbackStrategyOptions<string>
    {
        ShouldHandle = new PredicateBuilder<string>().Handle<Exception>(),
        FallbackAction = static args =>
        {
            Console.WriteLine("FallbackAction");
            return Outcome.FromResultAsValueTask("Fallback!");
        }
    })
    .AddRetry(new RetryStrategyOptions<string>
    {
        ShouldHandle = new PredicateBuilder<string>().Handle<Exception>(),
        MaxRetryAttempts = 1,
        Delay = TimeSpan.Zero,
        OnRetry = static args =>
        {
            Console.WriteLine($"OnRetry, Attempt: {args.AttemptNumber}.");
            return default;
        }
    })
    .AddTimeout(new TimeoutStrategyOptions
    {
        TimeoutGenerator = static args =>
        {
            return new ValueTask<TimeSpan>(TimeSpan.FromSeconds(1));
        },
        OnTimeout = static args =>
        {
            Console.WriteLine($"TestTimeoutSync Execution timed out after {args.Timeout.TotalSeconds} seconds.");
            return default;
        }
    })
    .Build();

var result = pipeline.Execute(static token =>
{
    var res = TheadSleep(token);
    Console.WriteLine(res);
    return res;
}, cancellationToken);
TestTimeoutSync Execution timed out after 1 seconds.
OnRetry, Attempt: 0.
TestTimeoutSync Execution timed out after 1 seconds.
FallbackAction
Fallback!
TestTimeoutSyncV8 Execution Timeout!
martintmk commented 10 months ago

^^ What Martin says.

You can also review the following table that shows what strategies are available for each builder type:

https://www.pollydocs.org/strategies/index.html#usage

JiaLinYou commented 10 months ago

Thank you for your answers, so if I want to use the AddCircuitBreaker method, the correct order of addition should be like new ResiliencePipelineBuilder().AddFallback().AddCircuitBreaker().AddRetry().AddTimeout() right?

martintmk commented 10 months ago

Thank you for your answers, so if I want to use the AddCircuitBreaker method, the correct order of addition should be like new ResiliencePipelineBuilder().AddFallback().AddCircuitBreaker().AddRetry().AddTimeout() right?

It's rather:

new ResiliencePipelineBuilder().AddFallback().AddRetry().AddCircuitBreaker().AddTimeout()

You want CB to register each individual retry attempt and each timeout.

JiaLinYou commented 10 months ago

Thank you for your correction, it has indeed taken effect.

What confuses me is that the order of adding methods is not AddFallback().AddCircuitBreaker().AddRetry().AddTimeout() But rather AddFallback().AddRetry().AddCircuitBreaker().AddTimeout()

I mistakenly thought that the addition methods order would be the same as the execution methods order, because I thought it would time out, then retry, then circuit breaker, and finally fallback. This seems to be different from the combination strategy in Polly v7.

Sincere thanks for your efforts, which have been of great help to me.

JiaLinYou commented 10 months ago

Sorry for that, but I have a new question again ^^.

When I use the SlidingWindRateLimiter object as a parameter to AddRateLimiter, the fourth request within one minute will be subjected to a flow limiting operation successfully.

image

But when I use the RateLimiterStrategyOptions object as a parameter to AddRateLimiter, it will not be restricted and the statement in the OnRejected method will not be printed out.

image

I have reviewed the document but still feel confused. Please forgive me as a beginner.

martincostello commented 10 months ago

It's much more useful to us, and others who made read this issue in the future, if you provide your code as text in code fences, not as screenshots. Then we can easily copy-paste your code to use, rather than having to type it all out again by reading your screenshots. It's also accessible to tools like screen readers for users of GitHub who need to use such tools.

JiaLinYou commented 10 months ago

Thank you for your reminder. Here is my code.


builder.Services.AddResiliencePipeline<string, string>("Test-TimeoutAsync-V8",
    pipelineBuilder =>
    {
        pipelineBuilder.AddFallback(new FallbackStrategyOptions<string>
        {
            OnFallback = args =>
            {
                Console.WriteLine($"This is OnFallback");
                return default;
            },
            FallbackAction = args =>
            {
                Console.WriteLine("This is FallbackAction");
                return Outcome.FromResultAsValueTask<string>("Fallback: TestTimeoutAsyncV8 timeout!");
            }
        })
                .AddRetry(new RetryStrategyOptions<string>
                {
                    ShouldHandle = new PredicateBuilder<string>().Handle<TimeoutRejectedException>(),
                    MaxRetryAttempts = 2,
                    Delay = TimeSpan.Zero,
                    OnRetry = retryArguments =>
                    {
                        Console.WriteLine($"OnRetry, Attempt: {retryArguments.AttemptNumber}.");
                        return ValueTask.CompletedTask;
                    }
                })
                .AddCircuitBreaker(new CircuitBreakerStrategyOptions<string>
                {
                    ShouldHandle = new PredicateBuilder<string>().Handle<Exception>(),
                    MinimumThroughput = 3,
                    FailureRatio = 0.1,
                    BreakDuration = TimeSpan.FromSeconds(15),
                    OnOpened = cb =>
                    {
                        Console.WriteLine($"OnOpened 开始熔断");
                        return ValueTask.CompletedTask;
                    },
                    OnClosed = cb =>
                    {
                        Console.WriteLine($"OnClosed 停止熔断");
                        return ValueTask.CompletedTask;
                    },
                    OnHalfOpened = cb =>
                    {
                        Console.WriteLine($"OnHalfOpened 释放一部分请求去服务端");
                        return ValueTask.CompletedTask;
                    }
                })
                .AddTimeout(new TimeoutStrategyOptions
                {
                    TimeoutGenerator = args =>
                    {
                        return new ValueTask<TimeSpan>(TimeSpan.FromSeconds(3));
                    },
                    OnTimeout = args =>
                    {
                        Console.WriteLine($"Execution timed out after {args.Timeout.TotalSeconds} seconds.");
                        return ValueTask.CompletedTask;
                    }
                })
                .AddConcurrencyLimiter(2, 10)
                .AddRateLimiter(new RateLimiterStrategyOptions
                {
                    DefaultRateLimiterOptions = new ConcurrencyLimiterOptions
                    {
                        QueueLimit = 10,
                        PermitLimit = 3
                    },
                    OnRejected = rateLimiterArguments =>
                    {
                        Console.WriteLine($"This is user-ResiliencePipeline: OnRejected {rateLimiterArguments.Lease}");
                        return default;
                    }
                });

                //.AddRateLimiter(new SlidingWindowRateLimiter(
                //    new SlidingWindowRateLimiterOptions
                //    {
                //        SegmentsPerWindow = 1,
                //        PermitLimit = 3,
                //        Window = TimeSpan.FromMinutes(1)
                //    }));

    });
martintmk commented 10 months ago

@JiaLinYou

When I use the SlidingWindRateLimiter object as a parameter to AddRateLimiter, the fourth request within one minute will be subjected to a flow limiting operation successfully.

Yup, because you are using AddRateLimiter and providing sliding limiter.

But when I use the RateLimiterStrategyOptions object as a parameter to AddRateLimiter, it will not be restricted and the statement in the OnRejected method will not be printed out.

This is because the default limiter and respective options creates ConcurrencyLimiter, where the conditions to reject (rate-limit) the execution are not met. (>13 concurrent requests, 3 active, 10 queued).

JiaLinYou commented 10 months ago

Thank you for your reminder. I will modify the code to

DefaultRateLimiterOptions = new ConcurrencyLimiterOptions
{
    QueueLimit = 0,
    PermitLimit = 1
 } 

Then I used Postman to initiate two requests simultaneously, and the statement in the OnRejected method was indeed printed out.

May I ask does RateLimiterStrategyOptions have a property similar to the Window specified detection time in SlidingWindRateLimiter?

martincostello commented 10 months ago

You can use our API documentation, as well as the source code in this repo, to see what options are available on various types.

JiaLinYou commented 10 months ago

Thank you for your answer,this is very useful for my learning.

JiaLinYou commented 10 months ago

I use AddRateLimiter twice at the same time, so that I can limit the request when I receive four requests within a minute, and also can limit the request when I receive two requests at the same time.

.AddRateLimiter(new RateLimiterStrategyOptions
                {
                    DefaultRateLimiterOptions = new ConcurrencyLimiterOptions
                    {
                        QueueLimit = 0,
                        PermitLimit = 1
                    },
                    OnRejected = rateLimiterArguments =>
                    {
                        Console.WriteLine($"This is user-ResiliencePipeline: OnRejected {rateLimiterArguments.Lease}");
                        return default;
                    }
                })
                .AddRateLimiter(new SlidingWindowRateLimiter(
                    new SlidingWindowRateLimiterOptions
                    {
                        SegmentsPerWindow = 1,
                        PermitLimit = 3,
                        Window = TimeSpan.FromMinutes(1)
                    }));
martincostello commented 10 months ago

I suggest consulting the Rate limiting middleware in ASP.NET Core documentation, specifically the bit about how to chain rate limiters together using the PartitionedRateLimiter.CreateChained<TResource>() method. You could create a single rate limiter that way, and then use it via the RateLimiterStrategyOptions.RateLimiter property.

JiaLinYou commented 10 months ago

Thank you for your reminder. I am currently trying to implements the above functions through 'RateLimiterStrategityOptions.RateLimiter' attribute.

JiaLinYou commented 9 months ago

Thanks for your help, I have successfully solved this problem. These codes can be used as a reference for those people in need

var limter = PartitionedRateLimiter.CreateChained(
    PartitionedRateLimiter.Create<ResilienceContext, string>(context =>
    {
        ResiliencePropertyKey<string> UserProperty = new("UserName");
        string partitionKey = context.Properties.GetValue(UserProperty, "Default");

        return RateLimitPartition.GetSlidingWindowLimiter(partitionKey, _ =>
        {
            return new SlidingWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                SegmentsPerWindow = 1,
                PermitLimit = 3,
                Window = TimeSpan.FromMinutes(1)
            };
        });
    }),
    PartitionedRateLimiter.Create<ResilienceContext, string>(context =>
    {
        ResiliencePropertyKey<string> UserProperty = new("UserName");
        string partitionKey = context.Properties.GetValue(UserProperty, "Default");

        return RateLimitPartition.GetConcurrencyLimiter(partitionKey, _ =>
        {
            return new ConcurrencyLimiterOptions
            {
                QueueLimit = 0,
                PermitLimit = 1
            };
        });
    })
);

builder.Services.AddResiliencePipeline<string, string>("Test-TimeoutAsync-V8",
    pipelineBuilder =>
    {
        pipelineBuilder.AddRateLimiter(new RateLimiterStrategyOptions
        {
            RateLimiter = args =>
            {
                return limter.AcquireAsync(args.Context,1, args.Context.CancellationToken);
            },
            OnRejected = rateLimiterArguments =>
            {
                Console.WriteLine($"This is user-ResiliencePipeline: OnRejected {rateLimiterArguments.Lease}");
                return default;
            }
        });
    });