1

I have this BackgroundService:

public class ReminderService : BackgroundService
{

    private readonly int[] reminderDays = { 1, 3, 6, 9 };

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stopToken.IsCancellationRequested)
        {
            DateTime now = DateTime.Now;
            DateTime start = now.Date.AddHours(13);
            if (now > start) start = start.AddDays(1);
            await Task.Delay(start.Subtract(now), stopToken);
            if (reminderDays.Contains(now.Day)) await DoWork();
        }
    }

    protected async Task DoWork()
    {
        // The work...
    }

}

I want the service to run the first, third, sixth and ninth day each month at 1 PM. From what I can see in my log, the service rarely runs a few milliseconds past the expected time but often a day too late (2, 4, 7, 10 at around 1 PM).

If can almost only be in my Task.Delay calculation, but I don't know why.

I have other services running with different starts/delays but with the same problem.

Can anyone see what's wrong? I'm open for other approaches but I prefer not to use libraries like Hangfire.

Mads
  • 385
  • 1
  • 5
  • 18
  • 1
    Instead of just waiting for the next execution, I'd recommend using a library like [NCrontab](https://www.nuget.org/packages/ncrontab). There's an example of how it runs [here](https://medium.com/@gtaposh/net-core-3-1-cron-jobs-background-service-e3026047b26d) – DavidG May 03 '22 at 12:47
  • 3
    [Cronos](https://github.com/HangfireIO/Cronos) is also a good library for calculating next occurrences. It is Stephen Cleary's [personal preference](https://stackoverflow.com/questions/71883959/how-to-start-my-worker-service-on-the-hour/71885865#71885865). – Theodor Zoulias May 03 '22 at 12:51
  • @TheodorZoulias I have another service that runs once each month. From what I remember that's too big a number for the Task.Delay to handle, why I check each day and only executes if now.Day == 1. Is that the same approach I should use here, you think? – Mads May 03 '22 at 13:03
  • @Mads yes, the maximum delay of `Task.Delay` is a bit more than 24 days: [Task.Delay for more than int.MaxValue milliseconds](https://stackoverflow.com/questions/27995221/task-delay-for-more-than-int-maxvalue-milliseconds). – Theodor Zoulias May 03 '22 at 13:27
  • 1
    @TheodorZoulias I'll give that a try. One last thing: in the example you provided the delay is calculated with "utcNow - nextUtc.Value" - shouldn't that be the other way around: "nextUtc.Value - utcNow" ? – Mads May 03 '22 at 13:53
  • @Mads I think so. I left a comment [there](https://stackoverflow.com/a/71885865/11178549) for Stephen Cleary to check it out. Also FYI the maximum delay of `Task.Delay` [has been doubled](https://github.com/dotnet/runtime/pull/43708 "Extend allowed Task.Delay/CTS TimeSpan values to match Timer") on .NET 6, and it's now 49+ days. – Theodor Zoulias May 03 '22 at 16:28
  • @TheodorZoulias please create an answer :-) – Mads May 03 '22 at 19:57
  • @Mads I have nothing to add to the excellent D-Shih's [answer](https://stackoverflow.com/questions/72099517/backgroundservice-runs-on-unscheduled-times/72099765#72099765)! – Theodor Zoulias May 03 '22 at 20:02
  • @TheodorZoulias D-Shih did provide an excellent answer, but you were first with the solution that I went with. Do you want me to accept his answer? :-) – Mads May 03 '22 at 20:06
  • @Mads yea, D-Shih has provided a complete answer. My initial comment was just a reply to DavidG's [comment](https://stackoverflow.com/questions/72099517/backgroundservice-runs-on-unscheduled-times?noredirect=1#comment127393104_72099517). I am not a fan of answering questions in the comments. :-) – Theodor Zoulias May 03 '22 at 20:10
  • @TheodorZoulias I hoped I could as one last thing: I've implemented Cronos (did not fix the initial problem) but now the BackgroundServices runs two hours later than specified in the cron string ("0 0 13 ? * *" runs at 15pm). Is there some UTC/daylight that I should pay attention to? – Mads May 10 '22 at 07:19
  • Hi @Mads. You could consider posting your Cronos implementation as a new question, so that we can see where are the problems and offer suggestions to fix them. – Theodor Zoulias May 10 '22 at 09:32

1 Answers1

3

You might look for a scheduler quartz, which is very suitable to use the crontab format that is very suitable for the background scheduler.

If you want to find a light way crontab library Cronos or NCrontab that can only get the time by crontab.

For all Of them, I would prefer using Cronos library because that supports UTC and daylight saving time transitions.

public class ReminderService : BackgroundService
{

    private const string cronExpression= "0 13 1,3,6,9 * *"; 
    private readonly CronExpression _cronJob;
    public ReminderService()
    {
      _cronJob = CronExpression.Parse(cronExpression);
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
          var now = DateTime.UtcNow;
          var nextUtc = _cronJob.GetNextOccurrence(now);
          await Task.Delay(now - nextUtc.Value, stoppingToken);
          await DoWork();
        }
    }

    protected async Task DoWork()
    {
        // The work...
    }
}

Here is a very awesome website cron-expression-generator helps us get crontab expression by UI

D-Shih
  • 44,943
  • 6
  • 31
  • 51
  • Thanks, @D-Shih! I went with the "lightweight" solution, Cronos, that was also suggested by Theodor Zoulias in the question comments. It took me a second or two to find out, that Cronos do not support the "year" format that the generator outputs. Besides that everything seems to work. :-) – Mads May 03 '22 at 20:56