使用“装饰者模式”捕获 BackgroundService 中的异常

前言

我们常用 BackgroundService 创建执行后台任务或执行长时间运行任务的进程。

但是在实际使用中,我们发现,如果 BackgroundService 在执行中发生异常,有时会抛出异常,中断程序;有时即使出错,也没有任何日志输出。

这显然是不对的,应该始终抛出异常。.

那么,这到底是怎么回事呢?

问题重现

通过检查我们的实现代码,我们发现,这和 await 出现的位置有关,如果在 await 执行前产生异常,则会抛出,否则静默。

例如下列代码:

//抛出异常
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    throw new Exception();
    await Task.Delay(5000);
}

//无异常
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await Task.Delay(5000);
    throw new Exception();
}

原因分析

查看 BackgroundService 的源代码[1],它实际是在启动时执行的 ExecuteAsync 方法:

public virtual Task StartAsync(CancellationToken cancellationToken)
{
    // Create linked token to allow cancelling executing task from provided token
    _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

    // Store the task we're executing
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    // If the task is completed then return it, this will bubble cancellation and failure to the caller
    if (_executingTask.IsCompleted)
    {
        return _executingTask;
    }

    // Otherwise it's running
    return Task.CompletedTask;
}

问题的关键在这里:

if (_executingTask.IsCompleted)
{
    return _executingTask;
}

return Task.CompletedTask;
  • 如果 ExecuteAsync 在 await 前报错,则 _executingTask.IsCompleted == true,返回了 _executingTask,因此可以捕获到到异常。

  • 如果 ExecuteAsync 在 await 前未报错,由于未执行 await ExecuteAsync(_stoppingCts.Token),程序会继续往下执行,则 _executingTask.IsCompleted == falseBackgroundService 将返回一个默认的 CompletedTask, 这样我们永远无法知道 _executingTask 是否有异常。(相关问题请参看我以前的文章《如何保证执行异步方法时不会遗漏 await 关键字》)

解决问题

虽然我们可以在 ExecuteAsync 方法实现内部使用 try-catch 来规避这类问题。

但是,既然知道了原因,我们能否在不修改现有 ExecuteAsync 方法实现的情况下解决?

我们的想法是,让 BackgroundService 始终检查 _executingTask 是否有异常

虽然原始的 BackgroundService 做不到,但是不妨碍我们装饰它一下。

装饰者模式

Decorator Pattern(装饰者模式),是指在不必改变原类文件和使用继承的情况下,动态地给一个对象添加一些额外的职责。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

在 .NET 框架中就大量使用了装饰者模式,比如 CryptoStream 就是 Stream 的装饰类,通过在写入底层之前加密数据,并在读取数据之前解密来扩展行为。

实现

我们创建 BackgroundService 的装饰类 CatchExceptionBackgroundService<T>

public class CatchExceptionBackgroundService<T> : BackgroundService where T : BackgroundService
{
    private readonly T _backgroundService;
    private Task _executingTask;

    public CatchExceptionBackgroundService(T backgroundService)
    {
        this._backgroundService = backgroundService;
    }
    public override Task StartAsync(CancellationToken cancellationToken)
    {
        var task = _backgroundService.StartAsync(cancellationToken);

        _executingTask = typeof(BackgroundService).GetField("_executingTask", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(_backgroundService) as Task;

        Timer timer = new Timer(CheckStatus,null,1000, 1000);

        return task;
    }

    public override Task StopAsync(CancellationToken cancellationToken)
    {
        return _backgroundService.StopAsync(cancellationToken);
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    { 
        return Task.CompletedTask;
    }
}
  • 使用构造函数注入实际的 BackgroundService
  • StartAsync 和 StopAsync调用实际的 BackgroundService 实例对应方法
  • 通过反射获取实际的 BackgroundService 实例中的 _executingTask
  • 定时检查 _executingTask 是否抛出异常

用于检查代的码如下:

public void CheckStatus(Object stateInfo)
{
    if (_executingTask != null && _executingTask.IsCompleted)
    {
        if (_executingTask.Exception is AggregateException exception)
        {
            _executingTask = null;
            throw exception.InnerException;
        }

        _executingTask = null;
    }
}

使用

只需修改 Startup.cs, 将原来的注册方法改为使用装饰类:

public void ConfigureServices(IServiceCollection services)
{
    //原来的注册方法
    //services.AddHostedService<DemoBackgroundService>();

    services.AddSingleton<DemoBackgroundService>();
    services.AddHostedService<CatchExceptionBackgroundService<DemoBackgroundService>>();
}

DemoBackgroundService 的实现代码如下:

public class DemoBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine("DemoBackgroundService.ExecuteAsync");
        await Task.Delay(5000);
        throw new Exception("DemoBackgroundService throw Exception");
    }
}

可以看到,工作正常:

使用“装饰者模式”捕获 BackgroundService 中的异常

结论

今天,我们使用装饰者模式,实现始终捕获 BackgroundService 产生的异常。