fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178)

This commit is contained in:
Kamron Batman 2025-05-07 22:42:44 -07:00 committed by GitHub
parent 990e86b983
commit f95b3a8b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 347 additions and 137 deletions

View 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);
}

View 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
}

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

View file

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

View file

@ -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;

View file

@ -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
}
}
}

View file

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

View file

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

View file

@ -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)
{