在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小时内。在这种情况下,他们所做的任何更改将从第二天开始生效。
答案 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
期间测试了更改,如果没有更改,则不执行任何操作。