in src/WebJobs.Extensions/Extensions/Timers/Scheduling/CronSchedule.cs [125:206]
internal bool TryAdjustAmbiguousTime(DateTimeOffset now, DateTimeOffset next, out DateTimeOffset? adjusted)
{
adjusted = null;
TimeZoneInfo timeZone = TimeZoneInfo.Local;
// Begin evaluating scenarios when Daylight Saving ends and time falls back. This leads to "ambiguous" times
// as the clock repeats an hour. For example, times from 1:00 - 1:59 AM will occur twice in Pacific Standard Time
// and are therefore considered "ambiguous" for this time zone.
bool isNowAmbiguous = timeZone.IsAmbiguousTime(now);
bool isNextAmbiguous = timeZone.IsAmbiguousTime(next);
if (!isNowAmbiguous && !isNextAmbiguous)
{
// There are no ambiguous times to adjust.
return false;
}
// We also need to differentiate between "interval" and "point-in-time" schedules when exiting Daylight Saving Time.
//
// For "interval" schedules, we want to continue running through this ambiguous hour as usual. Using an "every 30 minute" schedule
// and Pacific time offset an example, we'd want to:
// - Run at 01:00-7, 01:30-7, 01:00-8, 01:30-8, 02:00-8, 02:30-8, etc.
//
// For "point-in-time" schedules, we only want to run them once (i.e a 1:30 trigger only runs once on this day). Using an
// "every day at 01:30" schedule and Pacific time offset as an example in 2018, we'd want to:
// - Run on November 3rd at 01:30-7
// - Run on November 4th at 01:30-7 (and not run at 1:30-8)
// - Run on November 5th at 01:30-8
if (!IsInterval && isNextAmbiguous && next.Offset != now.Offset)
{
// "Fall Back" Scenario 1 -- Point-in-time schedule where the next time is ambiguous and the offsets have changed.
// This means that "now" is in DST and "next" is in Standard Time. Since we never want to run an ambiguous
// point-in-time schedule on Standard time, move the offset back to the Daylight offset.
// Example, all on the "fall back" day:
// - schedule: every day at 01:30
// - now: 00:59-7
// - next: 01:30-8 (because all ambiguous "next" use Standard time when using NCronTab)
// - offsetDiff: (-8)-(-7) = -1
// - adjusted: 01:30-7
// In this scenario, we see that we're going from DST to Standard, but the Standard time is ambiguous. In this case,
// we always want to bring it back to the DST offset in the current local time zone.
var offsetDiff = next.Offset - now.Offset;
adjusted = TimeZoneInfo.ConvertTime(next.Add(offsetDiff), timeZone);
Logger.DstAmbiguousTime(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
}
else if (!IsInterval && isNextAmbiguous && next.Offset == now.Offset)
{
// "Fall Back" Scenario 2 -- Point-in-time where "next" is ambiguous and "now" and "next" are in Standard time. This can happen
// when the timer starts and we are already past the "fall back" point. For example, if the trigger starts up at
// 01:29-8 and we have a "every day at 01:30" schedule, we have to assume that we've already run it at 01:30-7 and
// therefore calculate the time after this.
// Example:
// - schedule: every day at 01:30
// - now: 11/4/2018 01:29-8
// - next: 11/4/2018 01:30-8 (because all ambiguous "next" use Standard time when using NCronTab)
// - adjusted: 11/5/2018 01:30-8
// In this scenario, we see that we're already in Standard time, but the Standard time is ambiguous. In this case,
// we always assume that we've already run during DST and we don't want to run twice in one day.
var nextDateTime = next.LocalDateTime;
while (timeZone.IsAmbiguousTime(nextDateTime))
{
nextDateTime = _cronSchedule.GetNextOccurrence(nextDateTime);
}
adjusted = new DateTimeOffset(nextDateTime);
Logger.DstAmbiguousTimeSecondOccurrence(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
}
else if (IsInterval && (isNowAmbiguous || isNextAmbiguous) && next.Offset != now.Offset)
{
// "Fall Back" Scenario 3 -- Interval schedule where either point is ambiguous and we're crossing the offset boundary. For example,
// an "every 30 minute" interval in Pacific time:
// - 00:30-7 -> 01:00-8 (because all ambiguous "next" use Standard time) -> adjust back 01:00-7
// - 01:30-7 -> 02:00-8 -> adjust back to 01:00-8
var offsetDiff = next.Offset - now.Offset;
adjusted = TimeZoneInfo.ConvertTime(next.Add(offsetDiff), timeZone);
Logger.DstAmbiguousTimeInterval(_logger, now, _cronSchedule.ToString(), next, TimeZoneInfo.Local.DisplayName, adjusted.Value);
}
return adjusted is not null;
}