Schedule a hangfire job at specific time of the day based on time zone

帅比萌擦擦* 提交于 2020-01-24 22:10:08

问题


In hangfire i can schedule a job to run at a specific time by calling a method with delay

BackgroundJob.Schedule(
           () => Console.WriteLine("Hello, world"),
           TimeSpan.FromDays(1));

I have a table with following information

    User           Time              TimeZone
    --------------------------------------------------------
    User1          08:00:00           Central Standard Time
    User1          13:00:00           Central Standard Time
    User2          10:00:00           Eastern Standard Time
    User2          17:00:00           Eastern Standard Time
    User3          13:00:00           UTC

Given this information, For every user i want to send notice every day at configured time based on their time zone

ScheduleNotices method will run everyday at 12 AM UTC. This method will schedule jobs that needs to run that day.

 public async Task ScheduleNotices()
 {
       var schedules = await _dbContext.GetSchedules().ToListAsync();
       foreach(var schedule in schedules)
       {
          // Given schedule information how do i build enqueueAt that is timezone specific
          var enqueuAt = ??;
          BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.User), enqueuAt );
       }
 }

Update 1
The Schedules table information keep changing. User has option to add/delete time. I can create a recurring job that runs every minuet (minute is minimum unit hangfire supports) and then this recurring job can query Schedules table and send notices based the time schedule.
However that too much database interaction.So instead i will have only one recurring job ScheduleNotices that will run at 12 AM (once in a day) and will schedule jobs for next 24 hours. In this case any changes they make will be effective from next day.


回答1:


I think i got it. I added one more column in my Schedules table as LastScheduledDateTime and then my code looks like

ScheduleNotices is Recurring job that will run Daily at 12.00 AM. This job will schedules others jobs that needs to run that day

    public async Task ScheduleNotices()
    {
        var schedules = await _dbContext.Schedules
            .Include(x => x.User)
            .ToListAsync().ConfigureAwait(false);

        if (!schedules.HasAny())
        {
            return;
        }

        foreach (var schedule in schedules)
        {
            var today = DateTime.UtcNow.Date;

            // schedule notification only if not already scheduled for today
            if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value.Date < today)
            {
                //construct scheduled datetime for today
                var scheduleDate = new DateTime(today.Year, today.Month, today.Day, schedule.PreferredTime.Hours, schedule.PreferredTime.Minutes, schedule.PreferredTime.Seconds, DateTimeKind.Unspecified);

                // convert scheduled datetime to UTC
                schedule.LastScheduledDateTime = TimeZoneInfo.ConvertTimeToUtc(scheduleDate, TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone));

                //*** i think we dont have to convert to DateTimeOffSet since LastScheduledDateTime is already in UTC
                var dateTimeOffSet = new DateTimeOffset(schedule.LastScheduledDateTime.Value);

                BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), dateTimeOffSet);
            }
       }

        await _dbContext.SaveChangesAsync();
    }



回答2:


Your answer was pretty close. There were a few problems:

  • You were assuming that today in a given time zone was the same date as today in UTC. Depending on time zone, these could be different days. For example, 1 AM UTC on 2019-10-18, is 8:00 PM in US Central Time on 2019-10-17.

  • If you design around "has it happened yet today", you'll potentially skip over legitimate occurrences. Instead, it's much easier to just think about "what is the next future occurrence".

  • You weren't doing anything to handle invalid or ambiguous local times, such as occur with the start or end of DST and with changes in standard time. This is important for recurring events.

So on to the code:

// Get the current UTC time just once at the start
var utcNow = DateTimeOffset.UtcNow;

foreach (var schedule in schedules)
{
    // schedule notification only if not already scheduled in the future
    if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value < utcNow)
    {
        // Get the time zone for this schedule
        var tz = TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone);

        // Decide the next time to run within the given zone's local time
        var nextDateTime = nowInZone.TimeOfDay <= schedule.PreferredTime
            ? nowInZone.Date.Add(schedule.PreferredTime)
            : nowInZone.Date.AddDays(1).Add(schedule.PreferredTime);

        // Get the point in time for the next scheduled future occurrence
        var nextOccurrence = nextDateTime.ToDateTimeOffset(tz);

        // Do the scheduling
        BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), nextOccurrence);

        // Update the schedule
        schedule.LastScheduledDateTime = nextOccurrence;
    }
}

I think you'll find that your code and data are much clearer if you make your LastScheduledDateTime a DateTimeOffset? instead of a DateTime?. The above code assumes that. If you don't want to, then you can change that last line to:

        schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;

Also note the use of ToDateTimeOffset, which is an extension method. Place it in a static class somewhere. Its purpose is to create a DateTimeOffset from a DateTime taking a specific time zone into account. It applies typical scheduling concerns when dealing with ambiguous and invalid local times. (I last posted about it in this other Stack Overflow answer if you want to read more.) Here is the implementation:

public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
    if (dt.Kind != DateTimeKind.Unspecified)
    {
        // Handle UTC or Local kinds (regular and hidden 4th kind)
        DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
        return TimeZoneInfo.ConvertTime(dto, tz);
    }

    if (tz.IsAmbiguousTime(dt))
    {
        // Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
        TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
        TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
        return new DateTimeOffset(dt, offset);
    }

    if (tz.IsInvalidTime(dt))
    {
        // Advance by the gap, and return with the daylight offset  (2:30 ET becomes 3:30 EDT)
        TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
        TimeSpan gap = offsets[1] - offsets[0];
        return new DateTimeOffset(dt.Add(gap), offsets[1]);
    }

    // Simple case
    return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}

(In your case, the kind is always unspecified, so you could remove that first check if you want to, but I prefer to keep it fully functional in case of other usage.)

Incidentally, you don't need the if (!schedules.HasAny()) { return; } check. Entity Framework already tests for changes during SaveChangesAsync, and does nothing if there aren't any.



来源:https://stackoverflow.com/questions/58440682/schedule-a-hangfire-job-at-specific-time-of-the-day-based-on-time-zone

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!