Запланируйте работу при зависании в определенное время дня в зависимости от часового пояса. - PullRequest
1 голос
/ 18 октября 2019

В Hangfire я могу запланировать выполнение задания в определенное время, вызывая метод с задержкой

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:00 UTC. Этот метод будет планировать задания, которые должны быть выполнены в этот день.

 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:00 (один раз в день) и будет планировать задания на следующие 24 часа. В этом случае любые внесенные изменения вступят в силу со следующего дня.

Ответы [ 2 ]

0 голосов
/ 18 октября 2019

Ваш ответ был довольно близок. Было несколько проблем:

  • Вы предполагали, что сегодня в данном часовом поясе была та же дата, что и сегодня в UTC. В зависимости от часового пояса это могут быть разные дни. Например, 1:00 UTC 2019-10-18, это 8:00 вечера по центральному времени США 2019-10-17.

  • Если вы проектируете вокруг ", это уже произошлосегодня ", вы можете пропустить законные случаи. Вместо этого гораздо проще просто подумать о том, «что будет в будущем в будущем».

  • Вы не делали ничего, чтобы обрабатывать недопустимые или неоднозначные локальные времена, например, происходящие при запускеили конец летнего времени и с изменениями в стандартное время. Это важно для повторяющихся событий.

Итак, к коду:

// 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 a DateTimeOffset? вместо DateTime?. Приведенный выше код предполагает это. Если вы не хотите, то вы можете изменить эту последнюю строку на:

        schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;

Также обратите внимание на использование ToDateTimeOffset, который является методом расширения. Поместите это в статический класс где-нибудь. Его целью является создание DateTimeOffset из DateTime с учетом определенного часового пояса. Он применяет типичные проблемы планирования при работе с неоднозначным и недействительным местным временем. (Я в последний раз писал об этом в этом другом ответе переполнения стека , если вы хотите прочитать больше.) Вот реализация:

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; }. Entity Framework уже проверяет изменения во время SaveChangesAsync и ничего не делает, если их нет.

0 голосов
/ 18 октября 2019

Я думаю, что понял. Я добавил еще один столбец в мою таблицу Schedules как LastScheduledDateTime, а затем мой код выглядит так:

ScheduleNotices - это повторяющееся задание, которое будет запускаться ежедневно при 12.00 AM. Эта работа будет планировать другие задания, которые должны быть выполнены в этот день

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