0

I'm trying to synchronize a Hangfire recurring job with the clock. Basically, I want to have a recurring job starting by the next hour and then on each hour.

Example: if the current time is 9:04 PM, the recurring job should be as following: 10:00 PM -> 11:00 PM, 00:00 AM, 01:00 AM, 02:00 AM, etc.

It is similar to my previous question: C# Timer ticking on each rounded hour (literally), but using Hangfire.

In the code below, I tried to make it first by scheduling a BackgroundJob for the minutes left until the next hour and then RecurringJob for each hour after BackgroundJob was executed. The problem is that it ticks at random times.

private DateTime RoundCurrentToNextOneHour()
{
    DateTime now = DateTime.Now, result = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0);
    return result.AddMinutes(((now.Minute / 60) + 1) * 60);
}

public Task StartAsync(CancellationToken cancellationToken)
{
    _logger.LogInformation("Timed Background Service is starting.");

    BackgroundJob.Schedule(() => StartRecurringJob(), RoundCurrentToNextOneHour());

    return Task.CompletedTask;
}

public void StartRecurringJob()
{
    RecurringJob.AddOrUpdate(() => DoWork(), Cron.Hourly, TimeZoneInfo.Local);
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • How random? Seconds or minutes kinda random? – Gary Stewart Aug 15 '19 at 18:30
  • Randomly like it doesn't happen exactly in the next hour but a few minutes later after that hour. I did test on each 5 minutes: 09:35 PM, 09:40 PM, 09:45 PM, etc. and it wasn't accurate. – nop Aug 15 '19 at 18:34
  • Your math seems off. `now.Minute` can only return values in the range `0-59`. Thus `now.Minute / 60` is always `0`, and thus you are always adding `60` minutes regardless of the input. Is that what you intended? – Matt Johnson-Pint Aug 15 '19 at 18:42
  • @MattJohnson-Pint, I have no clue but I modified it to be on each 5 minutes: `return result.AddMinutes(((now.Minute / 5) + 1) * 5);` and that's the result: `Timed Background Service is starting. Current time: 8/15/2019 9:52:48 PM Timed Background Service is working. Current time: 8/15/2019 9:53:04 PM Timed Background Service is working. Current time: 8/15/2019 9:54:04 PM`. The first tick is supposed to be at 9:55 and it is at 9:53:04 which is wrong. And by the way the recurring job is happening on each minute, so it's correct. What's not correct is the first tick. – nop Aug 15 '19 at 18:56

2 Answers2

0

In this line of code, you are always adding 60 minutes.

result.AddMinutes(((now.Minute / 60) + 1) * 60)

This is because the Minute property can only be in the values 0 through 59, and thus reduces as:

result.AddMinutes((0 + 1) * 60)
result.AddMinutes(1 * 60)
result.AddMinutes(60)

Try this instead:

result.AddMinutes((60 - (now.Minute % 60)) % 60)

For example, 09:00 will come out as 09:00, but 09:01 through 09:59 will round up to 10:00. It sounds like this was the behavior you were asking for.

You might also want to read in the HangFire docs about the SchedulePollingInterval, which could affect your results.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
0

There is a better way to do that using cron expressions. If you open https://crontab.guru and try out some expressions, you will find an explanation for each of them.

A few examples with cron expressions:

  • on each 5 minutes: 0/5 * * * *.

If the current time was 11:12 PM, it would execute a method at 11:15 PM, then at 11:20 PM and so on.

  • on each hour would be 0 * * * *.

If the current time was 11:12 PM, it would execute a method at 00:00 AM, 01:00 AM, 02:00 AM and so on

In addition to the cron expressions, there are built-in cron expressions. Their source code is here: https://github.com/HangfireIO/Hangfire/blob/9cd09f38fa97e4c2dd48f6097985fd2b48b4568e/src/Hangfire.Core/Cron.cs#L231. If I wanted to do some action on each 1 hour, it would be Cron.Hourly().

Example code is given below:

RecurringJobManager manager = new RecurringJobManager();
manager.RemoveIfExists("myjob");

// Each 5 minutes, e.g. 01:05 pm, 01:10 pm, 01:15 pm, etc.
//manager.AddOrUpdate("myjob", Job.FromExpression(() => DoWork()), $"0/5 * * * *", TimeZoneInfo.Local);

// Each 1 hour, e.g. 01:00 pm, 02:00, 03:00 pm, etc.
manager.AddOrUpdate("myjob", Job.FromExpression(() => DoWork()), $"0 * * * *", TimeZoneInfo.Local);
nop
  • 4,711
  • 6
  • 32
  • 93
  • By running it on each 5 minutes, it seems to have some drift over the time. Perhaps it might be because the chron expression doesn't contain `seconds` and when it increments it by 5, it doesn't take the seconds into account which later leads to a drift over the time. However, I think it will be fine on each hour, because the chron expression basically says tick `at minute 0`. I would like to get a more competent explanation on that and if someone finds a solution. – nop Aug 15 '19 at 20:40