如何在.NET gRPC拦截器中启动长时间运行的后台任务
如何在.NET gRPC拦截器中启动长时间运行的后台任务
这种场景我之前做gRPC服务的时候碰到过,刚好可以给你一套落地的解决方案,咱们一步步来拆解实现哈~
先再明确下你的需求:你在ASP.NET Core的gRPC服务里,要调用一段可能长时间运行、本身还不支持取消的代码;希望超时后先给客户端返回取消响应,同时让后台代码继续跑,等它真正执行完再发个事件通知——用拦截器来做这个思路完全没问题,下面给你具体的实现步骤:
第一步:实现gRPC拦截器,包装调用逻辑
我们需要继承gRPC的Interceptor类,根据你的调用类型(一元/流式)重写对应的处理方法。核心思路是:给原始调用加超时控制,超时后就把任务丢到后台队列里继续跑,同时给客户端返回响应。比如针对最常用的一元gRPC调用,示例代码是这样的:
public class LongRunningTaskInterceptor : Interceptor { private readonly IEventPublisher _eventPublisher; // 假设这是你用来发事件的服务 private readonly IBackgroundTaskQueue _taskQueue; // 用后台任务队列托管长任务,避免依赖请求上下文 public LongRunningTaskInterceptor(IEventPublisher eventPublisher, IBackgroundTaskQueue taskQueue) { _eventPublisher = eventPublisher; _taskQueue = taskQueue; } public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { // 先定义超时时间,比如30秒,你可以根据业务调整 var timeout = TimeSpan.FromSeconds(30); var timeoutCts = new CancellationTokenSource(timeout); // 把请求的取消令牌和超时令牌绑在一起 var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken, timeoutCts.Token); try { // 尝试在超时时间内完成原始调用 return await continuation(request, context).WaitAsync(combinedCts.Token); } catch (OperationCanceledException) { // 区分是超时导致的取消,还是客户端主动取消 if (timeoutCts.IsCancellationRequested) { // 超时了,把长任务丢到后台队列,不用等它完成 _taskQueue.QueueBackgroundWorkItem(async token => { try { // 这里重新执行那段长运行代码,注意!绝对不能用原来的ServerCallContext // 要把request里的必要数据提前取出来传进去,别依赖请求上下文 var taskResult = await ExecuteLongRunningLogic(request, token); // 任务完成后发布事件 await _eventPublisher.PublishTaskCompletedEvent(taskResult); } catch (Exception ex) { // 后台任务的异常一定要处理,比如打日志,不然静默失败你都不知道 Console.WriteLine($"后台任务执行出错:{ex.Message}"); } }); // 给客户端返回取消响应,说明后台还在跑 context.Status = new Status(StatusCode.Cancelled, "请求超时,后台任务已继续执行"); return default!; } // 如果是客户端主动取消的,就直接抛出异常就行 throw; } } // 这里模拟你那坨不支持取消的长运行代码 private async Task<YourTaskResultType> ExecuteLongRunningLogic<TRequest>(TRequest request, CancellationToken token) { // 注意:如果原代码真的不支持取消,那token可能用不上,但一定要确保不依赖请求资源 // 替换成你实际的业务逻辑就行 await Task.Delay(TimeSpan.FromMinutes(5)); // 模拟长时间运行 return new YourTaskResultType(); } }第二步:实现后台任务队列,托管长任务
为啥要用后台队列?因为ASP.NET Core的请求上下文在请求结束后会被回收,如果直接用Task.Run跑后台任务,很可能会因为依赖了请求资源导致异常或者内存泄漏。用IBackgroundTaskQueue结合BackgroundService是官方推荐的后台任务托管方式,稳定可靠。先定义队列接口和实现:
public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem); Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly Channel<Func<CancellationToken, Task>> _queue; public BackgroundTaskQueue(int capacity) { // 这里的容量是队列最大长度,根据你的并发量调整 _queue = Channel.CreateBounded<Func<CancellationToken, Task>>(capacity); } public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem) { if (workItem == null) throw new ArgumentNullException(nameof(workItem)); _queue.Writer.TryWrite(workItem); } public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { return await _queue.Reader.ReadAsync(cancellationToken); } }再实现一个托管服务来处理队列里的任务:
public class QueuedHostedService : BackgroundService { private readonly ILogger<QueuedHostedService> _logger; private readonly IBackgroundTaskQueue _taskQueue; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger) { _taskQueue = taskQueue; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("后台任务队列服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { var workItem = await _taskQueue.DequeueAsync(stoppingToken); await workItem(stoppingToken); } catch (OperationCanceledException) { // 服务停止时的正常取消,不用管 } catch (Exception ex) { _logger.LogError(ex, "后台任务执行失败"); } } _logger.LogInformation("后台任务队列服务已停止"); } }第三步:把所有服务注册到DI容器
最后在Program.cs里,把拦截器、后台队列和托管服务都注册进去,让DI容器管理它们的生命周期:// 注册后台任务队列,容量设为100,根据你的并发需求调整 builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => new BackgroundTaskQueue(100)); // 注册后台托管服务 builder.Services.AddHostedService<QueuedHostedService>(); // 注册gRPC拦截器 builder.Services.AddGrpc(options => { options.Interceptors.Add<LongRunningTaskInterceptor>(); });
几个一定要注意的点
- 绝对不要依赖请求上下文:后台任务里不能用
HttpContext或者ServerCallContext里的任何请求相关资源!请求结束后这些资源会被ASP.NET Core回收,直接用会导致各种奇怪的异常或者内存泄漏。一定要把需要的数据提前从request里提取出来,传入后台任务。 - 异常处理必须到位:后台任务跑在后台,异常不会直接暴露给客户端,所以一定要加日志或者监控,不然静默失败了你都没法排查问题。
- 超时时间要贴合业务:别拍脑袋设个时间,要根据你的业务场景测试后调整,比如如果是大数据计算,超时时间可以设长一点;如果是普通查询,就设短一些。
- 流式调用要对应调整:如果你的gRPC方法是流式的(比如服务端流式、双向流式),要对应重写
ServerStreamingServerHandler或者DuplexStreamingServerHandler,核心逻辑和一元调用类似,只是处理流式的方式略有不同。
内容来源于stack exchange
相关产品推荐
相关产品推荐

