fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178)
This commit is contained in:
parent
990e86b983
commit
f95b3a8b3b
11 changed files with 347 additions and 137 deletions
|
|
@ -41,8 +41,8 @@ public class EventSchedulerTests
|
||||||
public int CallCount { get; private set; }
|
public int CallCount { get; private set; }
|
||||||
public Action Callback { get; }
|
public Action Callback { get; }
|
||||||
|
|
||||||
public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null)
|
public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null)
|
||||||
: base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc)
|
: base(time, recurrence)
|
||||||
{
|
{
|
||||||
CallCount = 0;
|
CallCount = 0;
|
||||||
Callback = callback;
|
Callback = callback;
|
||||||
|
|
@ -105,25 +105,28 @@ public class EventSchedulerTests
|
||||||
var executionOrder = new List<int>();
|
var executionOrder = new List<int>();
|
||||||
|
|
||||||
// Create events with staggered occurrences
|
// Create events with staggered occurrences
|
||||||
var evt3 = new TestScheduledEvent(
|
var evt1Start = Core._now.AddSeconds(10);
|
||||||
Core._now.AddSeconds(30),
|
|
||||||
() => executionOrder.Add(3)
|
|
||||||
);
|
|
||||||
|
|
||||||
var evt1 = new TestScheduledEvent(
|
var evt1 = new TestScheduledEvent(
|
||||||
Core._now.AddSeconds(10),
|
TimeOnly.FromDateTime(Core._now.AddSeconds(10)),
|
||||||
() => executionOrder.Add(1)
|
() => executionOrder.Add(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var evt2Start = Core._now.AddSeconds(20);
|
||||||
var evt2 = new TestScheduledEvent(
|
var evt2 = new TestScheduledEvent(
|
||||||
Core._now.AddSeconds(20),
|
TimeOnly.FromDateTime(evt2Start),
|
||||||
() => executionOrder.Add(2)
|
() => executionOrder.Add(2)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var evt3Start = Core._now.AddSeconds(30);
|
||||||
|
var evt3 = new TestScheduledEvent(
|
||||||
|
TimeOnly.FromDateTime(evt3Start),
|
||||||
|
() => executionOrder.Add(3)
|
||||||
|
);
|
||||||
|
|
||||||
// Add out of order
|
// Add out of order
|
||||||
EventScheduler.Shared.ScheduleEvent(evt3);
|
evt3.Schedule(evt3Start);
|
||||||
EventScheduler.Shared.ScheduleEvent(evt1);
|
evt1.Schedule(evt1Start);
|
||||||
EventScheduler.Shared.ScheduleEvent(evt2);
|
evt2.Schedule(evt2Start);
|
||||||
|
|
||||||
// Advance time to after all events
|
// Advance time to after all events
|
||||||
Core._now = Core._now.AddSeconds(40);
|
Core._now = Core._now.AddSeconds(40);
|
||||||
|
|
@ -157,12 +160,12 @@ public class EventSchedulerTests
|
||||||
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
|
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
|
||||||
|
|
||||||
var evt = new TestScheduledEvent(
|
var evt = new TestScheduledEvent(
|
||||||
Core._now,
|
TimeOnly.FromDateTime(Core._now),
|
||||||
() => callCount++,
|
() => callCount++,
|
||||||
recurrence
|
recurrence
|
||||||
);
|
);
|
||||||
|
|
||||||
EventScheduler.Shared.ScheduleEvent(evt);
|
evt.Schedule(Core._now);
|
||||||
|
|
||||||
// Advance time to after all occurrences should have happened
|
// Advance time to after all occurrences should have happened
|
||||||
Core._now = Core._now.AddSeconds(50);
|
Core._now = Core._now.AddSeconds(50);
|
||||||
|
|
@ -271,14 +274,13 @@ public class EventSchedulerTests
|
||||||
var endTime = Core._now.AddSeconds(36);
|
var endTime = Core._now.AddSeconds(36);
|
||||||
|
|
||||||
var customEvent = new CustomEvent(
|
var customEvent = new CustomEvent(
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
TimeOnly.FromDateTime(startTime),
|
TimeOnly.FromDateTime(startTime),
|
||||||
|
endTime,
|
||||||
recurrence,
|
recurrence,
|
||||||
() => callCount++
|
() => callCount++
|
||||||
);
|
);
|
||||||
|
|
||||||
EventScheduler.Shared.ScheduleEvent(customEvent);
|
customEvent.Schedule(startTime);
|
||||||
|
|
||||||
Core._now = Core._now.AddSeconds(50);
|
Core._now = Core._now.AddSeconds(50);
|
||||||
Timer.Slice(8);
|
Timer.Slice(8);
|
||||||
|
|
@ -297,12 +299,11 @@ public class EventSchedulerTests
|
||||||
private readonly Action _callback;
|
private readonly Action _callback;
|
||||||
|
|
||||||
public CustomEvent(
|
public CustomEvent(
|
||||||
DateTime startAfter,
|
|
||||||
DateTime endOn,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
|
DateTime endOn,
|
||||||
IRecurrencePattern recurrence,
|
IRecurrencePattern recurrence,
|
||||||
Action callback
|
Action callback
|
||||||
) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback;
|
) : base(time, endOn, recurrence) => _callback = callback;
|
||||||
|
|
||||||
public override void OnEvent()
|
public override void OnEvent()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,152 @@ public class RecurrencePatternTests
|
||||||
Assert.Equal(expected, result);
|
Assert.Equal(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.All, 2024, 5, 16, 12, 30)] // Next day (Thursday)
|
||||||
|
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.Monday, 2024, 5, 20, 12, 30)] // Next Monday
|
||||||
|
[InlineData(2024, 5, 15, 12, 30, 2, AllowedDays.Wednesday, 2024, 5, 29, 12, 30)] // Biweekly Wednesday
|
||||||
|
public void WeeklyRecurrencePattern_BasicPatterns_ReturnsCorrectDay(
|
||||||
|
int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
int intervalWeeks, AllowedDays allowedDays,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.Utc;
|
||||||
|
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
|
||||||
|
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2024, 12, 30, 12, 30, 1, AllowedDays.Monday, 2025, 1, 6, 12, 30)] // Across year boundary
|
||||||
|
[InlineData(2024, 5, 31, 12, 30, 1, AllowedDays.Saturday, 2024, 6, 1, 12, 30)] // Across month boundary
|
||||||
|
public void WeeklyRecurrencePattern_CrossBoundaries_ReturnsCorrectDay(
|
||||||
|
int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
int intervalWeeks, AllowedDays allowedDays,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.Utc;
|
||||||
|
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
|
||||||
|
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2024, 5, 15, 12, 30, AllowedMonths.January | AllowedMonths.February, AllowedDays.Wednesday, 2025, 1, 1, 12, 30)] // Skip to January
|
||||||
|
[InlineData(2024, 12, 15, 12, 30, AllowedMonths.January | AllowedMonths.December, AllowedDays.Wednesday, 2024, 12, 18, 12, 30)] // Same month
|
||||||
|
[InlineData(2024, 1, 31, 12, 30, AllowedMonths.February | AllowedMonths.April, AllowedDays.Saturday, 2024, 2, 3, 12, 30)] // Next month allowed
|
||||||
|
public void WeeklyRecurrencePattern_MonthFiltering_ReturnsCorrectDay(
|
||||||
|
int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
AllowedMonths allowedMonths, AllowedDays allowedDays,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.Utc;
|
||||||
|
var pattern = new WeeklyRecurrencePattern(1, allowedMonths, allowedDays);
|
||||||
|
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("America/New_York", 2024, 3, 9, 12, 0, 1, AllowedDays.Sunday, 2024, 3, 10, 12, 0)] // Before spring forward
|
||||||
|
[InlineData("America/New_York", 2024, 3, 10, 2, 30, 1, AllowedDays.Sunday, 2024, 3, 17, 2, 30)] // During spring forward (invalid time)
|
||||||
|
[InlineData("America/New_York", 2024, 11, 2, 12, 0, 1, AllowedDays.Sunday, 2024, 11, 3, 12, 0)] // Before fall back
|
||||||
|
[InlineData("America/New_York", 2024, 11, 3, 1, 30, 1, AllowedDays.Sunday, 2024, 11, 10, 1, 30)] // During fall back (ambiguous time)
|
||||||
|
public void WeeklyRecurrencePattern_DSTTransitions_HandlesCorrectly(
|
||||||
|
string tzId, int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
int intervalWeeks, AllowedDays allowedDays,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
|
||||||
|
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
|
||||||
|
|
||||||
|
var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0);
|
||||||
|
var afterUtc = startLocal.LocalToUtc(tz);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
|
||||||
|
Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0), resultLocal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2024, 5, 29, 12, 0, AllowedDays.None, 2024, 5, 29, 12, 30)] // Default to current day
|
||||||
|
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Wednesday | AllowedDays.Friday, 2024, 5, 29, 12, 30)] // Current day is Wednesday
|
||||||
|
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Monday | AllowedDays.Friday, 2024, 5, 31, 12, 30)] // Next allowed day (Friday)
|
||||||
|
public void WeeklyRecurrencePattern_DaysOfWeekHandling_ReturnsCorrectDay(
|
||||||
|
int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
AllowedDays allowedDays,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.Utc;
|
||||||
|
var pattern = new WeeklyRecurrencePattern(1, AllowedMonths.All, allowedDays);
|
||||||
|
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(2024, 1, 29, 12, 0, 1, 2024, 1, 29, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 1, 30, 12, 30)] // Current month allowed
|
||||||
|
[InlineData(2024, 1, 31, 12, 0, 1, 2024, 1, 31, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 3, 1, 12, 30)] // Skip February
|
||||||
|
public void WeeklyRecurrencePattern_WeekSpanningMonths_ReturnsCorrectDay(
|
||||||
|
int startYear, int startMonth, int startDay, int startHour, int startMinute,
|
||||||
|
int intervalWeeks,
|
||||||
|
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute,
|
||||||
|
AllowedMonths allowedMonths,
|
||||||
|
int nextExpectedYear, int nextExpectedMonth, int nextExpectedDay, int nextExpectedHour, int nextExpectedMinute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tz = TimeZoneInfo.Utc;
|
||||||
|
var pattern = new WeeklyRecurrencePattern(intervalWeeks, allowedMonths);
|
||||||
|
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
|
||||||
|
var time = new TimeOnly(expectedHour, expectedMinute);
|
||||||
|
|
||||||
|
// Act - first call should hit expectedDate
|
||||||
|
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
|
||||||
|
// Call again - should move to next available day in allowed months
|
||||||
|
var nextResult = pattern.GetNextOccurrence(result, time, tz);
|
||||||
|
var nextExpected = new DateTime(nextExpectedYear, nextExpectedMonth, nextExpectedDay, nextExpectedHour, nextExpectedMinute, 0, DateTimeKind.Utc);
|
||||||
|
Assert.Equal(nextExpected, nextResult);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)]
|
[InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)]
|
||||||
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)]
|
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)]
|
||||||
|
|
@ -145,7 +291,6 @@ public class RecurrencePatternTests
|
||||||
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
|
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional utility method tests
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
|
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
|
||||||
public void LocalToUtc_AmbiguousTime_HandlesConsistently(
|
public void LocalToUtc_AmbiguousTime_HandlesConsistently(
|
||||||
|
|
|
||||||
22
Projects/UOContent/Engines/Events/AllowedDays.cs
Normal file
22
Projects/UOContent/Engines/Events/AllowedDays.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Server.Engines.Events;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum AllowedDays : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Sunday = 1 << 0,
|
||||||
|
Monday = 1 << 1,
|
||||||
|
Tuesday = 1 << 2,
|
||||||
|
Wednesday = 1 << 3,
|
||||||
|
Thursday = 1 << 4,
|
||||||
|
Friday = 1 << 5,
|
||||||
|
Saturday = 1 << 6,
|
||||||
|
All = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DaysOfWeekExtension
|
||||||
|
{
|
||||||
|
public static AllowedDays ToDaysOfWeek(this DayOfWeek dayOfWeek) => (AllowedDays)(1 << (int)dayOfWeek);
|
||||||
|
}
|
||||||
23
Projects/UOContent/Engines/Events/AllowedMonths.cs
Normal file
23
Projects/UOContent/Engines/Events/AllowedMonths.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Server.Engines.Events;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum AllowedMonths
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
January = 1 << 0,
|
||||||
|
February = 1 << 1,
|
||||||
|
March = 1 << 2,
|
||||||
|
April = 1 << 3,
|
||||||
|
May = 1 << 4,
|
||||||
|
June = 1 << 5,
|
||||||
|
July = 1 << 6,
|
||||||
|
August = 1 << 7,
|
||||||
|
September = 1 << 8,
|
||||||
|
October = 1 << 9,
|
||||||
|
November = 1 << 10,
|
||||||
|
December = 1 << 11,
|
||||||
|
|
||||||
|
All = January | February | March | April | May | June | July | August | September | October | November | December
|
||||||
|
}
|
||||||
67
Projects/UOContent/Engines/Events/BaseScheduledEvent.cs
Normal file
67
Projects/UOContent/Engines/Events/BaseScheduledEvent.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Server.Logging;
|
||||||
|
|
||||||
|
namespace Server.Engines.Events;
|
||||||
|
|
||||||
|
public abstract class BaseScheduledEvent
|
||||||
|
{
|
||||||
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseScheduledEvent));
|
||||||
|
|
||||||
|
public TimeZoneInfo TimeZone { get; private set; } = TimeZoneInfo.Utc;
|
||||||
|
public DateTime NextOccurrence { get; private set; } = DateTime.MaxValue;
|
||||||
|
public bool Cancelled { get; private set; }
|
||||||
|
public EventScheduler Scheduler { get; private set; }
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Schedule(DateTime startAfter, TimeZoneInfo timeZone = null) =>
|
||||||
|
Schedule(EventScheduler.Shared, startAfter, timeZone);
|
||||||
|
|
||||||
|
public void Schedule(EventScheduler scheduler, DateTime startAfter, TimeZoneInfo timeZone = null)
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
Scheduler = scheduler;
|
||||||
|
TimeZone = timeZone ?? TimeZoneInfo.Utc;
|
||||||
|
Schedule(startAfter, timeZone, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Schedule(DateTime startAfter, TimeZoneInfo timeZone, bool isFirst)
|
||||||
|
{
|
||||||
|
Cancelled = false;
|
||||||
|
var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(timeZone);
|
||||||
|
|
||||||
|
var next = GetNextOccurrence(afterUtc);
|
||||||
|
|
||||||
|
// For the first occurrence, we should set it to the startAfter date if we have no recurrence.
|
||||||
|
NextOccurrence = next == DateTime.MaxValue && isFirst ? afterUtc : next;
|
||||||
|
|
||||||
|
if (NextOccurrence != DateTime.MaxValue)
|
||||||
|
{
|
||||||
|
Scheduler.ScheduleEvent(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract DateTime GetNextOccurrence(DateTime after);
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
Cancelled = true;
|
||||||
|
Scheduler?.UnscheduleEvent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Advance()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OnEvent();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "OnEvent failed to execute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Schedule(NextOccurrence, TimeZone, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void OnEvent();
|
||||||
|
}
|
||||||
|
|
@ -7,23 +7,17 @@ public sealed class CallbackScheduledEvent : ScheduledEvent
|
||||||
private readonly Action _callback;
|
private readonly Action _callback;
|
||||||
|
|
||||||
public CallbackScheduledEvent(
|
public CallbackScheduledEvent(
|
||||||
DateTime after,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
Action callback,
|
Action callback,
|
||||||
IRecurrencePattern recurrencePattern = null,
|
IRecurrencePattern recurrencePattern = null
|
||||||
TimeZoneInfo timeZone = null
|
) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||||
) : base(after, time, recurrencePattern, timeZone) =>
|
|
||||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
|
||||||
|
|
||||||
public CallbackScheduledEvent(
|
public CallbackScheduledEvent(
|
||||||
DateTime after,
|
|
||||||
DateTime endOn,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
|
DateTime endOn,
|
||||||
Action callback,
|
Action callback,
|
||||||
IRecurrencePattern recurrencePattern = null,
|
IRecurrencePattern recurrencePattern = null
|
||||||
TimeZoneInfo timeZone = null
|
) : base(time, endOn, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||||
) : base(after, endOn, time, recurrencePattern, timeZone) =>
|
|
||||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
|
||||||
|
|
||||||
public override void OnEvent() => _callback();
|
public override void OnEvent() => _callback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,32 +35,49 @@ public class DailyRecurrencePattern : IRecurrencePattern
|
||||||
public class WeeklyRecurrencePattern : IRecurrencePattern
|
public class WeeklyRecurrencePattern : IRecurrencePattern
|
||||||
{
|
{
|
||||||
public int IntervalWeeks { get; }
|
public int IntervalWeeks { get; }
|
||||||
public DaysOfWeek DaysOfWeek { get; }
|
public AllowedDays AllowedDays { get; }
|
||||||
|
public AllowedMonths AllowedMonths { get; }
|
||||||
|
|
||||||
public WeeklyRecurrencePattern(int intervalWeeks = 1, DaysOfWeek daysOfWeek = DaysOfWeek.None)
|
public WeeklyRecurrencePattern(int intervalWeeks = 1, AllowedMonths allowedMonths = AllowedMonths.All, AllowedDays allowedDays = AllowedDays.None)
|
||||||
{
|
{
|
||||||
IntervalWeeks = Math.Max(1, intervalWeeks);
|
IntervalWeeks = Math.Max(1, intervalWeeks);
|
||||||
DaysOfWeek = daysOfWeek;
|
AllowedDays = allowedDays == AllowedDays.None ? AllowedDays.All : allowedDays;
|
||||||
|
AllowedMonths = allowedMonths == AllowedMonths.None ? AllowedMonths.All : allowedMonths;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone)
|
public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone)
|
||||||
{
|
{
|
||||||
var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone);
|
var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone);
|
||||||
var daysOfWeek = DaysOfWeek == DaysOfWeek.None
|
var daysOfWeek = AllowedDays == AllowedDays.None ? local.DayOfWeek.ToDaysOfWeek() : AllowedDays;
|
||||||
? (DaysOfWeek)(1 << (int)local.DayOfWeek)
|
|
||||||
: DaysOfWeek;
|
|
||||||
|
|
||||||
var weekStart = local.Date.AddDays(-(int)local.DayOfWeek);
|
var weekStart = local.Date.AddDays(-(int)local.DayOfWeek);
|
||||||
|
|
||||||
// No more days in this week, jump IntervalWeeks ahead
|
// No more days in this week, jump IntervalWeeks ahead
|
||||||
for (int week = 0; week <= 100; week++)
|
for (var week = 0; week <= 52; week++)
|
||||||
{
|
{
|
||||||
DateTime nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week);
|
var nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week);
|
||||||
|
var weekEnd = nextWeekStart.AddDays(6);
|
||||||
|
|
||||||
for (int i = 0; i < 7; i++)
|
var startMonth = (AllowedMonths)(1 << (nextWeekStart.Month - 1));
|
||||||
|
var endMonth = (AllowedMonths)(1 << (weekEnd.Month - 1));
|
||||||
|
|
||||||
|
// Skip the entire week if the start and end months are not in the allowed months
|
||||||
|
if ((AllowedMonths & (startMonth | endMonth)) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
{
|
{
|
||||||
var day = nextWeekStart.AddDays(i);
|
var day = nextWeekStart.AddDays(i);
|
||||||
var dayOfWeekFlag = (DaysOfWeek)(1 << (int)day.DayOfWeek);
|
|
||||||
|
var currentMonth = (AllowedMonths)(1 << (day.Month - 1));
|
||||||
|
if ((AllowedMonths & currentMonth) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dayOfWeekFlag = (AllowedDays)(1 << (int)day.DayOfWeek);
|
||||||
if ((daysOfWeek & dayOfWeekFlag) != 0)
|
if ((daysOfWeek & dayOfWeekFlag) != 0)
|
||||||
{
|
{
|
||||||
var candidate = new DateTime(day.Year, day.Month, day.Day, time.Hour, time.Minute, 0);
|
var candidate = new DateTime(day.Year, day.Month, day.Day, time.Hour, time.Minute, 0);
|
||||||
|
|
@ -94,7 +111,7 @@ public class MonthlyRecurrencePattern : IRecurrencePattern
|
||||||
var month = local.Month;
|
var month = local.Month;
|
||||||
var day = DayOfMonth == -1 ? local.Day : DayOfMonth;
|
var day = DayOfMonth == -1 ? local.Day : DayOfMonth;
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
var nextMonth = month + IntervalMonths * i;
|
var nextMonth = month + IntervalMonths * i;
|
||||||
var candidate = new DateTime(year, 1, 1)
|
var candidate = new DateTime(year, 1, 1)
|
||||||
|
|
@ -132,7 +149,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
||||||
var year = local.Year;
|
var year = local.Year;
|
||||||
var month = local.Month;
|
var month = local.Month;
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++)
|
for (var i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
var nextMonth = month + IntervalMonths * i;
|
var nextMonth = month + IntervalMonths * i;
|
||||||
var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset);
|
var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset);
|
||||||
|
|
@ -147,7 +164,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
||||||
.Add(time.ToTimeSpan());
|
.Add(time.ToTimeSpan());
|
||||||
|
|
||||||
// Find the first occurrence of the desired day
|
// Find the first occurrence of the desired day
|
||||||
int daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7;
|
var daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7;
|
||||||
if (daysOffset > 7)
|
if (daysOffset > 7)
|
||||||
{
|
{
|
||||||
daysOffset -= 7;
|
daysOffset -= 7;
|
||||||
|
|
@ -168,7 +185,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
||||||
.Add(time.ToTimeSpan());
|
.Add(time.ToTimeSpan());
|
||||||
|
|
||||||
// Find the last occurrence of the desired day
|
// Find the last occurrence of the desired day
|
||||||
int daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7;
|
var daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7;
|
||||||
if (daysOffset >= 7)
|
if (daysOffset >= 7)
|
||||||
{
|
{
|
||||||
daysOffset -= 7;
|
daysOffset -= 7;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Server.Logging;
|
|
||||||
|
|
||||||
namespace Server.Engines.Events;
|
namespace Server.Engines.Events;
|
||||||
|
|
||||||
|
|
@ -17,25 +16,9 @@ public interface IRecurrencePattern
|
||||||
|
|
||||||
public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth }
|
public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth }
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum DaysOfWeek : byte
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Sunday = 1,
|
|
||||||
Monday = 2,
|
|
||||||
Tuesday = 4,
|
|
||||||
Wednesday = 8,
|
|
||||||
Thursday = 16,
|
|
||||||
Friday = 32,
|
|
||||||
Saturday = 64,
|
|
||||||
EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EventScheduler : Timer
|
public class EventScheduler : Timer
|
||||||
{
|
{
|
||||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler));
|
private readonly PriorityQueue<BaseScheduledEvent, DateTime> _schedule = new();
|
||||||
|
|
||||||
private readonly PriorityQueue<ScheduledEvent, DateTime> _schedule = new();
|
|
||||||
|
|
||||||
public static EventScheduler Shared { get; private set; }
|
public static EventScheduler Shared { get; private set; }
|
||||||
|
|
||||||
|
|
@ -116,16 +99,24 @@ public class EventScheduler : Timer
|
||||||
TimeZoneInfo timeZone = null
|
TimeZoneInfo timeZone = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone);
|
var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern);
|
||||||
ScheduleEvent(scheduledEvent);
|
scheduledEvent.Schedule(this, after, timeZone);
|
||||||
return scheduledEvent;
|
return scheduledEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleEvent(ScheduledEvent entry)
|
public void ScheduleEvent(BaseScheduledEvent entry)
|
||||||
|
{
|
||||||
|
if (entry != null && entry.NextOccurrence < DateTime.MaxValue)
|
||||||
|
{
|
||||||
|
_schedule.Enqueue(entry, entry.NextOccurrence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnscheduleEvent(BaseScheduledEvent entry)
|
||||||
{
|
{
|
||||||
if (entry != null)
|
if (entry != null)
|
||||||
{
|
{
|
||||||
_schedule.Enqueue(entry, entry.NextOccurrence);
|
_schedule.Remove(entry, out _, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,21 +140,7 @@ public class EventScheduler : Timer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime nextOccurrence;
|
entry.Advance(); // Advances the event and self queues if necessary
|
||||||
try
|
|
||||||
{
|
|
||||||
nextOccurrence = entry.Advance();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.Error(e, "Error while executing scheduled event.");
|
|
||||||
nextOccurrence = DateTime.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextOccurrence < DateTime.MaxValue)
|
|
||||||
{
|
|
||||||
_schedule.Enqueue(entry, nextOccurrence);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,59 +2,31 @@ using System;
|
||||||
|
|
||||||
namespace Server.Engines.Events;
|
namespace Server.Engines.Events;
|
||||||
|
|
||||||
public abstract class ScheduledEvent
|
public abstract class ScheduledEvent : BaseScheduledEvent
|
||||||
{
|
{
|
||||||
public IRecurrencePattern Recurrence { get; }
|
public IRecurrencePattern Recurrence { get; }
|
||||||
public TimeZoneInfo TimeZone { get; }
|
|
||||||
public TimeOnly Time { get; }
|
public TimeOnly Time { get; }
|
||||||
public DateTime EndDate { get; }
|
public DateTime EndDate { get; }
|
||||||
public DateTime NextOccurrence { get; protected set; }
|
|
||||||
public bool Cancelled { get; private set; }
|
|
||||||
|
|
||||||
public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null)
|
public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null)
|
||||||
: this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone)
|
: this(time, DateTime.MaxValue, recurrence)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null)
|
|
||||||
: this(startAfter, DateTime.MaxValue, time, recurrence, timeZone)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScheduledEvent(
|
public ScheduledEvent(
|
||||||
DateTime startAfter,
|
|
||||||
DateTime endOn,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
IRecurrencePattern recurrence,
|
DateTime endOn,
|
||||||
TimeZoneInfo timeZone = null
|
IRecurrencePattern recurrence = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Time = time;
|
Time = time;
|
||||||
Recurrence = recurrence;
|
Recurrence = recurrence;
|
||||||
TimeZone = timeZone ?? TimeZoneInfo.Utc;
|
|
||||||
EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone);
|
EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone);
|
||||||
|
|
||||||
var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone);
|
|
||||||
|
|
||||||
var next = GetOccurrence(afterUtc);
|
|
||||||
// For the first occurrence, we should set it to the startAfter date if we have no recurrence.
|
|
||||||
NextOccurrence = next == DateTime.MaxValue ? afterUtc : next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual DateTime GetOccurrence(DateTime after)
|
protected override DateTime GetNextOccurrence(DateTime after)
|
||||||
{
|
{
|
||||||
var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue;
|
var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue;
|
||||||
return next >= EndDate ? DateTime.MaxValue : next;
|
return next >= EndDate ? DateTime.MaxValue : next;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cancel() => Cancelled = true;
|
|
||||||
|
|
||||||
public virtual DateTime Advance()
|
|
||||||
{
|
|
||||||
OnEvent();
|
|
||||||
|
|
||||||
return NextOccurrence = GetOccurrence(NextOccurrence);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void OnEvent();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,23 @@ public class YearlyCallbackScheduledEvent : YearlyScheduledEvent
|
||||||
private readonly Action _callback;
|
private readonly Action _callback;
|
||||||
|
|
||||||
protected YearlyCallbackScheduledEvent(
|
protected YearlyCallbackScheduledEvent(
|
||||||
DateTime startAfter,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
MonthDay yearlyStart,
|
MonthDay yearlyStart,
|
||||||
MonthDay yearlyEnd,
|
MonthDay yearlyEnd,
|
||||||
Action callback,
|
Action callback,
|
||||||
IRecurrencePattern recurrence,
|
IRecurrencePattern recurrence
|
||||||
TimeZoneInfo timeZone = null
|
) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence)
|
||||||
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected YearlyCallbackScheduledEvent(
|
protected YearlyCallbackScheduledEvent(
|
||||||
DateTime startAfter,
|
|
||||||
DateTime endOn,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
MonthDay yearlyStart,
|
MonthDay yearlyStart,
|
||||||
MonthDay yearlyEnd,
|
MonthDay yearlyEnd,
|
||||||
|
DateTime endOn,
|
||||||
Action callback,
|
Action callback,
|
||||||
IRecurrencePattern recurrence,
|
IRecurrencePattern recurrence
|
||||||
TimeZoneInfo timeZone = null
|
) : base(time, yearlyStart, yearlyEnd, endOn, recurrence) => _callback = callback;
|
||||||
) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _callback = callback;
|
|
||||||
|
|
||||||
public override void OnEvent() => _callback();
|
public override void OnEvent() => _callback();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,29 @@ public abstract class YearlyScheduledEvent : ScheduledEvent
|
||||||
public MonthDay YearlyEnd { get; }
|
public MonthDay YearlyEnd { get; }
|
||||||
|
|
||||||
protected YearlyScheduledEvent(
|
protected YearlyScheduledEvent(
|
||||||
DateTime startAfter,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
MonthDay yearlyStart,
|
MonthDay yearlyStart,
|
||||||
MonthDay yearlyEnd,
|
MonthDay yearlyEnd,
|
||||||
IRecurrencePattern recurrence,
|
IRecurrencePattern recurrence
|
||||||
TimeZoneInfo timeZone = null
|
) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence)
|
||||||
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected YearlyScheduledEvent(
|
protected YearlyScheduledEvent(
|
||||||
DateTime startAfter,
|
|
||||||
DateTime endOn,
|
|
||||||
TimeOnly time,
|
TimeOnly time,
|
||||||
MonthDay yearlyStart,
|
MonthDay yearlyStart,
|
||||||
MonthDay yearlyEnd,
|
MonthDay yearlyEnd,
|
||||||
IRecurrencePattern recurrence,
|
DateTime endOn,
|
||||||
TimeZoneInfo timeZone = null
|
IRecurrencePattern recurrence
|
||||||
) : base(startAfter, endOn, time, recurrence, timeZone)
|
) : base(time, endOn, recurrence)
|
||||||
{
|
{
|
||||||
YearlyStart = yearlyStart;
|
YearlyStart = yearlyStart;
|
||||||
YearlyEnd = yearlyEnd;
|
YearlyEnd = yearlyEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DateTime GetOccurrence(DateTime after)
|
protected override DateTime GetNextOccurrence(DateTime after)
|
||||||
{
|
{
|
||||||
var next = base.GetOccurrence(after);
|
var next = base.GetNextOccurrence(after);
|
||||||
|
|
||||||
if (next == DateTime.MaxValue)
|
if (next == DateTime.MaxValue)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue