From 928f8e5e0429f44a457d206ccecdced796435312 Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Wed, 11 Jul 2018 16:35:08 -0400 Subject: [PATCH 1/6] feat: first go at ActionSchedulerService --- .../ICD.Common.Utils_SimplSharp.csproj | 3 + .../Scheduler/ActionSchedulerService.cs | 176 ++++++++++++++++++ .../Scheduler/IActionSchedulerService.cs | 9 + .../Services/Scheduler/IScheduledAction.cs | 19 ++ 4 files changed, 207 insertions(+) create mode 100644 ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs create mode 100644 ICD.Common.Utils/Services/Scheduler/IActionSchedulerService.cs create mode 100644 ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index dcc20bd..f1b77a5 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -113,6 +113,9 @@ + + + diff --git a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs new file mode 100644 index 0000000..ca601d6 --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ICD.Common.Utils.Extensions; +using ICD.Common.Utils.Services.Logging; +using ICD.Common.Utils.Timers; + +namespace ICD.Common.Utils.Services.Scheduler +{ + public sealed class ActionSchedulerService : IActionSchedulerService, IDisposable + { + private const long MAX_TIMER_INTERVAL = 5*60*1000; // 5 minutes + + private readonly List m_Actions; + private readonly SafeTimer m_Timer; + private readonly SafeCriticalSection m_CriticalSection; + + public ActionSchedulerService() + { + m_Actions = new List(); + m_Timer = new SafeTimer(TimerCallback, -1); + m_CriticalSection = new SafeCriticalSection(); + } + + public void Dispose() + { + foreach (IScheduledAction action in m_Actions) + { + Unsubscribe(action); + + IDisposable disposable = action as IDisposable; + if (disposable != null) + disposable.Dispose(); + } + + m_Actions.Clear(); + } + + #region Methods + + public void Add(IScheduledAction action) + { + m_CriticalSection.Enter(); + try + { + Subscribe(action); + m_Actions.AddSorted(action, a => a.NextRunTime); + } + finally + { + m_CriticalSection.Leave(); + } + } + + public void Remove(IScheduledAction action) + { + m_CriticalSection.Enter(); + try + { + Unsubscribe(action); + m_Actions.Remove(action); + } + finally + { + m_CriticalSection.Leave(); + } + } + + public override string ToString() + { + return "ActionSchedulerService"; + } + + #endregion + + #region Private Methods + + private void TimerCallback() + { + IScheduledAction[] actionsToRun; + + m_CriticalSection.Enter(); + try + { + actionsToRun = m_Actions + .Where(a => a.NextRunTime < DateTime.Now) + .OrderBy(a => a.NextRunTime) + .ToArray(); + } + finally + { + m_CriticalSection.Leave(); + } + + // leave the critical section so any actions that run can enter + foreach (IScheduledAction action in actionsToRun) + { + try + { + action.Run(); + } + catch (Exception ex) + { + Log(eSeverity.Error, ex, "Error occurred while running scheduled action"); + } + } + + // enter again to check the closest next run time + m_CriticalSection.Enter(); + try + { + var action = m_Actions.FirstOrDefault(); + if (action == null) + return; + + m_Timer.Reset((long)(DateTime.Now - action.NextRunTime).TotalMilliseconds); + } + finally + { + m_CriticalSection.Leave(); + } + } + + private void Log(eSeverity severity, string message, params object[] args) + { + ILoggerService logger = ServiceProvider.TryGetService(); + if (logger == null) + return; + + logger.AddEntry(severity, string.Format("{0} - {1}", this, message), args); + } + + private void Log(eSeverity severity, Exception ex, string message, params object[] args) + { + ILoggerService logger = ServiceProvider.TryGetService(); + if (logger == null) + return; + + logger.AddEntry(severity, ex, string.Format("{0} - {1}", this, message), args); + } + + #endregion + + #region Action Callbacks + + private void Subscribe(IScheduledAction action) + { + action.OnScheduledRunTimeChanged += ActionOnScheduledRunTimeChanged; + } + + private void Unsubscribe(IScheduledAction action) + { + action.OnScheduledRunTimeChanged -= ActionOnScheduledRunTimeChanged; + } + + private void ActionOnScheduledRunTimeChanged(object sender, EventArgs eventArgs) + { + IScheduledAction action = sender as IScheduledAction; + if (action == null) + return; + + m_CriticalSection.Enter(); + try + { + m_Actions.Remove(action); + m_Actions.AddSorted(action, a => a.NextRunTime); + } + finally + { + m_CriticalSection.Leave(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Services/Scheduler/IActionSchedulerService.cs b/ICD.Common.Utils/Services/Scheduler/IActionSchedulerService.cs new file mode 100644 index 0000000..2e0bf1a --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/IActionSchedulerService.cs @@ -0,0 +1,9 @@ +namespace ICD.Common.Utils.Services.Scheduler +{ + public interface IActionSchedulerService + { + void Add(IScheduledAction action); + + void Remove(IScheduledAction action); + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs b/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs new file mode 100644 index 0000000..de0f246 --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs @@ -0,0 +1,19 @@ +using System; + +namespace ICD.Common.Utils.Services.Scheduler +{ + public interface IScheduledAction + { + /// + /// Raised when the scheduled run time has changed. The sender of the event must be the action itself + /// + event EventHandler OnScheduledRunTimeChanged; + + /// + /// Gets the next time this action should be run + /// + DateTime NextRunTime { get; } + + void Run(); + } +} \ No newline at end of file From 465ac7c42c743c3dd6739b94e97eea856faa6b9f Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Thu, 12 Jul 2018 13:33:02 -0400 Subject: [PATCH 2/6] feat: timer rescheduling, abstract scheduled action, datetime and dayofweek extensions --- .../Extensions/DateTimeExtensions.cs | 18 +++++++ .../Extensions/DayOfWeekExtensions.cs | 17 ++++++ .../ICD.Common.Utils_SimplSharp.csproj | 1 + .../Scheduler/AbstractScheduledAction.cs | 43 +++++++++++++++ .../Scheduler/ActionSchedulerService.cs | 54 ++++++++++++++----- .../Services/Scheduler/IScheduledAction.cs | 3 +- 6 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 ICD.Common.Utils/Extensions/DayOfWeekExtensions.cs create mode 100644 ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs diff --git a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs index 3bc2d63..77f0b14 100644 --- a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs +++ b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; namespace ICD.Common.Utils.Extensions { @@ -28,5 +29,22 @@ namespace ICD.Common.Utils.Extensions // Todo - Better handle different cultures return extends.ToString("HH:mm:ss:fff"); } + + /// + /// Returns the closest DateTime to the target time that is greater than the target time + /// + /// + /// + /// + /// + public static DateTime? NextEarliestTime(this DateTime target, params DateTime[] times) + { + if (times.Length == 0) + return null; + + DateTime[] orderedTimes = times.OrderBy(dt => dt).ToArray(); + var time = orderedTimes.FirstOrDefault(dt => target < dt); + return time == default(DateTime) ? (DateTime?) null : time; + } } } diff --git a/ICD.Common.Utils/Extensions/DayOfWeekExtensions.cs b/ICD.Common.Utils/Extensions/DayOfWeekExtensions.cs new file mode 100644 index 0000000..3b3d3f2 --- /dev/null +++ b/ICD.Common.Utils/Extensions/DayOfWeekExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace ICD.Common.Utils.Extensions +{ + public static class DayOfWeekExtensions + { + public static bool IsWeekday(this DayOfWeek day) + { + return !IsWeekend(day); + } + + public static bool IsWeekend(this DayOfWeek day) + { + return day == DayOfWeek.Saturday || day == DayOfWeek.Sunday; + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index f1b77a5..ecb84b7 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -113,6 +113,7 @@ + diff --git a/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs new file mode 100644 index 0000000..0439f07 --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs @@ -0,0 +1,43 @@ +using System; +using ICD.Common.Utils.EventArguments; +using ICD.Common.Utils.Extensions; + +namespace ICD.Common.Utils.Services.Scheduler +{ + public abstract class AbstractScheduledAction : IScheduledAction + { + public event EventHandler OnScheduledRunTimeChanged; + + private DateTime? m_NextRunTime; + + public DateTime? NextRunTime + { + get { return m_NextRunTime; } + private set + { + if (m_NextRunTime == value) + return; + + m_NextRunTime = value; + + OnScheduledRunTimeChanged.Raise(this); + } + } + + public void Run() + { + RunFinal(); + NextRunTime = UpdateRunTime(); + } + + /// + /// Runs when the action has hit its scheduled time + /// + public abstract void RunFinal(); + + /// + /// Runs after RunFinal in order to set the next run time of this action + /// + public abstract DateTime? UpdateRunTime(); + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs index ca601d6..d98fb66 100644 --- a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs +++ b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs @@ -24,16 +24,10 @@ namespace ICD.Common.Utils.Services.Scheduler public void Dispose() { - foreach (IScheduledAction action in m_Actions) - { - Unsubscribe(action); + Clear(); - IDisposable disposable = action as IDisposable; - if (disposable != null) - disposable.Dispose(); - } - - m_Actions.Clear(); + m_Timer.Stop(); + m_Timer.Dispose(); } #region Methods @@ -50,6 +44,8 @@ namespace ICD.Common.Utils.Services.Scheduler { m_CriticalSection.Leave(); } + + RescheduleTimer(); } public void Remove(IScheduledAction action) @@ -64,6 +60,28 @@ namespace ICD.Common.Utils.Services.Scheduler { m_CriticalSection.Leave(); } + + RescheduleTimer(); + } + + public void Clear() + { + m_CriticalSection.Enter(); + try + { + foreach (IScheduledAction action in m_Actions) + { + Unsubscribe(action); + } + + m_Actions.Clear(); + } + finally + { + m_CriticalSection.Leave(); + } + + RescheduleTimer(); } public override string ToString() @@ -105,15 +123,25 @@ namespace ICD.Common.Utils.Services.Scheduler } } + RescheduleTimer(); + } + + private void RescheduleTimer() + { // enter again to check the closest next run time m_CriticalSection.Enter(); try { - var action = m_Actions.FirstOrDefault(); - if (action == null) + var action = m_Actions.FirstOrDefault(a => a.NextRunTime != null); + if (action == null || action.NextRunTime == null) + { + m_Timer.Stop(); return; + } - m_Timer.Reset((long)(DateTime.Now - action.NextRunTime).TotalMilliseconds); + long msToNextAction = (long)(DateTime.Now - action.NextRunTime.Value).TotalMilliseconds; + long timerDueTime = Math.Min(msToNextAction, MAX_TIMER_INTERVAL); + m_Timer.Reset(timerDueTime); } finally { @@ -169,6 +197,8 @@ namespace ICD.Common.Utils.Services.Scheduler { m_CriticalSection.Leave(); } + + RescheduleTimer(); } #endregion diff --git a/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs b/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs index de0f246..cacdfd6 100644 --- a/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs +++ b/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs @@ -1,4 +1,5 @@ using System; +using ICD.Common.Utils.EventArguments; namespace ICD.Common.Utils.Services.Scheduler { @@ -12,7 +13,7 @@ namespace ICD.Common.Utils.Services.Scheduler /// /// Gets the next time this action should be run /// - DateTime NextRunTime { get; } + DateTime? NextRunTime { get; } void Run(); } From 1f7819e53607afa2dd6ea004966ae5d84c2fa40c Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Thu, 12 Jul 2018 14:03:15 -0400 Subject: [PATCH 3/6] feat: add DateTimeExtensions.PreviousLatestTime --- .../Extensions/DateTimeExtensions.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs index 77f0b14..8a95078 100644 --- a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs +++ b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs @@ -46,5 +46,22 @@ namespace ICD.Common.Utils.Extensions var time = orderedTimes.FirstOrDefault(dt => target < dt); return time == default(DateTime) ? (DateTime?) null : time; } + + /// + /// Returns the closest DateTime to the target time that is less than the target time + /// + /// + /// + /// + /// + public static DateTime? PreviousLatestTime(this DateTime target, params DateTime[] times) + { + if (times.Length == 0) + return null; + + DateTime[] orderedTimes = times.OrderByDescending(dt => dt).ToArray(); + var time = orderedTimes.FirstOrDefault(dt => target > dt); + return time == default(DateTime) ? (DateTime?) null : time; + } } } From c25a82399dcf6c0739501e7c2f6d9ec9ada619ec Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Thu, 12 Jul 2018 14:36:58 -0400 Subject: [PATCH 4/6] fix: remove unnecessary default timer interval --- .../Services/Scheduler/ActionSchedulerService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs index d98fb66..ee59816 100644 --- a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs +++ b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs @@ -9,8 +9,6 @@ namespace ICD.Common.Utils.Services.Scheduler { public sealed class ActionSchedulerService : IActionSchedulerService, IDisposable { - private const long MAX_TIMER_INTERVAL = 5*60*1000; // 5 minutes - private readonly List m_Actions; private readonly SafeTimer m_Timer; private readonly SafeCriticalSection m_CriticalSection; @@ -140,8 +138,7 @@ namespace ICD.Common.Utils.Services.Scheduler } long msToNextAction = (long)(DateTime.Now - action.NextRunTime.Value).TotalMilliseconds; - long timerDueTime = Math.Min(msToNextAction, MAX_TIMER_INTERVAL); - m_Timer.Reset(timerDueTime); + m_Timer.Reset(msToNextAction); } finally { From a46987aabfb175c7e3ec07b5887406aabecbbf76 Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Thu, 12 Jul 2018 15:09:52 -0400 Subject: [PATCH 5/6] refactor: UpdateRunTime -> GetNextRunTime, add UpdateRunTime for caching GetNextRunTime value - also swap the operands on the delta time calculation --- .../Services/Scheduler/AbstractScheduledAction.cs | 9 +++++++-- .../Services/Scheduler/ActionSchedulerService.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs index 0439f07..3cfbd2f 100644 --- a/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs +++ b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs @@ -27,7 +27,12 @@ namespace ICD.Common.Utils.Services.Scheduler public void Run() { RunFinal(); - NextRunTime = UpdateRunTime(); + NextRunTime = GetNextRunTime(); + } + + public void UpdateNextRunTime() + { + NextRunTime = GetNextRunTime(); } /// @@ -38,6 +43,6 @@ namespace ICD.Common.Utils.Services.Scheduler /// /// Runs after RunFinal in order to set the next run time of this action /// - public abstract DateTime? UpdateRunTime(); + public abstract DateTime? GetNextRunTime(); } } \ No newline at end of file diff --git a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs index ee59816..ea232a0 100644 --- a/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs +++ b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs @@ -137,7 +137,7 @@ namespace ICD.Common.Utils.Services.Scheduler return; } - long msToNextAction = (long)(DateTime.Now - action.NextRunTime.Value).TotalMilliseconds; + long msToNextAction = (long)(action.NextRunTime.Value - DateTime.Now).TotalMilliseconds; m_Timer.Reset(msToNextAction); } finally From 1070cb00f8f19a11549cf9802a4c62fd316d1d74 Mon Sep 17 00:00:00 2001 From: Jeffery Thompson Date: Thu, 12 Jul 2018 15:48:45 -0400 Subject: [PATCH 6/6] fix: use TryFirst instead of comparison to default(DateTime) --- ICD.Common.Utils/Extensions/DateTimeExtensions.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs index 8a95078..63ca40f 100644 --- a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs +++ b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs @@ -42,9 +42,9 @@ namespace ICD.Common.Utils.Extensions if (times.Length == 0) return null; - DateTime[] orderedTimes = times.OrderBy(dt => dt).ToArray(); - var time = orderedTimes.FirstOrDefault(dt => target < dt); - return time == default(DateTime) ? (DateTime?) null : time; + DateTime earliestTime; + bool success = times.OrderBy(dt => dt).TryFirst(dt => target < dt, out earliestTime); + return success ? earliestTime : (DateTime?) null; } /// @@ -59,9 +59,9 @@ namespace ICD.Common.Utils.Extensions if (times.Length == 0) return null; - DateTime[] orderedTimes = times.OrderByDescending(dt => dt).ToArray(); - var time = orderedTimes.FirstOrDefault(dt => target > dt); - return time == default(DateTime) ? (DateTime?) null : time; + DateTime latestTime; + bool success = times.OrderByDescending(dt => dt).TryFirst(dt => target > dt, out latestTime); + return success ? latestTime : (DateTime?) null; } } }