Polly中组合WaitAndRetry与BulkheadPolicy,如何优先执行重试任务?
首先直接给结论:Polly原生的WaitAndRetry+Bulkhead组合确实无法直接实现重试任务优先的行为,但我们可以通过自定义任务调度逻辑,结合Polly的Bulkhead能力来达成目标。
问题根源分析
默认的策略组合中,WaitAndRetry在重试时会先执行Task.Delay等待,等待期间会释放Bulkhead的并行槽。但当等待结束后,重试任务会被重新加入Bulkhead的队列——而Bulkhead的队列是严格先进先出(FIFO)的,此时队列里已经塞满了新的首次执行任务,所以重试任务只能排在末尾,导致你看到的“先跑完所有首次任务再处理重试”的现象。
Polly的Bulkhead本身没有内置优先级队列的支持,所以必须通过外部调度来干预任务的执行顺序。
解决方案:自定义优先级队列+Polly Bulkhead
我们可以用一个优先级队列来管理所有待执行的任务(包括首次和重试),让重试任务拥有更高的优先级,同时用Polly的Bulkhead控制并行执行的数量。这样每当Bulkhead有空闲槽时,会优先取出队列中的重试任务执行。
以下是完整的实现代码:
using System.Collections.Concurrent; // 定义优先级:数值越小,优先级越高(重试任务优先) const int RetryPriority = 0; const int FirstAttemptPriority = 1; // 优先级队列:存放待执行的异步操作 var taskQueue = new ConcurrentPriorityQueue<Func<Task>, int>(); // 初始化Bulkhead策略:限制同时运行2个任务 var bulkheadPolicy = Policy.BulkheadAsync( maxParallelization: 2, maxQueuingActions: int.MaxValue ); // 批量添加首次执行任务(低优先级) foreach (var item in Enumerable.Range(1, 10)) { taskQueue.Enqueue(async () => await ExecuteWithRetryLogic(item), FirstAttemptPriority); } // 启动Bulkhead的消费任务:从优先级队列取任务执行 var consumerTasks = Enumerable.Range(0, 2).Select(_ => Task.Run(async () => { while (!taskQueue.IsEmpty) { if (taskQueue.TryDequeue(out var taskFunc, out _)) { await bulkheadPolicy.ExecuteAsync(taskFunc); } else { // 队列暂时为空时短暂等待,避免空轮询浪费资源 await Task.Delay(10); } } })).ToList(); // 自定义带重试逻辑的执行方法 async Task ExecuteWithRetryLogic(int item) { int attempt = 0; while (true) { attempt++; Console.WriteLine($"{DateTime.Now:HH:mm:ss} Starting #{item}/{attempt}"); await Task.Delay(1000); // 模拟业务执行时间 if (attempt == 1) { // 首次执行失败:将重试任务以高优先级放回队列 taskQueue.Enqueue(() => ExecuteWithRetryLogic(item), RetryPriority); return; } else { // 重试成功:退出循环 return; } } } // 等待所有任务完成 await Task.WhenAll(consumerTasks);
代码说明
- 优先级队列:使用
.NET 6+内置的ConcurrentPriorityQueue,保证线程安全的同时,优先取出优先级高的任务(重试任务)。 - Bulkhead的角色:仅负责控制并行执行的任务数量,不再参与任务排队逻辑——排队逻辑由我们自定义的优先级队列接管。
- 重试逻辑:将原来Polly
WaitAndRetry的重试逻辑拆出来,改成失败后把重试任务放回优先级队列,确保重试任务能被优先调度。
运行这段代码后,你会看到符合预期的输出:首次执行的任务1-4失败后,会在1秒后优先启动它们的重试,而不是继续执行任务5-10的首次操作。
为什么不反转策略顺序?
你提到反转策略顺序(Bulkhead在外,WaitAndRetry在内)会导致并行度降低,这是因为此时整个重试流程(包括等待时间)会被视为一个Bulkhead操作,等待期间会占用并行槽,导致实际同时运行的任务数远低于设置的maxParallelization,这确实不符合需求,所以这个方案不可行。
内容的提问来源于stack exchange,提问作者Theodor Zoulias

