【问题标题】:How to run an async task daily in a Kestrel process?如何在 Kestrel 进程中每天运行异步任务?
【发布时间】:2020-09-01 22:23:17
【问题描述】:

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

不好的答案:

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

奖金:

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

有些人错过了这一点。方法是async。如果不是async,问题就不难了。

【问题讨论】:

  • @GuruStron:看起来很像,但计时器不能异步。此外,计时器将在与 task.delay 相同的毫秒数上发生故障。
  • 一些配置设置告诉进程何时更新,即一天中的时间或您需要的任何内容。然后在您的 HostedService 中,而不是 Task.Delay 为 x 小时,让它休眠一个不会抛出的较短时间,当它醒来时,检查您的配置设置,检查一天中的时间/执行计算以查看是否它应该更新与否。如果它醒得太早/太晚,您可以建立一些容忍度。
  • 您可以将初始 Task.Delay 设置得很低,并根据更新前需要多少时间重新调整它,某种指数退避。你可以试试 Quartz.net andrewlock.net/…
  • 定时器可以是异步的:stackoverflow.com/a/38918443/1204153 这是运行定时器的IHostedService 的工作。没有其他合乎逻辑的方法可以做到这一点。
  • 另外,定时器的最大间隔是2,147,483,647毫秒……大约25天。它还可以选择在启动时立即触发。即使您需要它每 40 天触发一次,您也可以对服务中的值进行倒计时。将间隔设置为 1 秒。 @GuruStron 的建议是正确的。

标签: asp.net async-await kestrel-http-server


【解决方案1】:

我将为此添加一个答案,因为这是在 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);
        }
    }
}

所以,我知道我们在 cmets 中讨论过这个,但是 System.Threading.Timer 回调方法被认为是一个事件处理程序。在这种情况下使用async voidperfectly acceptable,因为将在线程池线程上引发转义该方法的异常,就像该方法是同步的一样。无论如何,您可能应该在其中抛出 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>();

【讨论】:

  • 延迟循环被接受。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多