根据时区安排在一天中的特定时间进行hangfire作业

时间:2019-10-17 21:00:52

标签: c# .net timezone hangfire timezone-offset

在hangfire中,我可以安排作业在calling a method with delay之前的特定时间运行

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

我有一张包含以下信息的表

    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

鉴于此信息,我想为每个用户每天根据其时区在配置的时间发送通知

ScheduleNotices方法将在世界标准时间每天凌晨12点运行。此方法将安排当天需要运行的作业。

 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 );
       }
 }

更新1
Schedules表信息不断变化。用户可以选择添加/删除时间。我可以创建一个运行于每分钟的循环作业(分钟是hangfire支持的最小单位),然后该循环作业可以查询Schedules表并根据时间表发送通知。
但是,由于数据库交互过多。因此,我将只有一个重复的作业ScheduleNotices,它将在上午12点(一天一次)运行,并将作业安排在接下来的24小时内。在这种情况下,他们所做的任何更改将从第二天开始生效。

2 个答案:

答案 0 :(得分:0)

我想我明白了。我在int表中又添加了Schedules列,然后我的代码如下

LastScheduledDateTime是将在ScheduleNotices每天运行的定期作业。该作业将安排当天需要执行的其他作业

12.00 AM

答案 1 :(得分:0)

Your answer非常接近。有几个问题:

  • 您假设给定时区中的今天与UTC中的今天是同一日期。根据时区,这些日期可能会有所不同。例如,2019年10月18日世界标准时间凌晨1点,美国中部时间2019年10月17日下午8:00。

  • 如果您围绕“今天是否发生过”进行设计,则可能会跳过合法事件。相反,想一想“下一个未来发生的事情”要容易得多。

  • 您没有采取任何措施来处理无效或含糊的当地时间,例如DST的开始或结束以及标准时间的更改。这对于重复发生的事件很重要。

继续执行代码:

// 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;
    }
}

我认为,如果将LastScheduledDateTime设为DateTimeOffset?而不是DateTime?,将会发现代码和数据更加清晰。上面的代码假定。如果您不想 ,则可以将最后一行更改为:

        schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;

还要注意使用ToDateTimeOffset,它是扩展方法。将其放在某个地方的静态类中。其目的是在考虑特定时区的情况下从DateTimeOffset创建一个DateTime。在处理模棱两可和无效的本地时间时,它会应用典型的调度问题。 (如果您想了解更多,我上次发布的文章是in this other Stack Overflow answer。)这是实现:

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));
}

(对于您而言,这种类型始终是未指定的,因此您可以根据需要删除该第一项检查,但是我更喜欢在其他用途​​的情况下使其完全起作用。)

顺便说一句,您不需要进行if (!schedules.HasAny()) { return; }检查。实体框架已经在SaveChangesAsync期间测试了更改,如果没有更改,则不执行任何操作。