如何在Kestrel进程中每天运行一个异步任务?

问题描述

如何在极长的时间间隔(例如每天甚至更长)中在Kestrel进程中运行异步任务?该任务需要在Web服务器进程的内存空间中运行,以更新一些慢慢过期的全局变量。

错误答案:

  • 尝试使用OS调度程序是一个糟糕的计划。
  • 从控制器调用await是不可接受的。任务很慢。
  • 对于Task.Delay(),延迟时间太长(大约16小时左右,Task.Delay将会抛出)。
  • HangFire等在这里毫无意义。这是一项内存中的工作,它并不关心数据库中的任何内容。此外,无论如何,如果没有用户上下文(登录用户点击某个控制器),我们就无法调用数据库。
  • System.Threading.Timer。是可重入的。

奖金:

  • 任务是幂等的。旧的运行完全无关紧要。
  • 特定页面渲染是否错过更改无关紧要;下一个将很快得到它。
  • 由于这是一台Kestrel服务器,因此我们并不担心停止后台任务。无论如何,当服务器进程关闭时,它将停止。
  • 任务应在启动后立即运行一次。这应该使协调更加容易。

有些人错过了这个。该方法为async。如果不是async,这个问题将不会很困难。

解决方法

我将为此添加一个答案,因为这是在ASP.NET Core中完成此类操作的唯一逻辑方法:IHostedService实现。

这是实现IHostedService的不可重入计时器后台服务。

public sealed class MyTimedBackgroundService : IHostedService
{
    private const int TimerInterval = 5000; // change this to 24*60*60 to fire off every 24 hours
    private Timer _t;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Requirement: "fire" timer method immediatly.
        await OnTimerFiredAsync();

        // set up a timer to be non-reentrant,fire in 5 seconds
        _t = new Timer(async _ => await OnTimerFiredAsync(),null,TimerInterval,Timeout.Infinite);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _t?.Dispose();
        return Task.CompletedTask;
    }

    private async Task OnTimerFiredAsync()
    {
        try
        {
            // do your work here
            Debug.WriteLine($"{TimerInterval / 1000} second tick. Simulating heavy I/O bound work");
            await Task.Delay(2000);
        }
        finally
        {
            // set timer to fire off again
            _t?.Change(TimerInterval,Timeout.Infinite);
        }
    }
}

因此,我知道我们在评论中对此进行了讨论,但是System.Threading.Timer回调方法被视为事件处理程序。在这种情况下,perfectly acceptable使用async void,因为在线程池线程上会引发一个转义该方法的异常,就像该方法是同步的一样。无论如何,您可能应该在其中丢一个catch来记录任何异常。

您提出了在某个时间间隔边界不安全的计时器。我高低寻找这些信息,但找不到。我以24小时间隔,2天间隔,2周间隔使用计时器...我从来没有让它们失败过。我也有很多年在生产服务器的ASP.NET Core中运行。我们本来可以看到的。

好的,所以您仍然不信任System.Threading.Timer ...

比方说,不。。。没有使用计时器的怪癖。好,那很好...让我们再走一条路。让我们从IHostedService转到BackgroundService(这是IHostedService的实现),然后简单地倒数。

这将减轻对计时器边界的任何担心,并且您不必担心async void事件处理程序。这也是免费的不可重入帐户。

public sealed class MyTimedBackgroundService : BackgroundService
{
    private const long TimerIntervalSeconds = 5; // change this to 24*60 to fire off every 24 hours

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Requirement: "fire" timer method immediatly.
        await OnTimerFiredAsync(stoppingToken);

        var countdown = TimerIntervalSeconds;

        while (!stoppingToken.IsCancellationRequested)
        {
            if (countdown-- <= 0)
            {
                try
                {
                    await OnTimerFiredAsync(stoppingToken);
                }
                catch(Exception ex)
                {
                    // TODO: log exception
                }
                finally
                {
                    countdown = TimerIntervalSeconds;
                }
            }
            await Task.Delay(1000,stoppingToken);
        }
    }

    private async Task OnTimerFiredAsync(CancellationToken stoppingToken)
    {
        // do your work here
        Debug.WriteLine($"{TimerIntervalSeconds} second tick. Simulating heavy I/O bound work");
        await Task.Delay(2000);
    }
}

额外的副作用是,您可以使用long作为间隔时间,从而使事件有25天以上的时间触发,而Timer的上限是25天。

您可以这样注入其中的任何一个:

services.AddHostedService<MyTimedBackgroundService>();

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...