SmtpClient.SendMailAsync引发特定异常时导致死锁

SmtpClient.SendMailAsync causes deadlock when throwing a specific exception
2020-11-21
  •  译文(汉语)
  •  原文(英语)

我正在尝试根据VS2013项目模板中的示例AccountController为ASP.NET MVC5网站设置电子邮件确认.我已经实现了IIdentityMessageServiceusing SmtpClient,试图使其尽可能简单:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("some.guy@company.com", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

调用它的控制器代码直接来自模板(由于我想排除其他可能的原因,因此被提取为单独的操作):

public async Task<ActionResult> TestAsyncEmail()
{
    Guid userId = User.Identity.GetUserId();

    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

    return View();
}

但是,当邮件发送失败时,但在仅一种特定情况下,当主机因某种原因无法到达时,我的行为变得奇怪.配置示例:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>

在这种情况下,请求似乎陷入僵局,从不向客户端返回任何内容.如果邮件由于任何其他原因而发送失败(例如主机主动拒绝连接),则可以正常处理该异常,并且我收到YSOD.

查看Windows事件日志,似乎InvalidOperationException在同一时间范围内抛出,消息为"异步模块或处理程序已完成,而异步操作仍处于待处理状态".如果尝试SmtpException在控制器ViewResult中捕获并在catch块中返回a ,则会在YSOD中得到相同的消息.因此,我认为await-ed操作在两种情况下均无法完成.

据我所知,我正在遵循SO其他帖子中概述的所有async / await最佳实践(例如HttpClient.GetAsync(...)在使用await / async时永远不会返回),主要是"使用async / await all方式".我也尝试使用ConfigureAwait(false),没有任何改变.由于仅在抛出特定异常时代码才会死锁,因此我认为一般模式在大多数情况下都是正确的,但是内部发生了一些事情,导致在这种情况下不正确.但是由于我对并发编程还很陌生,所以我觉得自己可能是错的.

我在做错什么吗?我总是可以SmtpClient.Send()在SendAsync方法中使用同步调用(即),但感觉应该可以正常工作.

速聊1:
看看Stephen Cleary关于在void method(SendMailAsync)上捕获异常的答案.异步无效方法有时是有问题的孩子.
速聊2:
-我async void 在示例中没有看到任何方法(已实现或已调用)-您是说某些特定的行吗?
速聊3:
解决方法是,您可以尝试手动解析主机并尽早失败...另外,请查看源代码以获取见解-希望这会有所帮助...
速聊4:
我记得有一个解决方法的相关问题...它是:从SignalR集线器发送异步邮件
速聊5:
,WithNoContext这里尝试我的产品,看看有什么不同.
解决过程1

尝试此实现,只需使用client.SendMailExAsync代替即可client.SendMailAsync.让我们知道是否有任何不同:

public static class SendMailEx
{
    public static Task SendMailExAsync(
        this System.Net.Mail.SmtpClient @this,
        System.Net.Mail.MailMessage message,
        CancellationToken token = default(CancellationToken))
    {
        // use Task.Run to negate SynchronizationContext
        return Task.Run(() => SendMailExImplAsync(@this, message, token));
    }

    private static async Task SendMailExImplAsync(
        System.Net.Mail.SmtpClient client, 
        System.Net.Mail.MailMessage message, 
        CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        System.Net.Mail.SendCompletedEventHandler handler = null;
        Action unsubscribe = () => client.SendCompleted -= handler;

        handler = async (s, e) =>
        {
            unsubscribe();

            // a hack to complete the handler asynchronously
            await Task.Yield(); 

            if (e.UserState != tcs)
                tcs.TrySetException(new InvalidOperationException("Unexpected UserState"));
            else if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(true);
        };

        client.SendCompleted += handler;
        try
        {
            client.SendAsync(message, tcs);
            using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false))
            {
                await tcs.Task;
            }
        }
        finally
        {
            unsubscribe();
        }
    }
}
速聊1:
那一个起作用;正如通常所料,异常被捕获,使调用栈冒泡,我得到了YSOD.似乎很多代码都可以执行看起来很简单的事情(!),但是我可以看到它如何快速变得复杂.无论如何,标记为已接受,因为它确实可以解决.感谢你的帮助!
速聊2:
很高兴它起作用;这是Task基于EAP模式的典型包装器.我不知道,如果它仍然会工作没有 await Task.Yield(),也许你可以试试看.
速聊3:
Task.Yield根据快速测试,似乎没有可以工作.
速聊4:
,在这种情况下handler,显然不需要异步.
速聊5:
那么SmtpClient中是否存在此代码可以解决的错误?

I'm trying to setup email confirmation for an ASP.NET MVC5 website, based on the example AccountController from the VS2013 project template. I've implemented the IIdentityMessageService using SmtpClient, trying to keep it as simple as possible:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("some.guy@company.com", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

The controller code that is calling it is straight from the template (extracted into a separate action since I wanted to exclude other possible causes):

public async Task<ActionResult> TestAsyncEmail()
{
    Guid userId = User.Identity.GetUserId();

    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

    return View();
}

However I'm getting odd behavior when the mail fails to send, but only in one specific instance, when the host is somehow unreachable. Example config:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>

In that case, the request appears to deadlock, never returning anything to the client. If the mail fails to send for any other reason (e.g. host actively refuses connection) the exception is handled normally and I get a YSOD.

Looking at the Windows event logs, it seems that an InvalidOperationException is thrown around the same timeframe, with the message "An asynchronous module or handler completed while an asynchronous operation was still pending."; I get that same message in a YSOD if I try to catch the SmtpException in the controller and return a ViewResult in the catch block. So I figure the await-ed operation fails to complete in either case.

As far as I can tell, I am following all the async/await best practices as outlined in other posts on SO (e.g. HttpClient.GetAsync(...) never returns when using await/async), mainly "using async/await all the way up". I've also tried using ConfigureAwait(false), with no change. Since the code deadlocks only if a specific exception is thrown, I figure the general pattern is correct for most cases, but something is happening internally that makes it incorrect in that case; but since I'm pretty new to concurrent programming, I've a feeling I could be wrong.

Is there something I'm doing wrong ? I can always use a synchronous call (ie. SmtpClient.Send()) in the SendAsync method, but it feels like this should work as is.

Talk1:
Take a look at Stephen Cleary's answer on catching an exception on a void method(SendMailAsync). Async Void Methods are problem childs sometimes.
Talk2:
- I don't see any async void methods in the sample (either implemented or called) - did you mean some particular line?
Talk3:
As workaround you may try to manually resolve the host and fail earlier... Also look at the source for insights - hopefully it would be helpful...
Talk4:
I remember a related question with a workaround... there it is: Sending async mail from SignalR hub
Talk5:
, try my WithNoContext from here, see if it makes any difference.
Solutions1

Try this implementation, just use client.SendMailExAsync instead of client.SendMailAsync. Let us know if it makes any difference:

public static class SendMailEx
{
    public static Task SendMailExAsync(
        this System.Net.Mail.SmtpClient @this,
        System.Net.Mail.MailMessage message,
        CancellationToken token = default(CancellationToken))
    {
        // use Task.Run to negate SynchronizationContext
        return Task.Run(() => SendMailExImplAsync(@this, message, token));
    }

    private static async Task SendMailExImplAsync(
        System.Net.Mail.SmtpClient client, 
        System.Net.Mail.MailMessage message, 
        CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        System.Net.Mail.SendCompletedEventHandler handler = null;
        Action unsubscribe = () => client.SendCompleted -= handler;

        handler = async (s, e) =>
        {
            unsubscribe();

            // a hack to complete the handler asynchronously
            await Task.Yield(); 

            if (e.UserState != tcs)
                tcs.TrySetException(new InvalidOperationException("Unexpected UserState"));
            else if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(true);
        };

        client.SendCompleted += handler;
        try
        {
            client.SendAsync(message, tcs);
            using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false))
            {
                await tcs.Task;
            }
        }
        finally
        {
            unsubscribe();
        }
    }
}
Talk1:
That one works; the exception is caught as one would normally expect, bubbles up the call stack and I get a YSOD. Seems like a whole lot of code to do something that seemed so simple (!), but I can see how it can get complicated fast. Anyway marking as accepted since it does solve it. Thanks for all your help!
Talk2:
Glad it works; it's a typical Task-based wrapper over EAP pattern. I wonder if it still would work without await Task.Yield(), maybe you could give it a try.
Talk3:
Seems to work without the Task.Yield, according to a quick test.
Talk4:
, in which case, the handler doesn't need to be async, apparently.
Talk5:
So is there a bug in SmtpClient that this code works around?
转载于:https://stackoverflow.com/questions/28333396/smtpclient-sendmailasync-causes-deadlock-when-throwing-a-specific-exception

本人是.net程序员,因为英语不行,使用工具翻译,希望对有需要的人有所帮助
如果本文质量不好,还请谅解,毕竟这些操作还是比较费时的,英语较好的可以看原文

留言回复
我们只提供高质量资源,素材,源码,坚持 下了就能用 原则,让客户花了钱觉得值
上班时间 : 周一至周五9:00-17:30 期待您的加入