You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Polly与异步方法时await后代码无法继续执行问题排查

问题排查:Polly重试SMTP连接后无法恢复执行

以下是相关代码及问题现象:

核心业务代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 21:30:54