Browse Source

Added event schedules into the database engine. Also refactored the event type system by adding the IEntityEvent<T> interface.

Kenric Nugteren 5 months ago
parent
commit
7819dbe168

+ 20 - 1
prs.server/Engines/Database/DatabaseEngine.cs

@@ -16,6 +16,7 @@ using InABox.Rpc;
 using InABox.Server;
 using InABox.Wpf.Reports;
 using PRS.Shared;
+using PRS.Shared.Events;
 using PRSServices;
 using Timer = System.Timers.Timer;
 
@@ -25,6 +26,7 @@ public class DatabaseEngine : Engine<DatabaseServerProperties>
 {
     private Timer? _certificateRefreshTimer;
     private Timer? _certificateHaltTimer;
+    private Timer? _scheduleTimer;
     
     private string _ipcPipeName = "";
     private IPCServer? _ipcServer;
@@ -268,8 +270,22 @@ public class DatabaseEngine : Engine<DatabaseServerProperties>
         
         PushManager.AddPollHandler(PollNotifications);
         Logger.Send(LogType.Information, "", $"- Push Notifications Configured");
+
+        if(_scheduleTimer is null)
+        {
+            _scheduleTimer = new Timer(TimeSpan.FromMinutes(1));
+            _scheduleTimer.Elapsed += _scheduleTimer_Elapsed;
+            _scheduleTimer.AutoReset = true;
+        }
+        _scheduleTimer.Start();
+        Logger.Send(LogType.Information, "", "Schedule timer started");
     }
-    
+
+    private void _scheduleTimer_Elapsed(object? sender, ElapsedEventArgs e)
+    {
+        EventUtils.CheckScheduledEvents();
+    }
+
     public override void Stop()
     {
         Logger.Send(LogType.Information, "", "Stopping..");
@@ -279,6 +295,9 @@ public class DatabaseEngine : Engine<DatabaseServerProperties>
 
         _pipeserver?.Stop();
         _pipeserver = null;
+
+        _scheduleTimer?.Stop();
+        _scheduleTimer = null;
         
         _ipcServer?.Dispose();
 

+ 6 - 21
prs.shared/Grids/EventEditor/EventEditor.xaml.cs

@@ -151,13 +151,7 @@ public partial class EventEditor : UserControl, INotifyPropertyChanged
 
     private bool EventTypeHasEntityType(EventType eventType)
     {
-        switch (eventType)
-        {
-            case Comal.Classes.EventType.AfterSave:
-                return true;
-            default:
-                return false;
-        }
+        return EventUtils.GetEventType(eventType).HasInterface(typeof(IEntityEvent<>));
     }
 
     private void UpdateEventData()
@@ -169,23 +163,14 @@ public partial class EventEditor : UserControl, INotifyPropertyChanged
             return;
         }
 
-        Type eventType;
         Type dataModelType;
-        switch (EventType.Value)
+
+        var eventType = EventUtils.GetEventType(EventType.Value);
+        if(eventType.GetInterfaceDefinition(typeof(IEntityEvent<>)) is Type entityEvInt)
         {
-            case Comal.Classes.EventType.AfterSave:
-                eventType = typeof(SaveEvent<>).MakeGenericType(EntityType!);
-                dataModelType = typeof(SaveEventDataModel<>).MakeGenericType(EntityType!);
-                break;
-            case Comal.Classes.EventType.Scheduled:
-                eventType = typeof(ScheduledEvent);
-                dataModelType = typeof(ScheduledEventDataModel);
-                break;
-            default:
-                Data = null;
-                HasProperties = false;
-                return;
+            eventType = eventType.MakeGenericType(EntityType!);
         }
+        dataModelType = eventType.GetInterfaceDefinition(typeof(IEvent<>))!.GenericTypeArguments[0];
 
         HasProperties = eventType.HasInterface(typeof(IPropertiesEvent<>));
 

+ 203 - 11
prs.stores/Events/Event.cs

@@ -245,14 +245,32 @@ public static class EventUtils
     private static Dictionary<EventType, Dictionary<Type, List<(Event Event, IEventData EventData)>>> _entityEvents = new();
     private static Dictionary<Guid, (EventType, Type)> _entityEventMap = new();
 
+    private static Dictionary<EventType, List<(Event Event, IEventData EventData)>> _genericEvents = new();
+    private static Dictionary<Guid, EventType> _genericEventMap = new();
+
+    public static Type GetEventType(EventType type)
+    {
+        switch (type)
+        {
+            case EventType.AfterSave:
+                return typeof(SaveEvent<>);
+            case EventType.Scheduled:
+                return typeof(ScheduledEvent);
+            default:
+                throw new Exception($"Invalid event type {type}");
+        }
+    }
+
     public static void ReloadCache(IProvider provider)
     {
         _loadedCache = true;
         _entityEvents.Clear();
         _entityEventMap.Clear();
+        _genericEvents.Clear();
+        _genericEventMap.Clear();
         var events = provider.Query(
             new Filter<Event>(x => x.Enabled).IsEqualTo(true),
-            Columns.None<Event>().Add(x => x.ID).Add(x => x.Code).Add(x => x.EventType).Add(x => x.Data))
+            Columns.None<Event>().Add(x => x.ID).Add(x => x.Code).Add(x => x.EventType).Add(x => x.Data).Add(x => x.NotificationExpression))
             .ToObjects<Event>();
         foreach(var ev in events)
         {
@@ -260,6 +278,33 @@ public static class EventUtils
         }
     }
 
+    private static void AddGenericEvent(Event ev)
+    {
+        if(ev.Data is not null && ev.Data.Length != 0)
+        {
+            var data = Deserialize(ev.Data);
+
+            var list = _genericEvents.GetValueOrAdd(ev.EventType);
+            list.RemoveAll(x => x.Event.ID == ev.ID);
+            
+            list.Add((ev, data));
+            _genericEventMap[ev.ID] = ev.EventType;
+        }
+    }
+    private static void RemoveGenericEvent(Event ev)
+    {
+        if(_genericEventMap.Remove(ev.ID, out var evType))
+        {
+            if(_genericEvents.TryGetValue(evType, out var eventTypeEvents))
+            {
+                eventTypeEvents.RemoveAll(x => x.Event.ID == ev.ID);
+                if(eventTypeEvents.Count == 0)
+                {
+                    _genericEvents.Remove(evType);
+                }
+            }
+        }
+    }
     private static void AddEntityEvent(Event ev)
     {
         if(ev.Data is not null && ev.Data.Length != 0)
@@ -297,26 +342,32 @@ public static class EventUtils
         }
     }
 
-    public static void RemoveEvent(IProvider provider, Event ev)
+    public static void AddEvent(IProvider provider, Event ev)
     {
         if (!_loadedCache) ReloadCache(provider);
 
-        switch (ev.EventType)
+        var type = GetEventType(ev.EventType);
+        if (type.HasInterface(typeof(IEntityEvent<>)))
         {
-            case EventType.AfterSave:
-                RemoveEntityEvent(ev);
-                break;
+            AddEntityEvent(ev);
+        }
+        else
+        {
+            AddGenericEvent(ev);
         }
     }
-    public static void AddEvent(IProvider provider, Event ev)
+    public static void RemoveEvent(IProvider provider, Event ev)
     {
         if (!_loadedCache) ReloadCache(provider);
 
-        switch (ev.EventType)
+        var type = GetEventType(ev.EventType);
+        if (type.HasInterface(typeof(IEntityEvent<>)))
         {
-            case EventType.AfterSave:
-                AddEntityEvent(ev);
-                break;
+            RemoveEntityEvent(ev);
+        }
+        else
+        {
+            RemoveGenericEvent(ev);
         }
     }
 
@@ -344,6 +395,138 @@ public static class EventUtils
         }
     }
 
+    private static bool CheckEventSchedule(ScheduledEvent ev)
+    {
+        var now = DateTime.Now;
+        switch (ev.Properties.Period)
+        {
+            case SchedulePeriod.Minute:
+            case SchedulePeriod.Hour:
+            case SchedulePeriod.Day:
+                var dayOfWeekSettings = ev.Properties.DayOfWeekSettings.FirstOrDefault(x => x.DayOfWeek == now.DayOfWeek);
+                if(dayOfWeekSettings is null || !dayOfWeekSettings.Enabled)
+                {
+                    return false;
+                }
+                var start = now - now.TimeOfDay + dayOfWeekSettings.StartTime;
+                var end = now - now.TimeOfDay + dayOfWeekSettings.EndTime;
+                if(ev.Properties.Period == SchedulePeriod.Day)
+                {
+                    if(ev.Properties.LastExecution < start && now >= start && (now.Date - ev.Properties.LastExecution.Date).TotalDays >= ev.Properties.Frequency)
+                    {
+                        ev.Properties.LastExecution = start;
+                        return true;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+                else if(ev.Properties.Period == SchedulePeriod.Hour)
+                {
+                    if(start <= now && now <= end)
+                    {
+                        var nowIntervals = Math.Floor((now - start).TotalHours / ev.Properties.Frequency);
+                        var lastIntervals = Math.Floor((ev.Properties.LastExecution - start).TotalHours / ev.Properties.Frequency);
+                        if(nowIntervals > lastIntervals)
+                        {
+                            ev.Properties.LastExecution = start.AddHours(nowIntervals * ev.Properties.Frequency);
+                        }
+                        return true;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+                else if(ev.Properties.Period == SchedulePeriod.Minute)
+                {
+                    if (start <= now && now <= end)
+                    {
+                        var nowIntervals = Math.Floor((now - start).TotalMinutes / ev.Properties.Frequency);
+                        var lastIntervals = Math.Floor((ev.Properties.LastExecution - start).TotalMinutes / ev.Properties.Frequency);
+                        if (nowIntervals > lastIntervals)
+                        {
+                            ev.Properties.LastExecution = start.AddMinutes(nowIntervals * ev.Properties.Frequency);
+                        }
+                        return true;
+                    }
+                    else
+                    {
+                        return false;
+                    }
+                }
+                else
+                {
+                    throw new Exception("Invalid state");
+                }
+            case SchedulePeriod.Week:
+                if (now >= ev.Properties.NextSchedule)
+                {
+                    while (now >= ev.Properties.NextSchedule)
+                    {
+                        ev.Properties.NextSchedule += TimeSpan.FromDays(7 * ev.Properties.Frequency);
+                    }
+                    return true;
+                }
+                else
+                {
+                    return false;
+                }
+            case SchedulePeriod.Month:
+                if(now >= ev.Properties.NextSchedule)
+                {
+                    while(now >= ev.Properties.NextSchedule)
+                    {
+                        ev.Properties.NextSchedule = ev.Properties.NextSchedule.AddMonths(ev.Properties.Frequency);
+                    }
+                    return true;
+                }
+                else
+                {
+                    return false;
+                }
+            case SchedulePeriod.Year:
+                if(now >= ev.Properties.NextSchedule)
+                {
+                    while(now >= ev.Properties.NextSchedule)
+                    {
+                        ev.Properties.NextSchedule = ev.Properties.NextSchedule.AddYears(ev.Properties.Frequency);
+                    }
+                    return true;
+                }
+                else
+                {
+                    return false;
+                }
+            default:
+                throw new Exception("Invalid state");
+        }
+    }
+    public static void CheckScheduledEvents()
+    {
+        var store = DbFactory.FindStore<Event>(Guid.Empty, "", Platform.DatabaseEngine, CoreUtils.GetVersion(), Logger.New());
+
+        if (!_loadedCache) ReloadCache(store.Provider);
+
+        var events = _genericEvents.GetValueOrDefault(EventType.Scheduled);
+        if (events is null) return;
+
+        foreach(var ev in events)
+        {
+            var eventData = (ev.EventData as EventData<ScheduledEvent, ScheduledEventDataModel>)!;
+            if (CheckEventSchedule(eventData.Event))
+            {
+                var model = new ScheduledEventDataModel();
+                eventData.Event.Init(store, eventData, model);
+                Run(store, ev.Event, eventData, model);
+
+                ev.Event.Data = Serialize(eventData);
+                store.Provider.Save(ev.Event);
+            }
+        }
+    }
+
     #endregion
 }
 
@@ -530,6 +713,15 @@ public interface IPropertiesEvent<TProperties>
     void SetProperties(TProperties properties);
 }
 
+/// <summary>
+/// Indicates that this event is generic for type <typeparamref name="T"/>; if this interface is implemented,
+/// the main type <b>must</b> be generic, with only one generic type argument.
+/// </summary>
+public interface IEntityEvent<T>
+    where T : Entity
+{
+}
+
 public interface IEvent<TDataModel> : IEvent
 {
     void Init(IStore store, IEventData data, TDataModel model);

+ 1 - 1
prs.stores/Events/SaveEvent.cs

@@ -12,7 +12,7 @@ using System.Text;
 
 namespace PRS.Shared.Events;
 
-public class SaveEvent<T> : IEvent<SaveEventDataModel<T>>
+public class SaveEvent<T> : IEvent<SaveEventDataModel<T>>, IEntityEvent<T>
     where T : Entity, new()
 {
     public Type Entity => typeof(T);

+ 34 - 0
prs.stores/Events/ScheduledEvent.cs

@@ -5,6 +5,7 @@ using InABox.Scripting;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.Serialization;
 using System.Text;
 using System.Threading.Tasks;
 
@@ -44,6 +45,28 @@ public class ScheduledEventDayOfWeek : BaseObject
     }
 }
 
+/// <summary>
+///     Properties class to manage the timing of a scheduled event.
+/// </summary>
+/// <remarks>
+///     <para>
+///         This is fairly complex. First, we have the <see cref="Frequency"/> and <see cref="Period"/> properties. These function as expected.
+///         If <see cref="Frequency"/> is 3, and <see cref="Period"/> is <see cref="SchedulePeriod.Hour"/>, the event happens every three hours. Similarly,
+///         if <see cref="Frequency"/> is 4, and <see cref="Period"/> is <see cref="SchedulePeriod.Month"/>, the event happens every four months.
+///     </para>
+///     <para>
+///         However, the other properties are dependent on <see cref="Period"/>. If <see cref="Period"/> is <see cref="SchedulePeriod.Minute"/>,
+///         <see cref="SchedulePeriod.Hour"/> or <see cref="SchedulePeriod.Day"/> (that is, anything a day or smaller), the <see cref="DayOfWeekSettings"/>
+///         is used, which says which days of the week the schedule should occur, and at what time on those days.
+///         If the period is <see cref="SchedulePeriod.Day"/>, then only the <see cref="ScheduledEventDayOfWeek.StartTime"/> is used, and the event occurs
+///         at that time. Otherwise, the event occurs at regular intervals according to the frequency and period, beginning at
+///         <see cref="ScheduledEventDayOfWeek.StartTime"/> and running until <see cref="ScheduledEventDayOfWeek.EndTime"/>.
+///     </para>
+///     <para>
+///         Alternatively, if the period is greater than a day, <see cref="NextSchedule"/> is used, and whenever the schedule executes, it is updated by the
+///         frequency and period <i>from the due date</i>, and not from when the schedule actually occurred.
+///     </para>
+/// </remarks>
 public class ScheduledEventProperties : BaseObject
 {
     [EditorSequence(1)]
@@ -55,6 +78,11 @@ public class ScheduledEventProperties : BaseObject
     [EditorSequence(3)]
     public DateTime NextSchedule { get; set; }
 
+    [EditorSequence(4)]
+    [Editable(Editable.Disabled)]
+    [TimestampEditor]
+    public DateTime LastExecution { get; set; }
+
     [EmbeddedListEditor(typeof(ScheduledEventDayOfWeek), AddRows = false)]
     public List<ScheduledEventDayOfWeek> DayOfWeekSettings { get; set; } = new()
     {
@@ -66,6 +94,12 @@ public class ScheduledEventProperties : BaseObject
         new(DayOfWeek.Saturday),
         new(DayOfWeek.Sunday),
     };
+
+    [OnDeserializing]
+    private void OnDeserialisingMethod(StreamingContext context)
+    {
+        DayOfWeekSettings.Clear();
+    }
 }
 
 public class ScheduledEvent : IEvent<ScheduledEventDataModel>, IPropertiesEvent<ScheduledEventProperties>