vlambda博客
学习文章列表

【踩坑】action重复执行和gRPC的重试策略

This browser does not support music or audio playback. Please play it in Weixin or another browser.
最近工作中遇到了两个奇葩的问题,源于对底层的不了解导致的。

问题1:测试反馈,有个邮件发送接口在点击后,会发两封响应邮件(.NET6 MVC async action)。


说实在的,当时我听到这个问题比较淡定,这种问题一般是重复点击导致的。当我打开Chrome的开发者工具时,看到点击后只发送了一条HTTP请求,我有点迷茫,不知道该如何排查。只好先在action上打断点调试,呃,断点执行了两次。这个就有点尴尬,我有点不相信chrome的网络请求次数了,打开了fiddler,请求也只有一次。那么无论再怎么觉得不可思议,问题也只能出现在我的代码上了。我只好搜关键词“mvc 重复执行”,出现的结果都是请求多次发送的问题。经过一段时间的折腾后,还是没有任何头绪。而且查看了日志,也是多条执行的记录。最后只能一步步调试,看流程怎么执行的。然后我就惊呆了。逻辑执行结束后,断点进入了一个过滤器中,开始执行日志写入逻辑,然后又进入了action中,重新执行页面逻辑。真相大白了。最后的那段调用基类的方法使action重复执行!!!需要删除所有调用基类方法的代码。
 /// <summary> /// 控制器耗时统计 /// </summary> public class LogActionFilter : ActionFilterAttribute { private readonly ILogger<LogActionFilter> _logger; private readonly Common _common; public LogActionFilter(ILogger<LogActionFilter> logger, Common common) { _logger = logger; _common = common; }
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var id = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier; var dt = DateTime.Now; if (id == null) { _logger.LogInformation("id 为空,不继续后面逻辑!"); } else { _logger.LogInformation($"id:{id}!"); } await next(); if (id == null) { _logger.LogInformation("id 为空,不继续后面逻辑!"); } else { if (context.HttpContext.Response.StatusCode == HttpStatusCode.Redirect.GetHashCode()) { _logger.LogInformation("302,不继续后面逻辑!"); _ = base.OnActionExecutionAsync(context, next); return; } _logger.LogInformation($"StatusCode={context.HttpContext.Response.StatusCode}"); var ts = DateTime.Now - dt; _common.WriteLog(OperationType.View, context.HttpContext, ts); } await base.OnActionExecutionAsync(context, next); } }


至于为什么用OnActionExecutionAsync方法,因为里面日志写入涉及了异步的代码,开始和结束方法不支持异步。之前、之后就是自定义业务逻辑。


问题2:gRPC服务隔段时间报错。

查了下日志,都是不可用状态。有点好奇出现的原因,因为gRPC是一个成熟组件不应该出现这种明显的错误。我在这位开发兄弟的博文上得到了了解(https://beckjin.com/2019/09/25/grpc-docker-swarm/)。我们线上也是docker swarm,也会存在这个问题,奇怪的是测试环境没出现这个问题。因为gRPC服务的服务器我们改不了,我只好采用重试策略来进行恢复。但在这块又遇到了另一个问题。就是异步调用gRPC方法时无法使用Polly进行重试(拦截器方法,如果是同步方法可以正常使用)。只能写一个公共的方法来处理gRPC的异步调用。

 public async Task<TResponse> GrpcPollyAsync<TResponse>(Func<AsyncUnaryCall<TResponse>> func) { return await Policy.Handle<RpcException>(t => t.Status.StatusCode == StatusCode.Unavailable) .RetryAsync(3, (exception, retryCount) => { _logger.LogError(exception, $"输出错误。进行重试。Retry {retryCount}。"); }) .ExecuteAsync(async () => await func()); }        // 调用 var response = await _polly.GrpcPollyAsync(() => _searchClient.QueryAsync(request));