From 4b9002b520b65717b249663b0282896b0bad0b92 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Mon, 8 Jun 2009 20:32:14 +0000 Subject: [PATCH] Renamed DefaultTimeSource to GenericTimeSource; the generic time source will now detect when the system date/time is adjusted; made some progress on the Scheduler implementation; wrote some unit tests for the Scheduler class git-svn-id: file:///srv/devel/repo-conversion/nusu@144 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (Xbox 360).csproj | 9 +- Nuclex.Support.csproj | 9 +- ...urce.Test.cs => GenericTimeSource.Test.cs} | 14 +- ...aultTimeSource.cs => GenericTimeSource.cs} | 58 ++- Source/Scheduling/ITimeSource.cs | 4 +- Source/Scheduling/Scheduler.Test.cs | 105 ++++++ Source/Scheduling/Scheduler.cs | 345 +++++++++++++++++- Source/Scheduling/WindowsTimeSource.cs | 4 +- 8 files changed, 519 insertions(+), 29 deletions(-) rename Source/Scheduling/{DefaultTimeSource.Test.cs => GenericTimeSource.Test.cs} (82%) rename Source/Scheduling/{DefaultTimeSource.cs => GenericTimeSource.cs} (67%) create mode 100644 Source/Scheduling/Scheduler.Test.cs diff --git a/Nuclex.Support (Xbox 360).csproj b/Nuclex.Support (Xbox 360).csproj index d716fe7..151973f 100644 --- a/Nuclex.Support (Xbox 360).csproj +++ b/Nuclex.Support (Xbox 360).csproj @@ -122,12 +122,15 @@ PrototypeFactory.cs - - DefaultTimeSource.cs + + GenericTimeSource.cs - + + + Scheduler.cs + WindowsTimeSource.cs diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 2d2affa..bf1bc76 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -104,12 +104,15 @@ PrototypeFactory.cs - - DefaultTimeSource.cs + + GenericTimeSource.cs - + + + Scheduler.cs + WindowsTimeSource.cs diff --git a/Source/Scheduling/DefaultTimeSource.Test.cs b/Source/Scheduling/GenericTimeSource.Test.cs similarity index 82% rename from Source/Scheduling/DefaultTimeSource.Test.cs rename to Source/Scheduling/GenericTimeSource.Test.cs index 9f2fe2f..c00feaf 100644 --- a/Source/Scheduling/DefaultTimeSource.Test.cs +++ b/Source/Scheduling/GenericTimeSource.Test.cs @@ -28,16 +28,16 @@ using NUnit.Framework; namespace Nuclex.Support.Scheduling { - /// Unit Test for the default scheduler time source + /// Unit Test for the generic scheduler time source [TestFixture] - public class DefaultTimeSourceTest { + public class GenericTimeSourceTest { /// /// Verifies that the time source's default constructor is working /// [Test] public void TestDefaultConstructor() { - DefaultTimeSource timeSource = new DefaultTimeSource(); + GenericTimeSource timeSource = new GenericTimeSource(); } /// @@ -45,7 +45,7 @@ namespace Nuclex.Support.Scheduling { /// [Test] public void TestCurrentUtcTime() { - DefaultTimeSource timeSource = new DefaultTimeSource(); + GenericTimeSource timeSource = new GenericTimeSource(); Assert.That( timeSource.CurrentUtcTime, Is.EqualTo(DateTime.UtcNow).Within(10).Seconds @@ -58,7 +58,7 @@ namespace Nuclex.Support.Scheduling { /// [Test] public void TestTicksWithStopwatch() { - DefaultTimeSource timeSource = new DefaultTimeSource(true); + GenericTimeSource timeSource = new GenericTimeSource(true); long ticks1 = timeSource.Ticks; long ticks2 = timeSource.Ticks; @@ -71,7 +71,7 @@ namespace Nuclex.Support.Scheduling { /// [Test] public void TestTicksWithTickCount() { - DefaultTimeSource timeSource = new DefaultTimeSource(false); + GenericTimeSource timeSource = new GenericTimeSource(false); long ticks1 = timeSource.Ticks; long ticks2 = timeSource.Ticks; @@ -83,7 +83,7 @@ namespace Nuclex.Support.Scheduling { /// [Test] public void TestWaitOne() { - DefaultTimeSource timeSource = new DefaultTimeSource(); + GenericTimeSource timeSource = new GenericTimeSource(); AutoResetEvent waitEvent = new AutoResetEvent(true); Assert.IsTrue(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks)); diff --git a/Source/Scheduling/DefaultTimeSource.cs b/Source/Scheduling/GenericTimeSource.cs similarity index 67% rename from Source/Scheduling/DefaultTimeSource.cs rename to Source/Scheduling/GenericTimeSource.cs index 52862a1..5ab5998 100644 --- a/Source/Scheduling/DefaultTimeSource.cs +++ b/Source/Scheduling/GenericTimeSource.cs @@ -26,13 +26,20 @@ using System.Threading; namespace Nuclex.Support.Scheduling { /// - /// Default time source implementation using the Stopwatch or Environment.TickCount + /// Generic time source implementation using the Stopwatch or Environment.TickCount /// - public class DefaultTimeSource : ITimeSource { + public class GenericTimeSource : ITimeSource { /// Number of ticks (100 ns intervals) in a millisecond private const long TicksPerMillisecond = 10000; + /// Tolerance for the detection of a date/time adjustment + /// + /// If the current system date/time jumps by more than this tolerance into any + /// direction, the default time source will trigger the DateTimeAdjusted event. + /// + private const long TimeAdjustmentToleranceTicks = 75 * TicksPerMillisecond; + /// Called when the system date/time are adjusted /// /// An adjustment is a change out of the ordinary, eg. when a time synchronization @@ -42,13 +49,13 @@ namespace Nuclex.Support.Scheduling { public event EventHandler DateTimeAdjusted; /// Initializes the static fields of the default time source - static DefaultTimeSource() { + static GenericTimeSource() { tickFrequency = 10000000.0; tickFrequency /= (double)Stopwatch.Frequency; } /// Initializes the default time source - public DefaultTimeSource() : this(Stopwatch.IsHighResolution) { } + public GenericTimeSource() : this(Stopwatch.IsHighResolution) { } /// Initializes the default time source /// @@ -67,23 +74,25 @@ namespace Nuclex.Support.Scheduling { /// but then your won't profit from the high-resolution timer if one is available. /// /// - public DefaultTimeSource(bool useStopwatch) { + public GenericTimeSource(bool useStopwatch) { this.useStopwatch = useStopwatch; + + checkForTimeAdjustment(); } - /// Waits for an AutoResetEvent become signalled + /// Waits for an AutoResetEvent to become signalled /// WaitHandle the method will wait for /// Number of ticks to wait /// /// True if the WaitHandle was signalled, false if the timeout was reached /// public virtual bool WaitOne(AutoResetEvent waitHandle, long ticks) { + checkForTimeAdjustment(); // Force a timeout at least each second so the caller can check the system time // since we're not able to provide the DateTimeAdjusted notification int milliseconds = (int)(ticks / TicksPerMillisecond); return waitHandle.WaitOne(Math.Min(1000, milliseconds), false); - } /// Current system time in UTC format @@ -127,6 +136,41 @@ namespace Nuclex.Support.Scheduling { } } + /// + /// Checks whether the system/date time have been adjusted since the last call + /// + private void checkForTimeAdjustment() { + + // Grab the current date/time and timer ticks in one go + DateTime currentLocalTime = DateTime.Now; + long currentTicks = Ticks; + + // Calculate the number of timer ticks that have passed since the last check and + // extrapolate the local date/time we should be expecting to see + long ticksSinceLastCheck = currentTicks - lastCheckedTicks; + DateTime expectedLocalTime = new DateTime( + lastCheckedLocalTime.Ticks + ticksSinceLastCheck, DateTimeKind.Local + ); + + // Find out by what amount the actual local date/time deviates from + // the extrapolated date/time and trigger the date/time adjustment event if + // we can reasonably assume that the system date/time have been adjusted. + long deviationTicks = Math.Abs(expectedLocalTime.Ticks - currentLocalTime.Ticks); + if(deviationTicks > TimeAdjustmentToleranceTicks) { + OnDateTimeAdjusted(this, EventArgs.Empty); + } + + // Remember the current local date/time and timer ticks for the next run + this.lastCheckedLocalTime = currentLocalTime; + this.lastCheckedTicks = currentTicks; + + } + + /// Last local time we checked for a date/time adjustment + private DateTime lastCheckedLocalTime; + /// Timer ticks at which we last checked the local time + private long lastCheckedTicks; + /// Number of ticks per Stopwatch time unit private static double tickFrequency; /// Whether ot use the Stopwatch class for measuring time diff --git a/Source/Scheduling/ITimeSource.cs b/Source/Scheduling/ITimeSource.cs index 72dd5c6..beee0b7 100644 --- a/Source/Scheduling/ITimeSource.cs +++ b/Source/Scheduling/ITimeSource.cs @@ -25,7 +25,7 @@ using System.Threading; namespace Nuclex.Support.Scheduling { /// Provides time measurement and change notification services - interface ITimeSource { + public interface ITimeSource { /// Called when the system date/time are adjusted /// @@ -35,7 +35,7 @@ namespace Nuclex.Support.Scheduling { /// event EventHandler DateTimeAdjusted; - /// Waits for an AutoResetEvent become signalled + /// Waits for an AutoResetEvent to become signalled /// WaitHandle the method will wait for /// Number of ticks to wait /// diff --git a/Source/Scheduling/Scheduler.Test.cs b/Source/Scheduling/Scheduler.Test.cs new file mode 100644 index 0000000..0a6d9f2 --- /dev/null +++ b/Source/Scheduling/Scheduler.Test.cs @@ -0,0 +1,105 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2009 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +using Microsoft.Win32; + +using NUnit.Framework; + +#if false + +namespace Nuclex.Support.Scheduling { + + /// Unit Test for the scheduler + [TestFixture] + public class SchedulerTest { + + /// + /// Test whether the Scheduler can explicitely create a windows time source + /// + [Test] + public void TestCreateWindowsTimeSource() { + ITimeSource timeSource = Scheduler.CreateTimeSource(true); + try { + Assert.That(timeSource is WindowsTimeSource); + } + finally { + IDisposable disposableTimeSource = timeSource as IDisposable; + if(disposableTimeSource != null) { + disposableTimeSource.Dispose(); + } + } + } + + /// + /// Test whether the Scheduler can explicitely create a generic time source + /// + [Test] + public void TestCreateGenericTimeSource() { + ITimeSource timeSource = Scheduler.CreateTimeSource(false); + try { + Assert.That(timeSource is GenericTimeSource); + } + finally { + IDisposable disposableTimeSource = timeSource as IDisposable; + if(disposableTimeSource != null) { + disposableTimeSource.Dispose(); + } + } + } + + /// + /// Test whether the Scheduler can automatically choose the right time source + /// + [Test] + public void TestCreateDefaultTimeSource() { + ITimeSource timeSource = Scheduler.CreateDefaultTimeSource(); + try { + Assert.IsNotNull(timeSource); + } + finally { + IDisposable disposableTimeSource = timeSource as IDisposable; + if(disposableTimeSource != null) { + disposableTimeSource.Dispose(); + } + } + } + + /// + /// Verifies that the default constructor of the scheduler is working + /// + [Test] + public void TestDefaultConstructor() { + using(Scheduler scheduler = new Scheduler()) { } + } + + } + +} // namespace Nuclex.Support.Scheduling + +#endif + +#endif // UNITTEST diff --git a/Source/Scheduling/Scheduler.cs b/Source/Scheduling/Scheduler.cs index f13fa24..0363553 100644 --- a/Source/Scheduling/Scheduler.cs +++ b/Source/Scheduling/Scheduler.cs @@ -23,20 +23,355 @@ using System.Collections.Generic; using System.Threading; using System.Diagnostics; -namespace Nuclex.Support.Scheduling { +using Nuclex.Support.Collections; #if false +namespace Nuclex.Support.Scheduling { + /// Schedules actions for execution at a future point in time public class Scheduler : IDisposable { - /// Immediately releases all resources owned by the instance - public void Dispose() { + /// One tick is 100 ns, meaning 10000 ticks equal 1 ms + private const long TicksPerMillisecond = 10000; + +#region class TimeSourceSingleton + + /// + /// Manages the singleton instance of the scheduler's default time source + /// + private class TimeSourceSingleton { + + /// + /// Explicit static constructor to guarantee the singleton is initialized only + /// when a static member of this class is accessed. + /// + static TimeSourceSingleton() { } // Do not remove! + + /// The singleton instance of the default time source + internal static readonly ITimeSource Instance = Scheduler.CreateDefaultTimeSource(); + } + #endregion // class TimeSourceSingleton + +#region class Notification + + /// Scheduled notification + private class Notification { + + /// Initializes a new notification + /// + /// Interval in which the notification will re-executed + /// + /// + /// Time source ticks the notification is next due at + /// + /// + /// Absolute time in UTC at which the notification is due + /// + /// + /// Callback to be invoked when the notification is due + /// + public Notification( + long intervalTicks, + long nextDueTicks, + DateTime absoluteUtcTime, + Delegate callback + ) { + this.IntervalTicks = intervalTicks; + this.NextDueTicks = nextDueTicks; + this.AbsoluteUtcTime = absoluteUtcTime; + this.Callback = callback; + this.Cancelled = false; + } + + /// + /// Ticks specifying the interval in which the notification will be re-executed + /// + public long IntervalTicks; + + /// Next due time for this notification + public long NextDueTicks; + /// Absolute time in UTC at which the notification is due + /// + /// Only stored for notifications scheduled in absolute time, meaning they + /// have to be adjusted if the system date/time changes + /// + public DateTime AbsoluteUtcTime; + /// Callback that will be invoked when the notification is due + public Delegate Callback; + /// Whether the notification has been cancelled + public bool Cancelled; + + } + + #endregion // class Notification + + /// Initializes a new scheduler using the default time source + public Scheduler() : this(DefaultTimeSource) { } + + /// Initializes a new scheduler using the specified time source + /// Source source the scheduler will use + public Scheduler(ITimeSource timeSource) { + this.timeSource = timeSource; + this.notifications = new PriorityQueue(); + this.notificationWaitEvent = new AutoResetEvent(false); + + this.timerThread = new Thread(new ThreadStart(runTimerThread)); + this.timerThread.Name = "Nuclex.Support.Scheduling.Scheduler"; + this.timerThread.Priority = ThreadPriority.Highest; + this.timerThread.IsBackground = true; + this.timerThread.Start(); + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.timerThread != null) { + this.endRequested = true; + this.notificationWaitEvent.Set(); + + // Wait for the timer thread to exit. If it doesn't exit in 10 seconds (which is + // a lot of time given that it doesn't do any real work), forcefully abort + // the thread. This may risk some leaks, but it's the only thing we can do. + Trace.Assert( + this.timerThread.Join(2500), "Scheduler timer thread did not exit in time" + ); + + // Get rid of the notification wait event now that we've made sure that + // the timer thread is down. + this.notificationWaitEvent.Close(); + + // Help the GC a bit + this.notificationWaitEvent = null; + this.notifications = null; + this.timeSource = null; + + // Set to null so we don't attempt to end the thread again if Dispose() is + // called multiple times. + this.timerThread = null; + } + } + + /// Schedules a notification at the specified absolute time + /// + /// Absolute time at which the notification will occur + /// + /// + /// Callback that will be invoked when the notification is due + /// + /// A handle that can be used to cancel the notification + /// + /// The notification is scheduled for the indicated absolute time. If the system + /// enters/leaves daylight saving time or the date/time is changed (for example + /// when the system synchronizes with an NTP server), this will affect + /// the notification. So if you need to be notified after a fixed time, use + /// the NotifyIn() method instead. + /// + public object NotifyAt(DateTime notificationTime, Delegate callback) { + if(notificationTime.Kind == DateTimeKind.Unspecified) { + throw new ArgumentException( + "Notification time is neither UTC or local", "notificationTime" + ); + } + + DateTime notificationTimeUtc = notificationTime.ToUniversalTime(); + long remainingTicks = notificationTimeUtc.Ticks - DateTime.UtcNow.Ticks; + long nextDueTicks = this.timeSource.Ticks + remainingTicks; + + return scheduleNotification( + new Notification( + 0, + nextDueTicks, + notificationTimeUtc, + callback + ) + ); + } + + /// Schedules a notification after the specified time span + /// Delay after which the notification will occur + /// + /// Callback that will be invoked when the notification is due + /// + /// A handle that can be used to cancel the notification + public object NotifyIn(TimeSpan delay, Delegate callback) { + return scheduleNotification( + new Notification( + 0, + delay.Ticks, + DateTime.MinValue, + callback + ) + ); + } + + /// + /// Schedules a notification after the specified amount of milliseconds + /// + /// + /// Number of milliseconds after which the notification will occur + /// + /// + /// Callback that will be invoked when the notification is due + /// + /// A handle that can be used to cancel the notification + public object NotifyIn(int delayMilliseconds, Delegate callback) { + return scheduleNotification( + new Notification( + 0, + (long)delayMilliseconds * TicksPerMillisecond, + DateTime.MinValue, + callback + ) + ); + } + + /// + /// Schedules a recurring notification after the specified time span + /// + /// Delay after which the first notification will occur + /// Interval at which the notification will be repeated + /// + /// Callback that will be invoked when the notification is due + /// + /// A handle that can be used to cancel the notification + public object NotifyEach(TimeSpan delay, TimeSpan interval, Delegate callback) { + return scheduleNotification( + new Notification( + interval.Ticks, + delay.Ticks, + DateTime.MinValue, + callback + ) + ); + } + + /// + /// Schedules a recurring notification after the specified amount of milliseconds + /// + /// + /// Milliseconds after which the first notification will occur + /// + /// + /// Interval in milliseconds at which the notification will be repeated + /// + /// + /// Callback that will be invoked when the notification is due + /// + /// A handle that can be used to cancel the notification + public object NotifyEach( + int delayMilliseconds, int intervalMilliseconds, Delegate callback + ) { + return scheduleNotification( + new Notification( + (long)intervalMilliseconds * TicksPerMillisecond, + (long)delayMilliseconds * TicksPerMillisecond, + DateTime.MinValue, + callback + ) + ); + } + + /// Returns the default time source for the scheduler + public static ITimeSource DefaultTimeSource { + get { return TimeSourceSingleton.Instance; } + } + + /// Creates a new default time source for the scheduler + /// + /// Whether the specialized windows time source should be used + /// + /// The newly created time source + internal static ITimeSource CreateTimeSource(bool useWindowsTimeSource) { + if(useWindowsTimeSource) { + return new WindowsTimeSource(); + } else { + return new GenericTimeSource(); + } + } + + /// Creates a new default time source for the scheduler + /// The newly created time source + internal static ITimeSource CreateDefaultTimeSource() { + return CreateTimeSource(WindowsTimeSource.Available); + } + + /// Schedules a notification for processing by the timer thread + /// Notification that will be scheduled + /// The scheduled notification + private object scheduleNotification(Notification notification) { + lock(this.notifications) { + this.notifications.Enqueue(notification); + + // If this notification has become that next due notification, wake up + // the timer thread so it can adjust its sleep period. + if(ReferenceEquals(this.notifications.Peek(), notification)) { + this.notificationWaitEvent.Set(); + } + } + + return notification; + } + + /// Executes the timer thread + private void runTimerThread() { + + for(; ; ) { + + // Get the notification that is due next and wait for it. When no notifications + // are queued, wait indefinitely until we're signalled + Notification nextDueNotification = getNextDueNotification(); + if(nextDueNotification == null) { + this.notificationWaitEvent.WaitOne(); + } else { + long remainingTicks = nextDueNotification.NextDueTicks - this.timeSource.Ticks; + this.timeSource.WaitOne(this.notificationWaitEvent, remainingTicks); + } + + // Have we been woken up because the Scheduler is being disposed? + if(this.endRequested) { + break; + } + + + //if(nextDueNotification.AbsoluteUtcTime != + + } + + } + + /// Retrieves the notification that is due next + /// The notification that is due next + private Notification getNextDueNotification() { + lock(this.notifications) { + if(this.notifications.Count == 0) { + return null; + } else { + Notification nextDueNotification = this.notifications.Peek(); + while(nextDueNotification.Cancelled) { + this.notifications.Dequeue(); + nextDueNotification = this.notifications.Peek(); + } + return nextDueNotification; + } + } + } + + /// Time source used by the scheduler + private ITimeSource timeSource; + /// Thread that will wait for the next scheduled event + private Thread timerThread; + /// Notifications in the scheduler's queue + private PriorityQueue notifications; + + /// Event used by the timer thread to wait for the next notification + private AutoResetEvent notificationWaitEvent; + /// Whether the timer thread should end + private volatile bool endRequested; } -#endif - } // namespace Nuclex.Support.Scheduling + +#endif diff --git a/Source/Scheduling/WindowsTimeSource.cs b/Source/Scheduling/WindowsTimeSource.cs index 0747c87..fc446b5 100644 --- a/Source/Scheduling/WindowsTimeSource.cs +++ b/Source/Scheduling/WindowsTimeSource.cs @@ -31,7 +31,7 @@ namespace Nuclex.Support.Scheduling { /// /// Time source that makes use of additional features only available on Windows /// - public class WindowsTimeSource : DefaultTimeSource, IDisposable { + public class WindowsTimeSource : GenericTimeSource, IDisposable { /// Number of ticks (100 ns intervals) in a millisecond private const long TicksPerMillisecond = 10000; @@ -50,7 +50,7 @@ namespace Nuclex.Support.Scheduling { } } - /// Waits for an AutoResetEvent become signalled + /// Waits for an AutoResetEvent to become signalled /// WaitHandle the method will wait for /// Number of ticks to wait ///