【Azure 应用服务】Azure Function App使用SendGrid发送邮件遇见异常消息The operation was canceled,分析源码渐入最源端

简介: 【Azure 应用服务】Azure Function App使用SendGrid发送邮件遇见异常消息The operation was canceled,分析源码渐入最源端

问题描述

在使用Azure Function App的 SendGrid Binging 功能,调用SendGrid服务器发送邮件功能时,遇到见间歇性,偶发性的异常。在重新运行SendGrid的Function,却又能恢复运行。

所以本文基于Azure Function使用SendGrid的异常错误日志,一步一步,分析源码中的方法内容。 然后调查为什么 Azure Function 没有自动重试(Retry)?

(如需要参考如何使用Azure Function SendGrid,参考:Azure Functions SendGrid 绑定)

错误消息:

System.Threading.Tasks.TaskCanceledException : The operation was canceled.
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async System.Net.Http.ConnectHelper.ConnectAsync(String host,Int32 port,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at async System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request,Boolean allowHttp2,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at async System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at async System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at async System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request,Boolean doRequestAuth,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async SendGrid.Helpers.Reliability.RetryDelegatingHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask,HttpRequestMessage request,CancellationTokenSource cts,Boolean disposeCts)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async SendGrid.SendGridClient.MakeRequest(HttpRequestMessage request,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async SendGrid.SendGridClient.RequestAsync(Method method,String requestBody,String queryParams,String urlPath,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async SendGrid.SendGridClient.SendEmailAsync(SendGridMessage msg,CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Client.SendGridClient.SendMessageAsync(SendGridMessage msg,CancellationToken cancellationToken) at C:\azure-webjobs-sdk-extensions\src\WebJobs.Extensions.SendGrid\Client\SendGridClient.cs : 23
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Extensions.Bindings.SendGridMessageAsyncCollector.FlushAsync(CancellationToken cancellationToken) at C:\azure-webjobs-sdk-extensions\src\WebJobs.Extensions.SendGrid\Bindings\SendGridMessageAsyncCollector.cs : 56
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Bindings.AsyncCollectorValueProvider`2.SetValueAsync[TUser,TMessage](Object value,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\AsyncCollector\AsyncCollectorValueProvider.cs : 49
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Binder.Complete(CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\Runtime\Binder.cs : 150
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Bindings.Runtime.RuntimeValueProvider.SetValueAsync(Object value,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Bindings\Runtime\RuntimeValueProvider.cs : 34
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ParameterHelper.ProcessOutputParameters(CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 925
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithWatchersAsync(IFunctionInstanceEx instance,ParameterHelper parameterHelper,ILogger logger,CancellationTokenSource functionCancellationTokenSource) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 518
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstanceEx instance,FunctionStartedMessage message,FunctionInstanceLogEntry instanceLogEntry,ParameterHelper parameterHelper,ILogger logger,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 279
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(IFunctionInstanceEx instance,FunctionStartedMessage message,FunctionInstanceLogEntry instanceLogEntry,ParameterHelper parameterHelper,ILogger logger,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 326
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync(IFunctionInstance functionInstance,CancellationToken cancellationToken) at C:\projects\azure-webjobs-sdk-rqm4t\src\Microsoft.Azure.WebJobs.Host\Executors\FunctionExecutor.cs : 94

 

问题分析

查看异常日志:

1)从最后一行看, 根据方法 Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync 可以得出,代码已经进入Function平台级别。可以初步排除是自己写的代码错误。

2)在逐行上看,发现 C:\azure-webjobs-sdk-extensions\src\WebJobs.Extensions.SendGrid\Client\SendGridClient.cs : 23 中,调用了 Client.SendGridClient.SendMessageAsync(SendGridMessage msg,CancellationToken cancellationToken) 并抛出异常。

因为Azure Funciton的Source Code已经开源,所以可以在Github中查看到 WebJobs.Extensions.SendGrid\Client\SendGridClient.cs 的源代码。

注意: 代码中直接调用SendGird SDK中的Client对象,发送邮件 SendEmailAsync。

 

3) 继续向上查看,发现进入 SendGrid SendGrid.SendGridClient.MakeRequest(HttpRequestMessage request,CancellationToken cancellationToken) 中。同第二步一样,继续在Github中搜索SendGrid的源码,查看MakeRequest方法中进行了那些操作。

 

 

注意:方法MakeRequest中也没有多余配置,只是更深一步传递 Request请求。然后对获取的结果进行异常处理或成功还回,外加设置响应编码为UTF8

 

4) 继续向上查看,进入 SendGrid.Helpers.Reliability.RetryDelegatingHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) 中。在此,发现可以设置以下四种条件的Retry次数。

private static readonly List<HttpStatusCode> RetriableServerErrorStatusCodes =
            new List<HttpStatusCode>()
            {
                HttpStatusCode.InternalServerError,
                HttpStatusCode.BadGateway,
                HttpStatusCode.ServiceUnavailable,
                HttpStatusCode.GatewayTimeout,
            };

RetryDeletegatingHandler.SendAsync 代码:

注意:如果设置了MaximumNumberOfRetries值,则当发送请求到SendGrid不成功时,会自动重试所设置的次数。重试次数必须在0 ~ 5 次之间。

if (maximumNumberOfRetries < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), "maximumNumberOfRetries must be greater than 0");
            }
            if (maximumNumberOfRetries > 5)
            {
                throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), "The maximum number of retries allowed is 5");
            }

 

5) 继续向上查看,发现后期的方法全是 System.Net.Http 类中对HTTP请求的处理。至此,代码查看到达源端。

 

6)回看异常消息 System.Threading.Tasks.TaskCanceledException : The operation was canceled.  这是因为参数 cancellationToken 的原因。因为请求为异步 Task操作。当在请求长时间没有处理完成并且cancellationToken中设置的取消时间已到,当前任务会被取消并抛出以上消息:The operation was canceled.

 

为什么需要取消令牌(CancellationToken) ?

因为Task没有方法支持在外部取消Task,只能通过一个公共变量存放线程的取消状态,在线程内部通过变量判断线程是否被取消,当CancellationToken是取消状态,Task内部未启动的任务不会启动新线程。

(Source: https://www.cnblogs.com/fanfan-90/p/12660996.html)

 

通过查看源码,我们找到了为什么出现TaskCanceledException的异常,但是为什么 Function App 没有设置SendGrid的重试参数呢? 继续查看源码,发现,Function App在初始化SendGrid Client对象时,并没有做任何的配置修改。所以Retry默认保持为0.

/// <summary>
/// Gets the maximum number of retries to execute against when sending an HTTP Request before throwing an exception. 
/// Defaults to 0 (no retries, you must explicitly enable).
/// </summary>
public int MaximumNumberOfRetries { get; }

Azure Function 的 SendGridOptions.cs 源文件中,并没有任何MaximumNumberOfRetries设置。

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using SendGrid.Helpers.Mail;
namespace Microsoft.Azure.WebJobs.Extensions.SendGrid
{
    /// <summary>
    /// Defines the configuration options for the SendGrid binding.
    /// </summary>
    public class SendGridOptions
    {
        /// <summary>
        /// Gets or sets the SendGrid ApiKey. If not explicitly set, the value will be defaulted
        /// to the value specified via the 'AzureWebJobsSendGridApiKey' app setting or the
        /// 'AzureWebJobsSendGridApiKey' environment variable.
        /// </summary>
        public string ApiKey { get; set; }
        /// <summary>
        /// Gets or sets the default "to" address that will be used for messages.
        /// This value can be overridden by job functions.
        /// </summary>
        /// <remarks>
        /// An example of when it would be useful to provide a default value for 'to' 
        /// would be for emailing your own admin account to notify you when particular
        /// jobs are executed. In this case, job functions can specify minimal info in
        /// their bindings, for example just a Subject and Text body.
        /// </remarks>
        public EmailAddress ToAddress { get; set; }
        /// <summary>
        /// Gets or sets the default "from" address that will be used for messages.
        /// This value can be overridden by job functions.
        /// </summary>
        public EmailAddress FromAddress { get; set; }
    }
}

 

参考资料

Azure Functions SendGrid 绑定: https://docs.azure.cn/zh-cn/azure-functions/functions-bindings-sendgrid?tabs=csharp

WebJobs.Extensions.SendGrid: https://github.com/Azure/azure-webjobs-sdk-extensions/tree/dev/src/WebJobs.Extensions.SendGrid

SendGrid: https://github.com/sendgrid/sendgrid-csharp/tree/main/src/SendGrid

多线程笔记-CancellationToken(取消令牌):https://www.cnblogs.com/fanfan-90/p/12660996.html

相关文章
|
3月前
|
存储 安全 数据安全/隐私保护
【Azure Function App】在Function App中使用System Managed Identity访问Storage Account
本文介绍了如何在Azure Function中使用托管身份(Managed Identity)替代AzureWebJobsStorage连接函数应用到存储账户,以提高安全性并减少Access Key的使用。具体步骤包括:1) 启用系统分配的身份;2) 为函数应用授予存储访问权限,添加必要角色(如Storage Blob Data Contributor);3) 配置`AzureWebJobsStorage__blobServiceUri`参数指定Blob Service Uri。完成后删除旧配置,即可通过Managed Identity访问Storage Account。
131 20
|
2月前
|
安全 Linux 开发工具
【Azure Function】分享把Function App从.NET 6.0升级到.NET 8.0 Isolated的步骤
本文介绍了将Azure Function App从.NET 6.0升级到.NET 8.0 Isolated的步骤。.NET 6.0作为长期支持版本,生命周期至2024年11月结束。为确保持续支持,建议升级至更新版本。升级步骤包括安装.NET 8 SDK、更新Azure Functions Core Tools、修改项目文件目标框架为net8.0、更新兼容的NuGet包、本地测试以及重新发布到Azure。更多详细信息可参考官方文档。
110 9
|
2月前
|
存储 数据可视化 开发工具
【Application Insights】Application Insights存储的Function App的日志存在"Operation Link" 为空的情况
在将 Azure Functions 升级到 .NET 8 和 Isolated Worker 模式后,Application Insights 的请求日志中 `operation_Link` 字段为空,导致分布式追踪无法正常关联。解决方法包括:确保引用正确的 SDK 包(如 `Microsoft.Azure.Functions.Worker.ApplicationInsights`),正确配置 Application Insights 服务,移除默认日志过滤规则,并使用最新依赖包以支持分布式追踪。通过这些步骤,可恢复端到端事务视图的可视化效果。
64 10
|
3月前
|
数据采集 数据可视化 数据挖掘
基于Python的App流量大数据分析与可视化方案
基于Python的App流量大数据分析与可视化方案
|
6月前
|
C#
【Azure Function】Function App出现System.IO.FileNotFoundException异常
Exception while executing function: xxxxxxx,The type initializer for 'xxxxxx.Storage.Adls2.StoreDataLakeGen2Reading' threw an exception. Could not load file or assembly 'Microsoft.Extensions.Configuration, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'. The system cannot find the
162 64
|
4月前
|
存储 文件存储 Android开发
仿第八区APP分发下载打包封装系统源码
该系统为仿第八区APP分发下载打包封装系统源码,支持安卓、iOS及EXE程序分发,自动判断并稳定安装。智能提取应用信息,自动生成PLIST文件和图标,提供合理的点数扣除机制。支持企业签名在线提交、专属下载页面生成、云端存储(阿里云、七牛云),并优化签名流程,支持中文包及合并分发,确保高效稳定的下载体验。 [点击查看源码](https://download.csdn.net/download/huayula/90463452)
265 22
|
5月前
|
小程序 搜索推荐
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
线下陪玩约玩APP旨在满足现代人的社交、兴趣分享、专业指导及休闲娱乐需求。用户可通过平台结识新朋友、找到志同道合的伙伴,并享受高质量的陪玩服务。平台提供用户注册登录、陪玩师筛选与预约、实时沟通等功能,支持个性化游戏体验和高效匹配。
188 0
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
|
5月前
|
JavaScript API
【Azure Function】Function App门户上的Test/Run返回错误:Failed to fetch
Running your function in portal requires the app to explicitly accept requests from https://portal.azure.cn. This is known as cross-origin resource sharing (CORS).Configure CORS to add https://portal.azure.cn to allowed origins.
135 7
|
5月前
|
前端开发 Java 测试技术
语音app系统软件源码开发搭建新手启蒙篇
在移动互联网时代,语音App已成为生活和工作的重要工具。本文为新手开发者提供语音App系统软件源码开发的启蒙指南,涵盖需求分析、技术选型、界面设计、编码实现、测试部署等关键环节。通过明确需求、选择合适的技术框架、优化用户体验、严格测试及持续维护更新,帮助开发者掌握开发流程,快速搭建功能完善的语音App。
|
5月前
|
安全 JavaScript 前端开发
小游戏源码开发之可跨app软件对接是如何设计和开发的
小游戏开发团队常需应对跨平台需求,为此设计了成熟的解决方案。流程涵盖游戏设计、技术选型、接口设计等。首先明确游戏功能与特性,选择合适的技术架构和引擎(如Unity或Cocos2d-x)。接着设计通用接口,确保与不同App的无缝对接,并制定接口规范。开发过程中实现游戏逻辑和界面,完成登录、分享及数据对接功能。最后进行测试优化,确保兼容性和性能,发布后持续维护更新。

热门文章

最新文章