0

In my app I have a handful of BackgroundServices. The problem is, that they run on unscheduled times. Sometimes exactly a day too late, sometimes two days in a row even though it should only run once a week.

This is how one of the BackgroundServices looks like:

using Cronos;

namespace MyApp.HostedServices
{

    public class MyFirstService : BackgroundService
    {

        private const string schedule = "0 1 0 ? * *";
        private readonly CronExpression _cron;

        public MyFirstService()
        {
            _cron = CronExpression.Parse(schedule, CronFormat.IncludeSeconds);
        }

        protected override async Task ExecuteAsync(CancellationToken stopToken)
        {
            while (!stopToken.IsCancellationRequested)
            {

                DateTime utcNow = DateTime.UtcNow;
                DateTime? nextUtc = _cron.GetNextOccurrence(utcNow);
                await Task.Delay(nextUtc.Value - utcNow, stopToken);
                if (utcNow.Day == 1) await DoWork();

                /*
                DateTime now = DateTime.Now;
                DateTime start = now.Date.AddMinutes(1);
                if (now > start) start = start.AddDays(1);
                await Task.Delay(start.Subtract(now), stopToken);
                if (now.Day == 1) await DoWork();
                */

            }
        }

        protected async Task DoWork()
        {
            // The work to be done
        }

    }

}

Another example:

using Cronos;

namespace MyApp.HostedServices
{

    public class MySecondService : BackgroundService
    {

        private const string schedule = "0 0 13 ? * *";
        private readonly CronExpression _cron;
        private readonly int[] reminderDays = { 1, 3, 6, 9 };

        public MySecondService()
        {
            _cron = CronExpression.Parse(schedule, CronFormat.IncludeSeconds);
        }

        protected override async Task ExecuteAsync(CancellationToken stopToken)
        {
            while (!stopToken.IsCancellationRequested)
            {

                DateTime utcNow = DateTime.UtcNow;
                DateTime? nextUtc = _cron.GetNextOccurrence(utcNow);
                await Task.Delay(nextUtc.Value - utcNow, stopToken);
                if (reminderDays.Contains(utcNow.Day)) await DoWork();

                /*
                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 to be done
        }

    }

}

I've asked the same question before and was adviced to use cron to calculate the next run instead of doing it myself. At first I thought that did it, but the problem persists.

For example did the MySecondService run both the sixth and seventh this month.

What could be wrong?

Mads
  • 385
  • 1
  • 5
  • 18
  • 1
    I think you can consider using quartz.net for that purpose, it will handle scheduling for you – Kamil Budziewski May 10 '22 at 07:18
  • You could try DateTimeOffset – NickD May 10 '22 at 07:20
  • 1
    @KamilBudziewski you mean https://www.quartz-scheduler.net? Why should that be better than Cronos? – Mads May 10 '22 at 07:23
  • @NickD Could you be more specific how you think I should do that, please? – Mads May 10 '22 at 07:24
  • 1
    Use cronos of quartz.net for this. Problem with your approach is that if the app is restarten for whatever reason (scaling, deployment, manually) your schedule isn't going to work. Libs like quartz.net persist state so it is much more reliable. – Peter Bons May 10 '22 at 07:29
  • persistance of quartz.net + automatic handling of job scheduling rather than creating custom backgroundservice – Kamil Budziewski May 10 '22 at 08:02
  • When should the `DoWork` run? At what hours, days and months? – Theodor Zoulias May 10 '22 at 09:51
  • @KamilBudziewski I've setup quartz.net and will monitor it for a couple of days to confirm that it runs at the specific times. – Mads May 10 '22 at 12:03
  • @TheodorZoulias "0 0 13 ? * *" It runs every day and if the day is in reminderDays array, then it should run. – Mads May 10 '22 at 12:03
  • @Mads why don't you embed the `reminderDays` in the cron expression, as shown by D-Shih in their [answer](https://stackoverflow.com/questions/72099517/backgroundservice-runs-on-unscheduled-times/72099765#72099765), like this: `0 0 13 1,3,6,9 * *`? – Theodor Zoulias May 10 '22 at 15:28
  • @TheodorZoulias I also am now after changing to quartz.net. It's mostly a leftover from before using cron, when I was calculating "next run" to use with Task.Delay. But I can't see how it'll change anything. If the cron runs as expected the reminderDays will contain the day. – Mads May 10 '22 at 18:01
  • @Mads if you don't trust the [quite well tested](https://github.com/HangfireIO/Cronos/blob/master/tests/Cronos.Tests/CronExpressionFacts.cs) Cronos library, you might as well not use it at all, and revert to calculating the next occurrence manually. – Theodor Zoulias May 10 '22 at 18:45
  • @TheodorZoulias I do trust the library, but I felt that I could better log what happened for a period of time by running it every day and then only execute 'DoWork' at the reminderDays. – Mads May 10 '22 at 18:52
  • @Mads logging the behavior of the library is one thing. Trying to proactivelly fix the errors of the library by writing code like `if (reminderDays.Contains(utcNow.Day)) await DoWork();` is another. I don't judge you for doing the later, it might be important that the `DoWork` doesn't run at the wrong day of the month, but at least you should do the former also. This way you would know that the Cronos works correctly, and the bug is in your own code. – Theodor Zoulias May 10 '22 at 19:07

1 Answers1

2

I suspect your problem is here:

if (reminderDays.Contains(utcNow.Day))

utcNow is the time that the scheduled job started its loop. I.e., if it started up on Tuesday and the next trigger time was Wednesday, utcNow would refer to Tuesday, not Wednesday.

I'm pretty sure you want nextUtc there instead, which represents the actual time DoWork executes:

if (reminderDays.Contains(nextUtc.Value.Day))
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810