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 Action Callback { get; }
|
||||
|
||||
public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null)
|
||||
: base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc)
|
||||
public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null)
|
||||
: base(time, recurrence)
|
||||
{
|
||||
CallCount = 0;
|
||||
Callback = callback;
|
||||
|
|
@ -105,25 +105,28 @@ public class EventSchedulerTests
|
|||
var executionOrder = new List<int>();
|
||||
|
||||
// Create events with staggered occurrences
|
||||
var evt3 = new TestScheduledEvent(
|
||||
Core._now.AddSeconds(30),
|
||||
() => executionOrder.Add(3)
|
||||
);
|
||||
|
||||
var evt1Start = Core._now.AddSeconds(10);
|
||||
var evt1 = new TestScheduledEvent(
|
||||
Core._now.AddSeconds(10),
|
||||
TimeOnly.FromDateTime(Core._now.AddSeconds(10)),
|
||||
() => executionOrder.Add(1)
|
||||
);
|
||||
|
||||
var evt2Start = Core._now.AddSeconds(20);
|
||||
var evt2 = new TestScheduledEvent(
|
||||
Core._now.AddSeconds(20),
|
||||
TimeOnly.FromDateTime(evt2Start),
|
||||
() => executionOrder.Add(2)
|
||||
);
|
||||
|
||||
var evt3Start = Core._now.AddSeconds(30);
|
||||
var evt3 = new TestScheduledEvent(
|
||||
TimeOnly.FromDateTime(evt3Start),
|
||||
() => executionOrder.Add(3)
|
||||
);
|
||||
|
||||
// Add out of order
|
||||
EventScheduler.Shared.ScheduleEvent(evt3);
|
||||
EventScheduler.Shared.ScheduleEvent(evt1);
|
||||
EventScheduler.Shared.ScheduleEvent(evt2);
|
||||
evt3.Schedule(evt3Start);
|
||||
evt1.Schedule(evt1Start);
|
||||
evt2.Schedule(evt2Start);
|
||||
|
||||
// Advance time to after all events
|
||||
Core._now = Core._now.AddSeconds(40);
|
||||
|
|
@ -157,12 +160,12 @@ public class EventSchedulerTests
|
|||
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
|
||||
|
||||
var evt = new TestScheduledEvent(
|
||||
Core._now,
|
||||
TimeOnly.FromDateTime(Core._now),
|
||||
() => callCount++,
|
||||
recurrence
|
||||
);
|
||||
|
||||
EventScheduler.Shared.ScheduleEvent(evt);
|
||||
evt.Schedule(Core._now);
|
||||
|
||||
// Advance time to after all occurrences should have happened
|
||||
Core._now = Core._now.AddSeconds(50);
|
||||
|
|
@ -271,14 +274,13 @@ public class EventSchedulerTests
|
|||
var endTime = Core._now.AddSeconds(36);
|
||||
|
||||
var customEvent = new CustomEvent(
|
||||
startTime,
|
||||
endTime,
|
||||
TimeOnly.FromDateTime(startTime),
|
||||
endTime,
|
||||
recurrence,
|
||||
() => callCount++
|
||||
);
|
||||
|
||||
EventScheduler.Shared.ScheduleEvent(customEvent);
|
||||
customEvent.Schedule(startTime);
|
||||
|
||||
Core._now = Core._now.AddSeconds(50);
|
||||
Timer.Slice(8);
|
||||
|
|
@ -297,12 +299,11 @@ public class EventSchedulerTests
|
|||
private readonly Action _callback;
|
||||
|
||||
public CustomEvent(
|
||||
DateTime startAfter,
|
||||
DateTime endOn,
|
||||
TimeOnly time,
|
||||
DateTime endOn,
|
||||
IRecurrencePattern recurrence,
|
||||
Action callback
|
||||
) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback;
|
||||
) : base(time, endOn, recurrence) => _callback = callback;
|
||||
|
||||
public override void OnEvent()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,6 +51,152 @@ public class RecurrencePatternTests
|
|||
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]
|
||||
[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)]
|
||||
|
|
@ -145,7 +291,6 @@ public class RecurrencePatternTests
|
|||
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
|
||||
}
|
||||
|
||||
// Additional utility method tests
|
||||
[Theory]
|
||||
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
|
||||
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;
|
||||
|
||||
public CallbackScheduledEvent(
|
||||
DateTime after,
|
||||
TimeOnly time,
|
||||
Action callback,
|
||||
IRecurrencePattern recurrencePattern = null,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : base(after, time, recurrencePattern, timeZone) =>
|
||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
IRecurrencePattern recurrencePattern = null
|
||||
) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
|
||||
public CallbackScheduledEvent(
|
||||
DateTime after,
|
||||
DateTime endOn,
|
||||
TimeOnly time,
|
||||
DateTime endOn,
|
||||
Action callback,
|
||||
IRecurrencePattern recurrencePattern = null,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : base(after, endOn, time, recurrencePattern, timeZone) =>
|
||||
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
IRecurrencePattern recurrencePattern = null
|
||||
) : base(time, endOn, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
|
||||
public override void OnEvent() => _callback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,32 +35,49 @@ public class DailyRecurrencePattern : IRecurrencePattern
|
|||
public class WeeklyRecurrencePattern : IRecurrencePattern
|
||||
{
|
||||
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);
|
||||
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)
|
||||
{
|
||||
var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone);
|
||||
var daysOfWeek = DaysOfWeek == DaysOfWeek.None
|
||||
? (DaysOfWeek)(1 << (int)local.DayOfWeek)
|
||||
: DaysOfWeek;
|
||||
var daysOfWeek = AllowedDays == AllowedDays.None ? local.DayOfWeek.ToDaysOfWeek() : AllowedDays;
|
||||
|
||||
var weekStart = local.Date.AddDays(-(int)local.DayOfWeek);
|
||||
|
||||
// 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 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)
|
||||
{
|
||||
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 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 candidate = new DateTime(year, 1, 1)
|
||||
|
|
@ -132,7 +149,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
|||
var year = local.Year;
|
||||
var month = local.Month;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var nextMonth = month + IntervalMonths * i;
|
||||
var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset);
|
||||
|
|
@ -147,7 +164,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
|||
.Add(time.ToTimeSpan());
|
||||
|
||||
// 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)
|
||||
{
|
||||
daysOffset -= 7;
|
||||
|
|
@ -168,7 +185,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
|
|||
.Add(time.ToTimeSpan());
|
||||
|
||||
// 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)
|
||||
{
|
||||
daysOffset -= 7;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Server.Logging;
|
||||
|
||||
namespace Server.Engines.Events;
|
||||
|
||||
|
|
@ -17,25 +16,9 @@ public interface IRecurrencePattern
|
|||
|
||||
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
|
||||
{
|
||||
private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler));
|
||||
|
||||
private readonly PriorityQueue<ScheduledEvent, DateTime> _schedule = new();
|
||||
private readonly PriorityQueue<BaseScheduledEvent, DateTime> _schedule = new();
|
||||
|
||||
public static EventScheduler Shared { get; private set; }
|
||||
|
||||
|
|
@ -116,16 +99,24 @@ public class EventScheduler : Timer
|
|||
TimeZoneInfo timeZone = null
|
||||
)
|
||||
{
|
||||
var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone);
|
||||
ScheduleEvent(scheduledEvent);
|
||||
var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern);
|
||||
scheduledEvent.Schedule(this, after, timeZone);
|
||||
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)
|
||||
{
|
||||
_schedule.Enqueue(entry, entry.NextOccurrence);
|
||||
_schedule.Remove(entry, out _, out _);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,21 +140,7 @@ public class EventScheduler : Timer
|
|||
continue;
|
||||
}
|
||||
|
||||
DateTime nextOccurrence;
|
||||
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);
|
||||
}
|
||||
entry.Advance(); // Advances the event and self queues if necessary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,59 +2,31 @@ using System;
|
|||
|
||||
namespace Server.Engines.Events;
|
||||
|
||||
public abstract class ScheduledEvent
|
||||
public abstract class ScheduledEvent : BaseScheduledEvent
|
||||
{
|
||||
public IRecurrencePattern Recurrence { get; }
|
||||
public TimeZoneInfo TimeZone { get; }
|
||||
public TimeOnly Time { get; }
|
||||
public DateTime EndDate { get; }
|
||||
public DateTime NextOccurrence { get; protected set; }
|
||||
public bool Cancelled { get; private set; }
|
||||
|
||||
public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null)
|
||||
: this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone)
|
||||
{
|
||||
}
|
||||
|
||||
public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null)
|
||||
: this(startAfter, DateTime.MaxValue, time, recurrence, timeZone)
|
||||
public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null)
|
||||
: this(time, DateTime.MaxValue, recurrence)
|
||||
{
|
||||
}
|
||||
|
||||
public ScheduledEvent(
|
||||
DateTime startAfter,
|
||||
DateTime endOn,
|
||||
TimeOnly time,
|
||||
IRecurrencePattern recurrence,
|
||||
TimeZoneInfo timeZone = null
|
||||
DateTime endOn,
|
||||
IRecurrencePattern recurrence = null
|
||||
)
|
||||
{
|
||||
Time = time;
|
||||
Recurrence = recurrence;
|
||||
TimeZone = timeZone ?? TimeZoneInfo.Utc;
|
||||
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;
|
||||
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;
|
||||
|
||||
protected YearlyCallbackScheduledEvent(
|
||||
DateTime startAfter,
|
||||
TimeOnly time,
|
||||
MonthDay yearlyStart,
|
||||
MonthDay yearlyEnd,
|
||||
Action callback,
|
||||
IRecurrencePattern recurrence,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone)
|
||||
IRecurrencePattern recurrence
|
||||
) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence)
|
||||
{
|
||||
}
|
||||
|
||||
protected YearlyCallbackScheduledEvent(
|
||||
DateTime startAfter,
|
||||
DateTime endOn,
|
||||
TimeOnly time,
|
||||
MonthDay yearlyStart,
|
||||
MonthDay yearlyEnd,
|
||||
DateTime endOn,
|
||||
Action callback,
|
||||
IRecurrencePattern recurrence,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _callback = callback;
|
||||
IRecurrencePattern recurrence
|
||||
) : base(time, yearlyStart, yearlyEnd, endOn, recurrence) => _callback = callback;
|
||||
|
||||
public override void OnEvent() => _callback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,33 +9,29 @@ public abstract class YearlyScheduledEvent : ScheduledEvent
|
|||
public MonthDay YearlyEnd { get; }
|
||||
|
||||
protected YearlyScheduledEvent(
|
||||
DateTime startAfter,
|
||||
TimeOnly time,
|
||||
MonthDay yearlyStart,
|
||||
MonthDay yearlyEnd,
|
||||
IRecurrencePattern recurrence,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone)
|
||||
IRecurrencePattern recurrence
|
||||
) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence)
|
||||
{
|
||||
}
|
||||
|
||||
protected YearlyScheduledEvent(
|
||||
DateTime startAfter,
|
||||
DateTime endOn,
|
||||
TimeOnly time,
|
||||
MonthDay yearlyStart,
|
||||
MonthDay yearlyEnd,
|
||||
IRecurrencePattern recurrence,
|
||||
TimeZoneInfo timeZone = null
|
||||
) : base(startAfter, endOn, time, recurrence, timeZone)
|
||||
DateTime endOn,
|
||||
IRecurrencePattern recurrence
|
||||
) : base(time, endOn, recurrence)
|
||||
{
|
||||
YearlyStart = yearlyStart;
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue