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