Skip to content

Commit df47323

Browse files
Fixed continuously triggering tasks in timezones ahead of UTC. (#152)
1 parent 560aaba commit df47323

5 files changed

Lines changed: 88 additions & 8 deletions

File tree

MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/DailyTriggerTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,27 @@ public static IEnumerable<object[]> GetNextExecutionData()
154154
new DateTimeOffset(2024, 04, 17, 10, 00, 00, TimeSpan.Zero), // Wednesday @ 10am
155155
TimeZoneInfo.Utc,
156156
};
157+
158+
// Regression: Positive UTC offset (UTC+8) where trigger time just passed.
159+
// When the server is at UTC+8, 02:01 local = 18:01 UTC (previous day).
160+
// The next execution should be the NEXT day, not immediate re-trigger.
161+
var sgTimezone = TimeZoneInfo.FindSystemTimeZoneById("Singapore Standard Time");
162+
yield return new object[]
163+
{
164+
new []{ new TimeSpan(2, 0, 0) }, // Trigger at 02:00 local
165+
new DateTimeOffset(2024, 04, 17, 2, 01, 00, TimeSpan.FromHours(8)), // 02:01 SGT (= 16 Apr 18:01 UTC)
166+
new DateTimeOffset(2024, 04, 18, 2, 00, 00, TimeSpan.FromHours(8)), // Next day 02:00 SGT
167+
sgTimezone,
168+
};
169+
170+
// Regression: Positive UTC offset (UTC+8) where trigger time is still ahead today.
171+
yield return new object[]
172+
{
173+
new []{ new TimeSpan(10, 0, 0) }, // Trigger at 10:00 local
174+
new DateTimeOffset(2024, 04, 17, 2, 00, 00, TimeSpan.FromHours(8)), // 02:00 SGT (= 16 Apr 18:00 UTC)
175+
new DateTimeOffset(2024, 04, 17, 10, 00, 00, TimeSpan.FromHours(8)), // Same day 10:00 SGT
176+
sgTimezone,
177+
};
157178
}
158179
}
159180
}

MFiles.VAF.Extensions.Tests/Configuration/ScheduledExecution/ScheduleTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,69 @@ public static IEnumerable<object[]> GetNextExecutionData_AusEastern()
383383
};
384384
}
385385

386+
[TestMethod]
387+
[DynamicData(nameof(GetNextExecutionData_Singapore), DynamicDataSourceType.Method)]
388+
public void GetNextExecution_Singapore
389+
(
390+
IEnumerable<TriggerBase> triggers,
391+
DateTime? after,
392+
DateTime? expected
393+
)
394+
{
395+
var execution = new Schedule()
396+
{
397+
Enabled = true,
398+
Triggers = triggers
399+
.Select(t => new Trigger(t))
400+
.Where(t => t != null)
401+
.ToList(),
402+
TriggerTimeType = TriggerTimeType.Custom,
403+
TriggerTimeCustomTimeZone = "Singapore Standard Time"
404+
}.GetNextExecution(after);
405+
Assert.AreEqual(expected?.ToUniversalTime(), execution?.ToUniversalTime());
406+
}
407+
408+
/// <summary>
409+
/// Returns data for a high positive UTC offset (UTC+8) where the UTC day differs from local day.
410+
/// Regression test: ensures trigger time just passed does not cause immediate re-trigger.
411+
/// </summary>
412+
public static IEnumerable<object[]> GetNextExecutionData_Singapore()
413+
{
414+
// Trigger at 02:00 SGT, current time is 02:01 SGT (= 18:01 UTC previous day).
415+
// Next execution should be tomorrow at 02:00 SGT, NOT immediate.
416+
yield return new object[]
417+
{
418+
new TriggerBase[]
419+
{
420+
new DailyTrigger(){
421+
TriggerTimes = new List<TimeSpan>()
422+
{
423+
new TimeSpan(2, 0, 0)
424+
}.ToList()
425+
}
426+
},
427+
new DateTime(2024, 04, 16, 18, 01, 00, DateTimeKind.Utc), // 02:01 SGT on 17th = 18:01 UTC on 16th
428+
new DateTime(2024, 04, 17, 18, 00, 00, DateTimeKind.Utc), // 02:00 SGT on 18th = 18:00 UTC on 17th
429+
};
430+
431+
// Trigger at 10:00 SGT, current time is 02:00 SGT (= 18:00 UTC previous day).
432+
// Next execution should be today at 10:00 SGT.
433+
yield return new object[]
434+
{
435+
new TriggerBase[]
436+
{
437+
new DailyTrigger(){
438+
TriggerTimes = new List<TimeSpan>()
439+
{
440+
new TimeSpan(10, 0, 0)
441+
}.ToList()
442+
}
443+
},
444+
new DateTime(2024, 04, 16, 18, 00, 00, DateTimeKind.Utc), // 02:00 SGT on 17th = 18:00 UTC on 16th
445+
new DateTime(2024, 04, 17, 02, 00, 00, DateTimeKind.Utc), // 10:00 SGT on 17th = 02:00 UTC on 17th
446+
};
447+
}
448+
386449
/// <summary>
387450
/// Returns data for a high UTC offset where the trigger times are the next day in UTC.
388451
/// </summary>

MFiles.VAF.Extensions/Configuration/ScheduledExecution/DailyTrigger.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ public DailyTrigger()
5959
timeZoneInfo = timeZoneInfo ?? TimeZoneInfo.Local;
6060

6161
// When should we start looking?
62-
var before = (after ?? DateTime.UtcNow);
63-
after = before.ToUniversalTime();
62+
after = (after ?? DateTime.UtcNow).ToUniversalTime();
6463

6564
// Convert the time into the timezone we're after.
6665
after = TimeZoneInfo.ConvertTime(after.Value, timeZoneInfo);
@@ -74,12 +73,7 @@ public DailyTrigger()
7473
// What is the potential time that this will run?
7574
DateTimeOffset potential;
7675
{
77-
// If the timezone conversion changed the date then go back to the start of the date.
78-
var date = after.Value.Date;
79-
if (after.Value.Date != before.Date)
80-
date = new DateTime(before.Date.Ticks);
81-
82-
var dateTime = date.Add(t);
76+
var dateTime = after.Value.Date.Add(t);
8377
potential = new DateTimeOffset(dateTime, timeZoneInfo.GetUtcOffset(dateTime));
8478
}
8579

MFiles.VAF.Extensions/Configuration/ScheduledExecution/DayOfMonthTrigger.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public DayOfMonthTrigger()
8989
(
9090
d => GetNextDayOfMonth(after.Value, d, this.UnrepresentableDateHandling)
9191
)
92+
.Select(d => new DateTimeOffset(d.DateTime, timeZoneInfo.GetUtcOffset(d.DateTime)))
9293
.Select
9394
(
9495
d => new DailyTrigger() { Type = ScheduleTriggerType.Daily, TriggerTimes = this.TriggerTimes }

MFiles.VAF.Extensions/Configuration/ScheduledExecution/WeeklyTrigger.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public WeeklyTrigger()
6565
// Get the next execution time (this will not find run times today).
6666
var potentialMatches = this.TriggerDays
6767
.SelectMany(d => GetNextDayOfWeek(after.Value, d))
68+
.Select(d => new DateTimeOffset(d.DateTime, timeZoneInfo.GetUtcOffset(d.DateTime)))
6869
.Select
6970
(
7071
d => new DailyTrigger() { Type = ScheduleTriggerType.Daily, TriggerTimes = this.TriggerTimes }

0 commit comments

Comments
 (0)