使用AsyncInterceptor+Polly实现AOP重试:异步方法无等待间隔问题排查
你的异步重试间隔不生效的核心问题在于Castle IInvocation.Proceed() 方法在异步场景下只能被调用一次,而你的Polly重试逻辑每次都重复调用invocation.Proceed(),导致后续重试没有真正重新执行仓储的异步方法,而是复用了第一次执行时返回的已失败Task。
1. 验证仓储异步方法是否被重复执行
在你的GetByIdAsync方法中添加日志输出,确认重试时是否多次进入该方法:
public async Task<Entity> GetByIdAsync(int id) { Console.WriteLine("Entering GetByIdAsync"); // 添加日志排查 if (_exceptionsCoutner <= 2) { _exceptionsCoutner++; throw new DatabaseException(); } return await Task.FromResult(new Entity { Id = id }); }
如果日志只输出一次,说明后续重试没有重新执行目标方法,问题出在拦截器的Proceed()调用逻辑上。
2. 检查Polly重试计数是否正常递增
修改Polly的日志委托,添加重试次数参数,确认每次重试的计数是否正确:
.WaitAndRetryAsync(RetryIntervals, (exception, timeSpan, retryCount) => { Console.WriteLine($"Exception {timeSpan}, Retry Count: {retryCount}"); })
如果重试计数是递增的(0→1→2),说明Polly本身的重试逻辑正常,只是目标方法没有被重新执行;如果计数始终为0,说明Polly没有正确识别重试场景(这种情况可能性极低)。
3. 确认Castle AsyncInterceptor的Proceed()行为
根据Castle Windsor官方文档,IInvocation.Proceed()在异步调用中仅应被调用一次——它触发目标方法的执行,并将结果赋值给invocation.ReturnValue。多次调用Proceed()会导致未定义行为,通常不会重新执行目标方法,而是直接返回之前的Task结果。
替换异步拦截中依赖invocation.Proceed()的逻辑,改用反射直接调用仓储的目标方法,确保每次重试都重新执行方法:
private async Task<TResult> InternalInterceptAsync<TResult>(IInvocation invocation) { return await Policy .Handle<DatabaseException>() .WaitAndRetryAsync(RetryIntervals, (exception, timeSpan) => { Console.WriteLine($"Exception {timeSpan}"); }) .ExecuteAsync(async () => { // 通过反射调用目标方法,每次重试都会重新执行 var task = (Task<TResult>)invocation.Method.Invoke( invocation.InvocationTarget, invocation.Arguments ); return await task; }); }
同样,建议同步拦截也改用反射调用,保持逻辑一致:
private object InternalInterceptSync(IInvocation invocation) { return Policy .Handle<DatabaseException>() .WaitAndRetry(RetryIntervals, (exception, timeSpan) => { Console.WriteLine($"Exception {timeSpan}"); }) .Execute(() => { return invocation.Method.Invoke( invocation.InvocationTarget, invocation.Arguments ); }); }
这样修改后,每次重试都会真正重新执行仓储的方法,计数器会正常递增,Polly的重试间隔也会按预期生效。
内容的提问来源于stack exchange,提问作者Александр Сысоев

