diff --git a/ICD.Common.Utils/Extensions/DateTimeExtensions.cs b/ICD.Common.Utils/Extensions/DateTimeExtensions.cs index 3bc2d63..63ca40f 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,39 @@ 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 earliestTime; + bool success = times.OrderBy(dt => dt).TryFirst(dt => target < dt, out earliestTime); + return success ? earliestTime : (DateTime?) null; + } + + /// + /// 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 latestTime; + bool success = times.OrderByDescending(dt => dt).TryFirst(dt => target > dt, out latestTime); + return success ? latestTime : (DateTime?) null; + } } } 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 dcc20bd..ecb84b7 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -113,6 +113,10 @@ + + + + diff --git a/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs new file mode 100644 index 0000000..3cfbd2f --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/AbstractScheduledAction.cs @@ -0,0 +1,48 @@ +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 = GetNextRunTime(); + } + + public void UpdateNextRunTime() + { + NextRunTime = GetNextRunTime(); + } + + /// + /// 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? 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 new file mode 100644 index 0000000..ea232a0 --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/ActionSchedulerService.cs @@ -0,0 +1,203 @@ +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 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() + { + Clear(); + + m_Timer.Stop(); + m_Timer.Dispose(); + } + + #region Methods + + public void Add(IScheduledAction action) + { + m_CriticalSection.Enter(); + try + { + Subscribe(action); + m_Actions.AddSorted(action, a => a.NextRunTime); + } + finally + { + m_CriticalSection.Leave(); + } + + RescheduleTimer(); + } + + public void Remove(IScheduledAction action) + { + m_CriticalSection.Enter(); + try + { + Unsubscribe(action); + m_Actions.Remove(action); + } + finally + { + 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() + { + 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"); + } + } + + RescheduleTimer(); + } + + private void RescheduleTimer() + { + // enter again to check the closest next run time + m_CriticalSection.Enter(); + try + { + var action = m_Actions.FirstOrDefault(a => a.NextRunTime != null); + if (action == null || action.NextRunTime == null) + { + m_Timer.Stop(); + return; + } + + long msToNextAction = (long)(action.NextRunTime.Value - DateTime.Now).TotalMilliseconds; + m_Timer.Reset(msToNextAction); + } + 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(); + } + + RescheduleTimer(); + } + + #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..cacdfd6 --- /dev/null +++ b/ICD.Common.Utils/Services/Scheduler/IScheduledAction.cs @@ -0,0 +1,20 @@ +using System; +using ICD.Common.Utils.EventArguments; + +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