使用Polly与异步方法时await后代码无法继续执行问题排查
以下是相关代码及问题现象:
核心业务代码
public async Task Process(string[] userEmails, IEmailMessage emailMessage) { var smtpClient = await _smtpClient.GetOpenConnection(); _logger.Information("Breaking users into groups and sending out e-mails"); // More Code }
初始GetOpenConnection实现
public async Task<IMailTransport> GetOpenConnection() { var policy = Policy.Handle<Exception>() .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(60), (exception, _) => _logger.Error(exception, "Could not establish connection")); return await policy.ExecuteAsync(EstablishConnection); } private async Task<IMailTransport> EstablishConnection() { var smtpConfiguration = _smtpConfiguration.GetConfiguration(); _logger.Information("Get an open connection for SMTP client"); _smtpClient.Connect(smtpConfiguration.Network.Host, smtpConfiguration.Network.Port, SecureSocketOptions.None, CancellationToken.None); var smtpClient = _smtpClient.GetSmtpClient(); return await Task.FromResult(smtpClient); }
问题现象
关闭测试SMTP服务器(smtp4dev)后,连接失败符合预期,Polly会每隔60秒重试连接。但重新开启服务器后,代码并未在Process方法的await处继续执行。若将GetOpenConnection改为同步方法则一切正常,但会阻塞执行直到返回连接。
更新1:补充执行逻辑说明
首次获取连接无错误时,代码可正常执行到Process方法的_logger行及后续代码;仅当连接失败并触发重试后,才无法进入该行代码。
更新2:修改为异步连接后的异常情况
修改Connect方法为异步实现:
public async Task Connect(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { await _smtpClient.ConnectAsync(host, port, options, cancellationToken); }
同时更新EstablishConnection:
private async Task<IMailTransport> EstablishConnection() { var smtpConfiguration = _smtpConfiguration.GetConfiguration(); _logger.Information("Get an open connection for SMTP client"); await _smtpClient.Connect(smtpConfiguration.Network.Host, smtpConfiguration.Network.Port, SecureSocketOptions.None, CancellationToken.None); var smtpClient = _smtpClient.GetSmtpClient(); _logger.Information("Got an open connection for SMTP client"); return await Task.FromResult(smtpClient); }
修改后,Polly不再触发重试连接。
更新3:补充事件触发相关代码
public void OnPostInsert(PostInsertEvent @event) { _logger.Information("OnPostInsert"); _ = DispatchEvents(@event.Entity) .ContinueWith(x => _logger.Error(x.Exception, "An error occurred while dispatching the insert event"), TaskContinuationOptions.OnlyOnFaulted); } private async Task DispatchEvents(object domainEntity) { Ensure.That(domainEntity, nameof(domainEntity)).IsNotNull(); _logger.Information("Dispatch domain events"); var entityBaseType = domainEntity.GetType().BaseType; if (entityBaseType is not { IsGenericType: true }) return; if (entityBaseType.GetGenericTypeDefinition() != typeof(EntityBase<>)) return; if (domainEntity.GetType().GetProperty("DomainEvents")?.GetValue(domainEntity, null) is not IReadOnlyList<INotification> domainEvents) { return; } foreach (var domainEvent in domainEvents) { _logger.Information("Publishing Event {@DomainEvent}", domainEvent); await _mediator.Publish(domainEvent, CancellationToken.None); } }
排查关键点
1. 初始实现的同步/异步混用问题
初始EstablishConnection中调用了同步的_smtpClient.Connect,却包装成async Task返回,这种混用会导致线程阻塞,若运行在带SynchronizationContext的环境(如旧版ASP.NET、UI线程),极易引发死锁。重试成功后,线程上下文切换异常会导致await无法正确恢复执行。
2. 更新2后Polly停止重试的可能原因
- 检查
ConnectAsync是否抛出了Polly未捕获的异常类型(比如非Exception派生的异常),或异常被内部吞掉,导致Polly认为执行成功; - 查看日志是否有
Got an open connection for SMTP client输出,确认是连接成功未触发后续流程,还是根本没触发重试; - 确认
_smtpClient实例是否在首次连接失败后处于错误状态,复用该实例会导致无法重新建立连接。
3. 事件触发的"Fire and Forget"隐患
OnPostInsert中使用_ = DispatchEvents(...).ContinueWith(...)的无等待方式,会导致任务上下文丢失或未观察到的异常。建议改为异步事件处理器:
public async void OnPostInsert(PostInsertEvent @event) { _logger.Information("OnPostInsert"); try { await DispatchEvents(@event.Entity); } catch (Exception ex) { _logger.Error(ex, "An error occurred while dispatching the insert event"); } }
4. SMTP客户端实例复用问题
重试时复用已失败的_smtpClient实例会导致无法重新连接,每次重试应创建新实例:
private async Task<IMailTransport> EstablishConnection() { var smtpConfiguration = _smtpConfiguration.GetConfiguration(); _logger.Information("Get an open connection for SMTP client"); // 创建新的客户端实例 var client = new SmtpClient(); await client.ConnectAsync(smtpConfiguration.Network.Host, smtpConfiguration.Network.Port, SecureSocketOptions.None, CancellationToken.None); _logger.Information("Got an open connection for SMTP client"); return client; }
5. 冗余的Task.FromResult使用
return await Task.FromResult(smtpClient);可简化为return smtpClient;,异步方法中直接返回同步值即可,无需额外包装。
内容的提问来源于stack exchange,提问作者DerHaifisch

