Moved all transaction-related code from Nuclex.Support into a new project (even though it's very carefully designed, it has proven to be rarely required and .NET 4.0 Tasks make it an almost redundant implementation)
git-svn-id: file:///srv/devel/repo-conversion/nusu@218 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
parent
b7c883ea8d
commit
eea74c8228
|
@ -1,83 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the AbortedException class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class AbortedExceptionTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the exception's default constructor is working
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
AbortedException testException = new AbortedException();
|
|
||||||
|
|
||||||
string testExceptionString = testException.ToString();
|
|
||||||
Assert.IsNotNull(testExceptionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the exception correctly stores its inner exception
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestInnerException() {
|
|
||||||
Exception inner = new Exception("This is a test");
|
|
||||||
AbortedException testException = new AbortedException(
|
|
||||||
"Hello World", inner
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.AreSame(inner, testException.InnerException);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Test whether the exception can be serialized
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSerialization() {
|
|
||||||
BinaryFormatter formatter = new BinaryFormatter();
|
|
||||||
|
|
||||||
using(MemoryStream memory = new MemoryStream()) {
|
|
||||||
AbortedException exception1 = new AbortedException("Hello World");
|
|
||||||
|
|
||||||
formatter.Serialize(memory, exception1);
|
|
||||||
memory.Position = 0;
|
|
||||||
object exception2 = formatter.Deserialize(memory);
|
|
||||||
|
|
||||||
Assert.IsInstanceOf<AbortedException>(exception2);
|
|
||||||
Assert.AreEqual(exception1.Message, ((AbortedException)exception2).Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,63 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Indicates that an operation has been forcefully aborted</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This exception is the typical result of using AsyncAbort() on a running
|
|
||||||
/// background process.
|
|
||||||
/// </remarks>
|
|
||||||
#if !NO_SERIALIZATION
|
|
||||||
[Serializable]
|
|
||||||
#endif
|
|
||||||
public class AbortedException : Exception {
|
|
||||||
|
|
||||||
/// <summary>Initializes the exception</summary>
|
|
||||||
public AbortedException() { }
|
|
||||||
|
|
||||||
/// <summary>Initializes the exception with an error message</summary>
|
|
||||||
/// <param name="message">Error message describing the cause of the exception</param>
|
|
||||||
public AbortedException(string message) : base(message) { }
|
|
||||||
|
|
||||||
/// <summary>Initializes the exception as a followup exception</summary>
|
|
||||||
/// <param name="message">Error message describing the cause of the exception</param>
|
|
||||||
/// <param name="inner">Preceding exception that has caused this exception</param>
|
|
||||||
public AbortedException(string message, Exception inner) : base(message, inner) { }
|
|
||||||
|
|
||||||
#if !NO_SERIALIZATION
|
|
||||||
|
|
||||||
/// <summary>Initializes the exception from its serialized state</summary>
|
|
||||||
/// <param name="info">Contains the serialized fields of the exception</param>
|
|
||||||
/// <param name="context">Additional environmental informations</param>
|
|
||||||
protected AbortedException(
|
|
||||||
System.Runtime.Serialization.SerializationInfo info,
|
|
||||||
System.Runtime.Serialization.StreamingContext context
|
|
||||||
) :
|
|
||||||
base(info, context) { }
|
|
||||||
|
|
||||||
#endif // !NO_SERIALIZATION
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,97 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.Threading;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the generic scheduler time source</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class GenericTimeSourceTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source's default constructor is working
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
GenericTimeSource timeSource = new GenericTimeSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source can provide the current UTC time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCurrentUtcTime() {
|
|
||||||
GenericTimeSource timeSource = new GenericTimeSource();
|
|
||||||
|
|
||||||
Assert.That(
|
|
||||||
timeSource.CurrentUtcTime, Is.EqualTo(DateTime.UtcNow).Within(10).Seconds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default time source's tick property is working if
|
|
||||||
/// the Stopwatch class is used to measure time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTicksWithStopwatch() {
|
|
||||||
GenericTimeSource timeSource = new GenericTimeSource(true);
|
|
||||||
long ticks1 = timeSource.Ticks;
|
|
||||||
long ticks2 = timeSource.Ticks;
|
|
||||||
|
|
||||||
Assert.That(ticks2, Is.GreaterThanOrEqualTo(ticks1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default time source's tick property is working if
|
|
||||||
/// Environment.TickCount is used to measure time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTicksWithTickCount() {
|
|
||||||
GenericTimeSource timeSource = new GenericTimeSource(false);
|
|
||||||
long ticks1 = timeSource.Ticks;
|
|
||||||
long ticks2 = timeSource.Ticks;
|
|
||||||
|
|
||||||
Assert.That(ticks2, Is.GreaterThanOrEqualTo(ticks1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default time source's WaitOne() method works correctly
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWaitOne() {
|
|
||||||
GenericTimeSource timeSource = new GenericTimeSource();
|
|
||||||
AutoResetEvent waitEvent = new AutoResetEvent(true);
|
|
||||||
|
|
||||||
Assert.IsTrue(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks));
|
|
||||||
Assert.IsFalse(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,189 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generic time source implementation using the Stopwatch or Environment.TickCount
|
|
||||||
/// </summary>
|
|
||||||
public class GenericTimeSource : ITimeSource {
|
|
||||||
|
|
||||||
/// <summary>Number of ticks (100 ns intervals) in a millisecond</summary>
|
|
||||||
private const long TicksPerMillisecond = 10000;
|
|
||||||
|
|
||||||
/// <summary>Tolerance for the detection of a date/time adjustment</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// If the current system date/time jumps by more than this tolerance into any
|
|
||||||
/// direction, the default time source will trigger the DateTimeAdjusted event.
|
|
||||||
/// </remarks>
|
|
||||||
private const long TimeAdjustmentToleranceTicks = 75 * TicksPerMillisecond;
|
|
||||||
|
|
||||||
/// <summary>Called when the system date/time are adjusted</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// An adjustment is a change out of the ordinary, eg. when a time synchronization
|
|
||||||
/// alters the current system time, when daylight saving time takes effect or
|
|
||||||
/// when the user manually adjusts the system date/time.
|
|
||||||
/// </remarks>
|
|
||||||
public event EventHandler DateTimeAdjusted;
|
|
||||||
|
|
||||||
/// <summary>Initializes the static fields of the default time source</summary>
|
|
||||||
static GenericTimeSource() {
|
|
||||||
tickFrequency = 10000000.0;
|
|
||||||
tickFrequency /= (double)Stopwatch.Frequency;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Initializes the default time source</summary>
|
|
||||||
public GenericTimeSource() : this(Stopwatch.IsHighResolution) { }
|
|
||||||
|
|
||||||
/// <summary>Initializes the default time source</summary>
|
|
||||||
/// <param name="useStopwatch">
|
|
||||||
/// Whether to use the Stopwatch class for measuring time
|
|
||||||
/// </param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Normally it's a good idea to use the default constructor. If the Stopwatch
|
|
||||||
/// is unable to use the high-resolution timer, it will fall back to
|
|
||||||
/// DateTime.Now (as stated on MSDN). This is bad because then the tick count
|
|
||||||
/// will jump whenever the system time changes (eg. when the system synchronizes
|
|
||||||
/// its time with a time server).
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Your can safely use this constructor if you always set its arugment to 'false',
|
|
||||||
/// but then your won't profit from the high-resolution timer if one is available.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public GenericTimeSource(bool useStopwatch) {
|
|
||||||
this.useStopwatch = useStopwatch;
|
|
||||||
|
|
||||||
// Update the lastCheckedTime and lastCheckedTicks fields
|
|
||||||
checkForTimeAdjustment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits for an AutoResetEvent to become signalled</summary>
|
|
||||||
/// <param name="waitHandle">WaitHandle the method will wait for</param>
|
|
||||||
/// <param name="ticks">Number of ticks to wait</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the WaitHandle was signalled, false if the timeout was reached
|
|
||||||
/// </returns>
|
|
||||||
public virtual bool WaitOne(AutoResetEvent waitHandle, long ticks) {
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
#if WINDOWS
|
|
||||||
bool signalled = waitHandle.WaitOne(Math.Min(1000, milliseconds), false);
|
|
||||||
#else
|
|
||||||
bool signalled = waitHandle.WaitOne(Math.Min(1000, milliseconds));
|
|
||||||
#endif
|
|
||||||
// See whether the system date/time have been adjusted while we were asleep.
|
|
||||||
checkForTimeAdjustment();
|
|
||||||
|
|
||||||
// Now tell the caller whether his event was signalled
|
|
||||||
return signalled;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Current system time in UTC format</summary>
|
|
||||||
public DateTime CurrentUtcTime {
|
|
||||||
get { return DateTime.UtcNow; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>How long the time source has been running</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// There is no guarantee this value starts at zero (or anywhere near it) when
|
|
||||||
/// the time source is created. The only requirement for this value is that it
|
|
||||||
/// keeps increasing with the passing of time and that it stays unaffected
|
|
||||||
/// (eg. doesn't skip or jump back) when the system date/time are changed.
|
|
||||||
/// </remarks>
|
|
||||||
public long Ticks {
|
|
||||||
get {
|
|
||||||
|
|
||||||
// The docs say if Stopwatch.IsHighResolution is false, it will return
|
|
||||||
// DateTime.Now (actually DateTime.UtcNow). This means that the Stopwatch is
|
|
||||||
// prone to skips and jumps during DST crossings and NTP synchronizations,
|
|
||||||
// so we cannot use it in that case.
|
|
||||||
if(this.useStopwatch) {
|
|
||||||
double timestamp = (double)Stopwatch.GetTimestamp();
|
|
||||||
return (long)(timestamp * tickFrequency);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Use Environment.TickCount instead. Not as accurate, but at least
|
|
||||||
// it will not jump around when the date or time are adjusted.
|
|
||||||
return Environment.TickCount * TicksPerMillisecond;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the system time is changed</summary>
|
|
||||||
/// <param name="sender">Not used</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
protected virtual void OnDateTimeAdjusted(object sender, EventArgs arguments) {
|
|
||||||
EventHandler copy = DateTimeAdjusted;
|
|
||||||
if(copy != null) {
|
|
||||||
copy(sender, arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the system/date time have been adjusted since the last call
|
|
||||||
/// </summary>
|
|
||||||
private void checkForTimeAdjustment() {
|
|
||||||
|
|
||||||
// Grab the current date/time and timer ticks in one go
|
|
||||||
long currentDateTimeTicks = DateTime.UtcNow.Ticks;
|
|
||||||
long currentStopwatchTicks = 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 = currentStopwatchTicks - lastCheckedStopwatchTicks;
|
|
||||||
long expectedLocalTimeTicks = this.lastCheckedDateTimeTicks + ticksSinceLastCheck;
|
|
||||||
|
|
||||||
// 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(expectedLocalTimeTicks - currentDateTimeTicks);
|
|
||||||
if(deviationTicks > TimeAdjustmentToleranceTicks) {
|
|
||||||
OnDateTimeAdjusted(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember the current local date/time and timer ticks for the next run
|
|
||||||
this.lastCheckedDateTimeTicks = currentDateTimeTicks;
|
|
||||||
this.lastCheckedStopwatchTicks = currentStopwatchTicks;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Last local time we checked for a date/time adjustment</summary>
|
|
||||||
private long lastCheckedDateTimeTicks;
|
|
||||||
/// <summary>Timer ticks at which we last checked the local time</summary>
|
|
||||||
private long lastCheckedStopwatchTicks;
|
|
||||||
|
|
||||||
/// <summary>Number of ticks per Stopwatch time unit</summary>
|
|
||||||
private static double tickFrequency;
|
|
||||||
/// <summary>Whether ot use the Stopwatch class for measuring time</summary>
|
|
||||||
private bool useStopwatch;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,42 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Interface for abortable processes</summary>
|
|
||||||
public interface IAbortable {
|
|
||||||
|
|
||||||
/// <summary>Aborts the running process. Can be called from any thread.</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The receive should honor the abort request and stop whatever it is
|
|
||||||
/// doing as soon as possible. The method does not impose any requirement
|
|
||||||
/// on the timeliness of the reaction of the running process, but implementers
|
|
||||||
/// are advised to not ignore the abort request and urged to try and design
|
|
||||||
/// their code in such a way that it can be stopped in a reasonable time
|
|
||||||
/// (eg. within 1 second of the abort request).
|
|
||||||
/// </remarks>
|
|
||||||
void AsyncAbort();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,111 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
#if SCHEDULER_USE_CUSTOM_CALLBACK
|
|
||||||
/// <summary>Signature for a timed callback from the scheduler service</summary>
|
|
||||||
public delegate void SchedulerCallback();
|
|
||||||
#endif // SCHEDULER_USE_CUSTOM_CALLBACK
|
|
||||||
|
|
||||||
/// <summary>Service that allows the scheduled invocation of tasks</summary>
|
|
||||||
public interface ISchedulerService {
|
|
||||||
|
|
||||||
/// <summary>Time source being used by the scheduler</summary>
|
|
||||||
ITimeSource TimeSource { get; }
|
|
||||||
|
|
||||||
/// <summary>Schedules a notification at the specified absolute time</summary>
|
|
||||||
/// <param name="notificationTime">
|
|
||||||
/// Absolute time at which the notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 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.
|
|
||||||
/// </remarks>
|
|
||||||
object NotifyAt(DateTime notificationTime, WaitCallback callback);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a recurring notification after the specified amount of milliseconds
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delayMilliseconds">
|
|
||||||
/// Milliseconds after which the first notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="intervalMilliseconds">
|
|
||||||
/// Interval in milliseconds at which the notification will be repeated
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
object NotifyEach(
|
|
||||||
int delayMilliseconds, int intervalMilliseconds, WaitCallback callback
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a recurring notification after the specified time span
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delay">Delay after which the first notification will occur</param>
|
|
||||||
/// <param name="interval">Interval at which the notification will be repeated</param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
object NotifyEach(TimeSpan delay, TimeSpan interval, WaitCallback callback);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a notification after the specified amount of milliseconds
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delayMilliseconds">
|
|
||||||
/// Number of milliseconds after which the notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
object NotifyIn(int delayMilliseconds, WaitCallback callback);
|
|
||||||
|
|
||||||
/// <summary>Schedules a notification after the specified time span</summary>
|
|
||||||
/// <param name="delay">Delay after which the notification will occur</param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
object NotifyIn(TimeSpan delay, WaitCallback callback);
|
|
||||||
|
|
||||||
/// <summary>Cancels a scheduled notification</summary>
|
|
||||||
/// <param name="notificationHandle">
|
|
||||||
/// Handle of the notification that will be cancelled
|
|
||||||
/// </param>
|
|
||||||
void Cancel(object notificationHandle);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,67 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Provides time measurement and change notification services</summary>
|
|
||||||
public interface ITimeSource {
|
|
||||||
|
|
||||||
/// <summary>Called when the system date/time are adjusted</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// An adjustment is a change out of the ordinary, eg. when a time synchronization
|
|
||||||
/// alters the current system time, when daylight saving time takes effect or
|
|
||||||
/// when the user manually adjusts the system date/time.
|
|
||||||
/// </remarks>
|
|
||||||
event EventHandler DateTimeAdjusted;
|
|
||||||
|
|
||||||
/// <summary>Waits for an AutoResetEvent to become signalled</summary>
|
|
||||||
/// <param name="waitHandle">WaitHandle the method will wait for</param>
|
|
||||||
/// <param name="ticks">Number of ticks to wait</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the WaitHandle was signalled, false if the timeout was reached
|
|
||||||
/// or the time source thinks its time to recheck the system date/time.
|
|
||||||
/// </returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// Depending on whether the system will provide notifications when date/time
|
|
||||||
/// is adjusted, the time source will be forced to let this method block for
|
|
||||||
/// less than the indicated time before returning a timeout in order to give
|
|
||||||
/// the caller a chance to recheck the system time.
|
|
||||||
/// </remarks>
|
|
||||||
bool WaitOne(AutoResetEvent waitHandle, long ticks);
|
|
||||||
|
|
||||||
/// <summary>Current system time in UTC format</summary>
|
|
||||||
DateTime CurrentUtcTime { get; }
|
|
||||||
|
|
||||||
/// <summary>How long the time source has been running</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// There is no guarantee this value starts at zero (or anywhere near it) when
|
|
||||||
/// the time source is created. The only requirement for this value is that it
|
|
||||||
/// keeps increasing with the passing of time and that it stays unaffected
|
|
||||||
/// (eg. doesn't skip or jump back) when the system date/time are changed.
|
|
||||||
/// </remarks>
|
|
||||||
long Ticks { get; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,68 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the operation class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class OperationTest {
|
|
||||||
|
|
||||||
#region class TestOperation
|
|
||||||
|
|
||||||
/// <summary>Dummy operation used to run the unit tests</summary>
|
|
||||||
private class TestOperation : Operation {
|
|
||||||
|
|
||||||
/// <summary>Launches the background operation</summary>
|
|
||||||
public override void Start() {
|
|
||||||
// This could become a race condition of this code would be used in a fashion
|
|
||||||
// different than what current unit tests do with it
|
|
||||||
if(!base.Ended) {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestOperation
|
|
||||||
|
|
||||||
/// <summary>Tests whether operations can be started</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestOperationStarting() {
|
|
||||||
TestOperation myOperation = new TestOperation();
|
|
||||||
|
|
||||||
Assert.IsFalse(myOperation.Ended);
|
|
||||||
myOperation.Start();
|
|
||||||
Assert.IsTrue(myOperation.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,36 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using Nuclex.Support.Tracking;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Base class for observable operations running in the background</summary>
|
|
||||||
public abstract class Operation : Request {
|
|
||||||
|
|
||||||
/// <summary>Launches the background operation</summary>
|
|
||||||
public abstract void Start();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,350 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NMock;
|
|
||||||
|
|
||||||
using Nuclex.Support.Tracking;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the operation queue class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class OperationQueueTest {
|
|
||||||
|
|
||||||
#region interface IOperationQueueSubscriber
|
|
||||||
|
|
||||||
/// <summary>Interface used to test the operation queue</summary>
|
|
||||||
public interface IOperationQueueSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Called when the operations queue's progress changes</summary>
|
|
||||||
/// <param name="sender">Operation queue whose progress has changed</param>
|
|
||||||
/// <param name="arguments">Contains the new progress achieved</param>
|
|
||||||
void ProgressChanged(object sender, ProgressReportEventArgs arguments);
|
|
||||||
|
|
||||||
/// <summary>Called when the operation queue has ended</summary>
|
|
||||||
/// <param name="sender">Operation queue that as ended</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
void Ended(object sender, EventArgs arguments);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // interface IOperationQueueSubscriber
|
|
||||||
|
|
||||||
#region class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
/// <summary>Compares two ProgressUpdateEventArgsInstances for NMock validation</summary>
|
|
||||||
private class ProgressUpdateEventArgsMatcher : Matcher {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new ProgressUpdateEventArgsMatcher </summary>
|
|
||||||
/// <param name="expected">Expected progress update event arguments</param>
|
|
||||||
public ProgressUpdateEventArgsMatcher(ProgressReportEventArgs expected) {
|
|
||||||
this.expected = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actualAsObject">Actual value to compare to the expected value</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the actual value matches the expected value; otherwise false
|
|
||||||
/// </returns>
|
|
||||||
public override bool Matches(object actualAsObject) {
|
|
||||||
ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs);
|
|
||||||
if(actual == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return (actual.Progress == this.expected.Progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a string representation of the expected value</summary>
|
|
||||||
/// <param name="writer">Writer to write the string representation into</param>
|
|
||||||
public override void DescribeTo(TextWriter writer) {
|
|
||||||
writer.Write(this.expected.Progress.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Expected progress update event args value</summary>
|
|
||||||
private ProgressReportEventArgs expected;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
#region class TestOperation
|
|
||||||
|
|
||||||
/// <summary>Operation used for testing in this unit test</summary>
|
|
||||||
private class TestOperation : Operation, IProgressReporter {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Begins executing the operation. Yeah, sure :)</summary>
|
|
||||||
public override void Start() { }
|
|
||||||
|
|
||||||
/// <summary>Moves the operation into the ended state</summary>
|
|
||||||
public void SetEnded() {
|
|
||||||
SetEnded(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Moves the operation into the ended state with an exception</summary>
|
|
||||||
/// <param name="exception">Exception</param>
|
|
||||||
public void SetEnded(Exception exception) {
|
|
||||||
this.exception = exception;
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Changes the testing operation's indicated progress</summary>
|
|
||||||
/// <param name="progress">
|
|
||||||
/// New progress to be reported by the testing operation
|
|
||||||
/// </param>
|
|
||||||
public void ChangeProgress(float progress) {
|
|
||||||
OnAsyncProgressChanged(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override void ReraiseExceptions() {
|
|
||||||
if(this.exception != null)
|
|
||||||
throw this.exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="progress">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this operation about the achieved progress.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(float progress) {
|
|
||||||
OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="eventArguments">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this operation about the achieved progress.
|
|
||||||
/// Allows for classes derived from the Operation class to easily provide
|
|
||||||
/// a custom event arguments class that has been derived from the
|
|
||||||
/// operation's ProgressUpdateEventArgs class.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, eventArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Exception that has occured in the background process</summary>
|
|
||||||
private volatile Exception exception;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestOperation
|
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() {
|
|
||||||
this.mockery = new MockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Validates that the queue executes operations sequentially</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSequentialExecution() {
|
|
||||||
TestOperation operation1 = new TestOperation();
|
|
||||||
TestOperation operation2 = new TestOperation();
|
|
||||||
|
|
||||||
OperationQueue<TestOperation> testQueueOperation =
|
|
||||||
new OperationQueue<TestOperation>(
|
|
||||||
new TestOperation[] { operation1, operation2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
Mock<IOperationQueueSubscriber> mockedSubscriber = mockSubscriber(testQueueOperation);
|
|
||||||
|
|
||||||
testQueueOperation.Start();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new Matcher[] {
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(OperationQueue<TestOperation>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
operation1.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new Matcher[] {
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(OperationQueue<TestOperation>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
operation1.SetEnded();
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the queue executes operations sequentially and honors the weights
|
|
||||||
/// assigned to the individual operations
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWeightedSequentialExecution() {
|
|
||||||
TestOperation operation1 = new TestOperation();
|
|
||||||
TestOperation operation2 = new TestOperation();
|
|
||||||
|
|
||||||
OperationQueue<TestOperation> testQueueOperation =
|
|
||||||
new OperationQueue<TestOperation>(
|
|
||||||
new WeightedTransaction<TestOperation>[] {
|
|
||||||
new WeightedTransaction<TestOperation>(operation1, 0.5f),
|
|
||||||
new WeightedTransaction<TestOperation>(operation2, 2.0f)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Mock<IOperationQueueSubscriber> mockedSubscriber = mockSubscriber(testQueueOperation);
|
|
||||||
|
|
||||||
testQueueOperation.Start();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(OperationQueue<TestOperation>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.1f))
|
|
||||||
);
|
|
||||||
|
|
||||||
operation1.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(OperationQueue<TestOperation>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.2f))
|
|
||||||
);
|
|
||||||
|
|
||||||
operation1.SetEnded();
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the operation queue propagates the ended event once all contained
|
|
||||||
/// operations have completed
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndPropagation() {
|
|
||||||
TestOperation operation1 = new TestOperation();
|
|
||||||
TestOperation operation2 = new TestOperation();
|
|
||||||
|
|
||||||
OperationQueue<TestOperation> testQueueOperation =
|
|
||||||
new OperationQueue<TestOperation>(
|
|
||||||
new TestOperation[] {
|
|
||||||
operation1,
|
|
||||||
operation2
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
testQueueOperation.Start();
|
|
||||||
|
|
||||||
Assert.IsFalse(testQueueOperation.Ended);
|
|
||||||
operation1.SetEnded();
|
|
||||||
Assert.IsFalse(testQueueOperation.Ended);
|
|
||||||
operation2.SetEnded();
|
|
||||||
Assert.IsTrue(testQueueOperation.Ended);
|
|
||||||
|
|
||||||
testQueueOperation.Join();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the operation queue delivers an exception occuring in one of the
|
|
||||||
/// contained operations to the operation queue joiner
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestExceptionPropagation() {
|
|
||||||
TestOperation operation1 = new TestOperation();
|
|
||||||
TestOperation operation2 = new TestOperation();
|
|
||||||
|
|
||||||
OperationQueue<TestOperation> testQueueOperation =
|
|
||||||
new OperationQueue<TestOperation>(
|
|
||||||
new TestOperation[] {
|
|
||||||
operation1,
|
|
||||||
operation2
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
testQueueOperation.Start();
|
|
||||||
|
|
||||||
Assert.IsFalse(testQueueOperation.Ended);
|
|
||||||
operation1.SetEnded();
|
|
||||||
Assert.IsFalse(testQueueOperation.Ended);
|
|
||||||
operation2.SetEnded(new AbortedException("Hello World"));
|
|
||||||
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { testQueueOperation.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that the operation queue transparently wraps the child operations in
|
|
||||||
/// an observation wrapper that is not visible to an outside user
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTransparentWrapping() {
|
|
||||||
WeightedTransaction<TestOperation> operation1 = new WeightedTransaction<TestOperation>(
|
|
||||||
new TestOperation()
|
|
||||||
);
|
|
||||||
WeightedTransaction<TestOperation> operation2 = new WeightedTransaction<TestOperation>(
|
|
||||||
new TestOperation()
|
|
||||||
);
|
|
||||||
|
|
||||||
OperationQueue<TestOperation> testQueueOperation =
|
|
||||||
new OperationQueue<TestOperation>(
|
|
||||||
new WeightedTransaction<TestOperation>[] {
|
|
||||||
operation1,
|
|
||||||
operation2
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Order is important due to sequential execution!
|
|
||||||
Assert.AreSame(operation1, testQueueOperation.Children[0]);
|
|
||||||
Assert.AreSame(operation2, testQueueOperation.Children[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mocks a subscriber for the events of an operation</summary>
|
|
||||||
/// <param name="operation">Operation to mock an event subscriber for</param>
|
|
||||||
/// <returns>The mocked event subscriber</returns>
|
|
||||||
private Mock<IOperationQueueSubscriber> mockSubscriber(Operation operation) {
|
|
||||||
Mock<IOperationQueueSubscriber> mockedSubscriber =
|
|
||||||
this.mockery.CreateMock<IOperationQueueSubscriber>();
|
|
||||||
|
|
||||||
operation.AsyncEnded += new EventHandler(mockedSubscriber.MockObject.Ended);
|
|
||||||
(operation as IProgressReporter).AsyncProgressChanged +=
|
|
||||||
new EventHandler<ProgressReportEventArgs>(mockedSubscriber.MockObject.ProgressChanged);
|
|
||||||
|
|
||||||
return mockedSubscriber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mock object factory</summary>
|
|
||||||
private MockFactory mockery;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,242 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using Nuclex.Support.Tracking;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Operation that sequentially executes a series of operations</summary>
|
|
||||||
/// <typeparam name="OperationType">
|
|
||||||
/// Type of the child operations the QueueOperation will contain
|
|
||||||
/// </typeparam>
|
|
||||||
public class OperationQueue<OperationType> : Operation, IProgressReporter
|
|
||||||
where OperationType : Operation {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new queue operation with default weights</summary>
|
|
||||||
/// <param name="childs">Child operations to execute in this operation</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// All child operations will have a default weight of 1.0
|
|
||||||
/// </remarks>
|
|
||||||
public OperationQueue(IEnumerable<OperationType> childs) : this() {
|
|
||||||
|
|
||||||
// Construct a WeightedTransaction with the default weight for each
|
|
||||||
// transaction and wrap it in an ObservedTransaction
|
|
||||||
foreach(OperationType operation in childs)
|
|
||||||
this.children.Add(new WeightedTransaction<OperationType>(operation));
|
|
||||||
|
|
||||||
// Since all transactions have a weight of 1.0, the total weight is
|
|
||||||
// equal to the number of transactions in our list
|
|
||||||
this.totalWeight = (float)this.children.Count;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Initializes a new queue operation with custom weights</summary>
|
|
||||||
/// <param name="childs">Child operations to execute in this operation</param>
|
|
||||||
public OperationQueue(IEnumerable<WeightedTransaction<OperationType>> childs) : this() {
|
|
||||||
|
|
||||||
// Construct an ObservedTransactionn around each of the WeightedTransactions
|
|
||||||
foreach(WeightedTransaction<OperationType> operation in childs) {
|
|
||||||
this.children.Add(operation);
|
|
||||||
|
|
||||||
// Sum up the total weight
|
|
||||||
this.totalWeight += operation.Weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Initializes a new queue operation</summary>
|
|
||||||
private OperationQueue() {
|
|
||||||
this.asyncOperationEndedDelegate = new EventHandler(asyncOperationEnded);
|
|
||||||
this.asyncOperationProgressChangedDelegate = new EventHandler<ProgressReportEventArgs>(
|
|
||||||
asyncOperationProgressChanged
|
|
||||||
);
|
|
||||||
|
|
||||||
this.children = new List<WeightedTransaction<OperationType>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Provides access to the child operations of this queue</summary>
|
|
||||||
public IList<WeightedTransaction<OperationType>> Children {
|
|
||||||
get { return this.children; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Launches the background operation</summary>
|
|
||||||
public override void Start() {
|
|
||||||
startCurrentOperation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override void ReraiseExceptions() {
|
|
||||||
if(this.exception != null)
|
|
||||||
throw this.exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="progress">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(float progress) {
|
|
||||||
OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="eventArguments">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// Allows for classes derived from the transaction class to easily provide
|
|
||||||
/// a custom event arguments class that has been derived from the
|
|
||||||
/// transaction's ProgressUpdateEventArgs class.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, eventArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Prepares the current operation and calls its Start() method</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This subscribes the queue to the events of to the current operation
|
|
||||||
/// and launches the operation by calling its Start() method.
|
|
||||||
/// </remarks>
|
|
||||||
private void startCurrentOperation() {
|
|
||||||
do {
|
|
||||||
Thread.MemoryBarrier();
|
|
||||||
OperationType operation = this.children[this.currentOperationIndex].Transaction;
|
|
||||||
|
|
||||||
operation.AsyncEnded += this.asyncOperationEndedDelegate;
|
|
||||||
|
|
||||||
IProgressReporter progressReporter = operation as IProgressReporter;
|
|
||||||
if(progressReporter != null)
|
|
||||||
progressReporter.AsyncProgressChanged += this.asyncOperationProgressChangedDelegate;
|
|
||||||
|
|
||||||
Interlocked.Exchange(ref this.completionStatus, 1);
|
|
||||||
operation.Start();
|
|
||||||
} while(Interlocked.Decrement(ref this.completionStatus) > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Disconnects from the current operation and calls its End() method</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This unsubscribes the queue from the current operation's events, calls End()
|
|
||||||
/// on the operation and, if the operation didn't have an exception to report,
|
|
||||||
/// counts up the accumulated progress of th e queue.
|
|
||||||
/// </remarks>
|
|
||||||
private void endCurrentOperation() {
|
|
||||||
Thread.MemoryBarrier();
|
|
||||||
OperationType operation = this.children[this.currentOperationIndex].Transaction;
|
|
||||||
|
|
||||||
// Disconnect from the operation's events
|
|
||||||
operation.AsyncEnded -= this.asyncOperationEndedDelegate;
|
|
||||||
|
|
||||||
IProgressReporter progressReporter = operation as IProgressReporter;
|
|
||||||
if(progressReporter != null)
|
|
||||||
progressReporter.AsyncProgressChanged -= this.asyncOperationProgressChangedDelegate;
|
|
||||||
|
|
||||||
try {
|
|
||||||
operation.Join();
|
|
||||||
|
|
||||||
// Add the operations weight to the total amount of completed weight in the queue
|
|
||||||
this.completedWeight += this.children[this.currentOperationIndex].Weight;
|
|
||||||
|
|
||||||
// Trigger another progress update
|
|
||||||
OnAsyncProgressChanged(this.completedWeight / this.totalWeight);
|
|
||||||
}
|
|
||||||
catch(Exception exception) {
|
|
||||||
this.exception = exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the current executing operation ends</summary>
|
|
||||||
/// <param name="sender">Operation that ended</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
private void asyncOperationEnded(object sender, EventArgs arguments) {
|
|
||||||
|
|
||||||
// Unsubscribe from the current operation's events and update the
|
|
||||||
// accumulating progress counter
|
|
||||||
endCurrentOperation();
|
|
||||||
|
|
||||||
// Only jump to the next operation if no exception occured
|
|
||||||
if(this.exception == null) {
|
|
||||||
int newIndex = Interlocked.Increment(ref this.currentOperationIndex);
|
|
||||||
Thread.MemoryBarrier();
|
|
||||||
|
|
||||||
// Execute the next operation unless we reached the end of the queue
|
|
||||||
if(newIndex < this.children.Count) {
|
|
||||||
if(Interlocked.Increment(ref this.completionStatus) == 1) {
|
|
||||||
startCurrentOperation();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either an exception has occured or we reached the end of the operation
|
|
||||||
// queue. In any case, we need to report that the operation is over.
|
|
||||||
OnAsyncEnded();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when currently executing operation makes progress</summary>
|
|
||||||
/// <param name="sender">Operation that has achieved progress</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
private void asyncOperationProgressChanged(
|
|
||||||
object sender, ProgressReportEventArgs arguments
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Determine the completed weight of the currently executing operation
|
|
||||||
float operationWeight = this.children[this.currentOperationIndex].Weight;
|
|
||||||
float operationCompletedWeight = arguments.Progress * operationWeight;
|
|
||||||
|
|
||||||
// Build the total normalized amount of progress for the queue
|
|
||||||
float progress = (this.completedWeight + operationCompletedWeight) / this.totalWeight;
|
|
||||||
|
|
||||||
// Done, we can send the actual progress to any event subscribers
|
|
||||||
OnAsyncProgressChanged(progress);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Delegate to the asyncOperationEnded() method</summary>
|
|
||||||
private EventHandler asyncOperationEndedDelegate;
|
|
||||||
/// <summary>Delegate to the asyncOperationProgressUpdated() method</summary>
|
|
||||||
private EventHandler<ProgressReportEventArgs> asyncOperationProgressChangedDelegate;
|
|
||||||
/// <summary>Operations being managed in the queue</summary>
|
|
||||||
private List<WeightedTransaction<OperationType>> children;
|
|
||||||
/// <summary>Summed weight of all operations in the queue</summary>
|
|
||||||
private float totalWeight;
|
|
||||||
/// <summary>Accumulated weight of the operations already completed</summary>
|
|
||||||
private float completedWeight;
|
|
||||||
/// <summary>Index of the operation currently executing</summary>
|
|
||||||
private int currentOperationIndex;
|
|
||||||
/// <summary>Used to detect when an operation completes synchronously</summary>
|
|
||||||
private int completionStatus;
|
|
||||||
/// <summary>Exception that has occured in the background process</summary>
|
|
||||||
private volatile Exception exception;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,489 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the scheduler</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class SchedulerTest {
|
|
||||||
|
|
||||||
#region class MockTimeSource
|
|
||||||
|
|
||||||
/// <summary>Mocked time source</summary>
|
|
||||||
private class MockTimeSource : ITimeSource {
|
|
||||||
|
|
||||||
/// <summary>Called when the system date/time are adjusted</summary>
|
|
||||||
public event EventHandler DateTimeAdjusted;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new mocked time source</summary>
|
|
||||||
/// <param name="utcStartTime">Start time in UTC format</param>
|
|
||||||
public MockTimeSource(DateTime utcStartTime) {
|
|
||||||
this.currentTime = utcStartTime;
|
|
||||||
this.currentTicks = 1000000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits for an AutoResetEvent to become signalled</summary>
|
|
||||||
/// <param name="waitHandle">WaitHandle the method will wait for</param>
|
|
||||||
/// <param name="ticks">Number of ticks to wait</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the WaitHandle was signalled, false if the timeout was reached
|
|
||||||
/// or the time source thinks its time to recheck the system date/time.
|
|
||||||
/// </returns>
|
|
||||||
public bool WaitOne(AutoResetEvent waitHandle, long ticks) {
|
|
||||||
long currentTicks;
|
|
||||||
long eventDueTicks;
|
|
||||||
lock(this) {
|
|
||||||
this.autoResetEvent = waitHandle;
|
|
||||||
this.eventDueTicks += ticks;
|
|
||||||
|
|
||||||
currentTicks = this.currentTicks;
|
|
||||||
eventDueTicks = this.eventDueTicks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we need to wait, use the wait handle. We do not use the wait handle's
|
|
||||||
// return value (or even its timeout) because we might trigger it ourselves
|
|
||||||
// to simulate the passing of time.
|
|
||||||
if(eventDueTicks > 0) {
|
|
||||||
this.autoResetEvent = waitHandle;
|
|
||||||
waitHandle.WaitOne();
|
|
||||||
this.autoResetEvent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not use the cached values here -- we might have used the WaitHandle and
|
|
||||||
// the simulation time could have been advanced while we were waiting.
|
|
||||||
lock(this) {
|
|
||||||
return (this.eventDueTicks > 0); // True = signalled, false = timed out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Current system time in UTC format</summary>
|
|
||||||
public DateTime CurrentUtcTime {
|
|
||||||
get { lock(this) { return this.currentTime; } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>How long the time source has been running</summary>
|
|
||||||
public long Ticks {
|
|
||||||
get {
|
|
||||||
lock(this) {
|
|
||||||
this.eventDueTicks = 0;
|
|
||||||
return this.currentTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Advances the time of the time source</summary>
|
|
||||||
/// <param name="timeSpan">
|
|
||||||
/// Time span by which to advance the time source's time
|
|
||||||
/// </param>
|
|
||||||
public void AdvanceTime(TimeSpan timeSpan) {
|
|
||||||
lock(this) {
|
|
||||||
this.currentTicks += timeSpan.Ticks;
|
|
||||||
this.currentTime += timeSpan;
|
|
||||||
|
|
||||||
// Problem: The Scheduler has just calculated the remaining ticks until
|
|
||||||
// notification occurs. Next, another thread advances simulation time
|
|
||||||
// and then the scheduler calls this. It will wait, even though
|
|
||||||
// the simultion time has progressed.
|
|
||||||
// To compensate this, we track the remaining time until the event is due
|
|
||||||
// and allow for a negative time budget if AdvanceTime() is called after
|
|
||||||
// the scheduler has just queried the current tick count.
|
|
||||||
this.eventDueTicks -= timeSpan.Ticks;
|
|
||||||
|
|
||||||
if(this.eventDueTicks <= 0) {
|
|
||||||
AutoResetEvent copy = this.autoResetEvent;
|
|
||||||
if(copy != null) {
|
|
||||||
copy.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Manually triggers the date time adjusted event</summary>
|
|
||||||
/// <param name="newUtcTime">New simulation time to jump to</param>
|
|
||||||
public void AdjustTime(DateTime newUtcTime) {
|
|
||||||
lock(this) {
|
|
||||||
this.currentTime = newUtcTime;
|
|
||||||
}
|
|
||||||
EventHandler copy = DateTimeAdjusted;
|
|
||||||
if(copy != null) {
|
|
||||||
copy(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Auto reset event the time source is currently waiting on</summary>
|
|
||||||
private volatile AutoResetEvent autoResetEvent;
|
|
||||||
/// <summary>Ticks at which the auto reset event will be due</summary>
|
|
||||||
private long eventDueTicks;
|
|
||||||
/// <summary>Current time source tick counter</summary>
|
|
||||||
private long currentTicks;
|
|
||||||
/// <summary>Current system time and date</summary>
|
|
||||||
private DateTime currentTime;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class MockTimeSource
|
|
||||||
|
|
||||||
#region class TestSubscriber
|
|
||||||
|
|
||||||
/// <summary>Subscriber used to test the scheduler notifications</summary>
|
|
||||||
private class TestSubscriber : IDisposable {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new test subscriber</summary>
|
|
||||||
public TestSubscriber() {
|
|
||||||
this.waitHandle = new AutoResetEvent(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
|
||||||
public void Dispose() {
|
|
||||||
if(this.waitHandle != null) {
|
|
||||||
this.waitHandle.Close();
|
|
||||||
this.waitHandle = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Callback method that can be subscribed to the scheduler</summary>
|
|
||||||
/// <param name="state">Not used</param>
|
|
||||||
public void Callback(object state) {
|
|
||||||
Interlocked.Increment(ref this.callbackCount);
|
|
||||||
this.waitHandle.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Blocks ther caller until the callback is invoked</summary>
|
|
||||||
/// <param name="milliseconds">
|
|
||||||
/// Maximum number of milliseconds to wait for the callback
|
|
||||||
/// </param>
|
|
||||||
/// <returns>True if the callback was invoked, false if the call timed out</returns>
|
|
||||||
public bool WaitForCallback(int milliseconds) {
|
|
||||||
return this.waitHandle.WaitOne(milliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Number of times the callback has been invoked</summary>
|
|
||||||
public int CallbackCount {
|
|
||||||
get { return Thread.VolatileRead(ref this.callbackCount); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Callback invocation count</summary>
|
|
||||||
private int callbackCount;
|
|
||||||
/// <summary>WaitHandle used to wait for the callback</summary>
|
|
||||||
private AutoResetEvent waitHandle;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestSubscriber
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Test whether the Scheduler can explicitely create a windows time source
|
|
||||||
/// </summary>
|
|
||||||
[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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Test whether the Scheduler can explicitely create a generic time source
|
|
||||||
/// </summary>
|
|
||||||
[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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Test whether the Scheduler can automatically choose the right time source
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCreateDefaultTimeSource() {
|
|
||||||
ITimeSource timeSource = Scheduler.CreateDefaultTimeSource();
|
|
||||||
try {
|
|
||||||
Assert.IsNotNull(timeSource);
|
|
||||||
} finally {
|
|
||||||
IDisposable disposableTimeSource = timeSource as IDisposable;
|
|
||||||
if(disposableTimeSource != null) {
|
|
||||||
disposableTimeSource.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default constructor of the scheduler is working
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
using(Scheduler scheduler = new Scheduler()) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default constructor of the scheduler is working
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestThrowOnNotifyAtWithUnspecifiedDateTimeKind() {
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler()) {
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
delegate() {
|
|
||||||
scheduler.NotifyAt(new DateTime(2000, 1, 1), subscriber.Callback);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the NotifyAt() method invokes the callback at the right time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestNotifyAt() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromDays(1));
|
|
||||||
|
|
||||||
Assert.IsTrue(subscriber.WaitForCallback(1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that a notification at an absolute time is processed correctly
|
|
||||||
/// if a time synchronization occurs during the wait.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestNotifyAtWithDateTimeAdjustment() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyAt(makeUtc(new DateTime(2010, 1, 2)), subscriber.Callback);
|
|
||||||
|
|
||||||
// Let 12 hours pass, after that, we simulate a time synchronization
|
|
||||||
// that puts the system 12 hours ahead of the original time.
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(12));
|
|
||||||
mockTimeSource.AdjustTime(new DateTime(2010, 1, 2));
|
|
||||||
|
|
||||||
Assert.IsTrue(subscriber.WaitForCallback(1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Tests whether the scheduler's Cancel() method is working</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCancelNotification() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber1 = new TestSubscriber()) {
|
|
||||||
using(TestSubscriber subscriber2 = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
object handle = scheduler.NotifyIn(
|
|
||||||
TimeSpan.FromHours(24), subscriber1.Callback
|
|
||||||
);
|
|
||||||
scheduler.NotifyIn(TimeSpan.FromHours(36), subscriber2.Callback);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(12));
|
|
||||||
scheduler.Cancel(handle);
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(24));
|
|
||||||
|
|
||||||
// Wait for the second subscriber to be notified. This is still a race
|
|
||||||
// condition (there's no guarantee the thread pool will run tasks in
|
|
||||||
// the order they were added), but it's the best we can do without
|
|
||||||
// relying on an ugly Thread.Sleep() in this test.
|
|
||||||
Assert.IsTrue(subscriber2.WaitForCallback(1000));
|
|
||||||
Assert.AreEqual(0, subscriber1.CallbackCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests the scheduler with two notifications that are scheduled in inverse
|
|
||||||
/// order of their due time.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestInverseOrderNotification() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber1 = new TestSubscriber()) {
|
|
||||||
using(TestSubscriber subscriber2 = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyIn(TimeSpan.FromHours(24), subscriber1.Callback);
|
|
||||||
scheduler.NotifyIn(TimeSpan.FromHours(12), subscriber2.Callback);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(18));
|
|
||||||
|
|
||||||
Assert.IsTrue(subscriber2.WaitForCallback(1000));
|
|
||||||
Assert.AreEqual(0, subscriber1.CallbackCount);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(18));
|
|
||||||
|
|
||||||
Assert.IsTrue(subscriber1.WaitForCallback(1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests the scheduler with two notifications that are scheduled to
|
|
||||||
/// occur at the exact same time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTwoNotificationsAtSameTime() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber1 = new TestSubscriber()) {
|
|
||||||
using(TestSubscriber subscriber2 = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyIn(60000, subscriber1.Callback);
|
|
||||||
scheduler.NotifyIn(60000, subscriber2.Callback);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(60000));
|
|
||||||
|
|
||||||
Assert.IsTrue(subscriber1.WaitForCallback(1000));
|
|
||||||
Assert.IsTrue(subscriber2.WaitForCallback(1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the scheduler's NotifyEach() method is working correctly
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestNotifyEachWithMilliseconds() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyEach(1000, 1000, subscriber.Callback);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromMilliseconds(4000));
|
|
||||||
|
|
||||||
// Wait for 4 invocations of the callback. We might not catch each trigger
|
|
||||||
// of the AutoResetEvent, but we know that it will be 4 at most.
|
|
||||||
for(int invocation = 0; invocation < 4; ++invocation) {
|
|
||||||
Assert.IsTrue(subscriber.WaitForCallback(1000));
|
|
||||||
|
|
||||||
if(subscriber.CallbackCount == 4) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the scheduler's NotifyEach() method is working correctly
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestNotifyEachWithTimespan() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.NotifyEach(
|
|
||||||
TimeSpan.FromHours(12), TimeSpan.FromHours(1), subscriber.Callback
|
|
||||||
);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(14));
|
|
||||||
|
|
||||||
// Wait for 3 invocations of the callback. We might not catch each trigger
|
|
||||||
// of the AutoResetEvent, but we know that it will be 3 at most.
|
|
||||||
for(int invocation = 0; invocation < 3; ++invocation) {
|
|
||||||
Assert.IsTrue(subscriber.WaitForCallback(1000));
|
|
||||||
|
|
||||||
if(subscriber.CallbackCount == 3) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reproduction case for a bug that occurred when the final notification in
|
|
||||||
/// the scheduler was cancelled (call to PriorityQueue.Peek() on empty queue)
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCancelFinalNotification() {
|
|
||||||
MockTimeSource mockTimeSource = new MockTimeSource(new DateTime(2010, 1, 1));
|
|
||||||
using(TestSubscriber subscriber = new TestSubscriber()) {
|
|
||||||
using(Scheduler scheduler = new Scheduler(mockTimeSource)) {
|
|
||||||
scheduler.Cancel(
|
|
||||||
scheduler.NotifyIn(TimeSpan.FromHours(12), subscriber.Callback)
|
|
||||||
);
|
|
||||||
|
|
||||||
mockTimeSource.AdvanceTime(TimeSpan.FromHours(14));
|
|
||||||
Thread.Sleep(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Unit testing caused this exception
|
|
||||||
//
|
|
||||||
// Nuclex.Support.Scheduling.SchedulerTest.TestThrowOnNotifyAtWithUnspecifiedDateTimeKind :
|
|
||||||
// System.NullReferenceException: Der Objektverweis wurde nicht auf
|
|
||||||
// eine Objektinstanz festgelegt.
|
|
||||||
// bei Nuclex.Support.Scheduling.SchedulerTest.TestSubscriber.Callback(Object state)
|
|
||||||
// in D:\Devel\framework\Nuclex.Support\Source\Scheduling\Scheduler.Test.cs:Zeile 177.
|
|
||||||
// bei System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
|
|
||||||
// bei System.Threading.ExecutionContext.Run(
|
|
||||||
// ExecutionContext executionContext, ContextCallback callback, Object state
|
|
||||||
// )
|
|
||||||
// bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(
|
|
||||||
// _ThreadPoolWaitCallback tpWaitCallBack
|
|
||||||
// )
|
|
||||||
// bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)
|
|
||||||
|
|
||||||
/// <summary>Returns the provided date/time value as a utc time value</summary>
|
|
||||||
/// <param name="dateTime">Date/time value that will be returned as UTC</param>
|
|
||||||
/// <returns>The provided date/time value as UTC</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// This doesn't convert the time, it just returns the exact same date and time
|
|
||||||
/// but tags it as UTC by setting the DateTimeKind to UTC.
|
|
||||||
/// </remarks>
|
|
||||||
private static DateTime makeUtc(DateTime dateTime) {
|
|
||||||
return new DateTime(dateTime.Ticks, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,79 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
using Nuclex.Support.Collections;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Schedules actions for execution at a future point in time</summary>
|
|
||||||
partial class Scheduler {
|
|
||||||
|
|
||||||
#region class TimeSourceSingleton
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages the singleton instance of the scheduler's default time source
|
|
||||||
/// </summary>
|
|
||||||
private class TimeSourceSingleton {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicit static constructor to guarantee the singleton is initialized only
|
|
||||||
/// when a static member of this class is accessed.
|
|
||||||
/// </summary>
|
|
||||||
static TimeSourceSingleton() { } // Do not remove!
|
|
||||||
|
|
||||||
/// <summary>The singleton instance of the default time source</summary>
|
|
||||||
internal static readonly ITimeSource Instance = Scheduler.CreateDefaultTimeSource();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TimeSourceSingleton
|
|
||||||
|
|
||||||
/// <summary>Returns the default time source for the scheduler</summary>
|
|
||||||
public static ITimeSource DefaultTimeSource {
|
|
||||||
get { return TimeSourceSingleton.Instance; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a new default time source for the scheduler</summary>
|
|
||||||
/// <param name="useWindowsTimeSource">
|
|
||||||
/// Whether the specialized windows time source should be used
|
|
||||||
/// </param>
|
|
||||||
/// <returns>The newly created time source</returns>
|
|
||||||
internal static ITimeSource CreateTimeSource(bool useWindowsTimeSource) {
|
|
||||||
if(useWindowsTimeSource) {
|
|
||||||
return new WindowsTimeSource();
|
|
||||||
} else {
|
|
||||||
return new GenericTimeSource();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a new default time source for the scheduler</summary>
|
|
||||||
/// <returns>The newly created time source</returns>
|
|
||||||
internal static ITimeSource CreateDefaultTimeSource() {
|
|
||||||
return CreateTimeSource(WindowsTimeSource.Available);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,460 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
using Nuclex.Support.Collections;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Schedules actions for execution at a future point in time</summary>
|
|
||||||
public partial class Scheduler : ISchedulerService, IDisposable {
|
|
||||||
|
|
||||||
/// <summary>One tick is 100 ns, meaning 10000 ticks equal 1 ms</summary>
|
|
||||||
private const long TicksPerMillisecond = 10000;
|
|
||||||
|
|
||||||
#region class Notification
|
|
||||||
|
|
||||||
/// <summary>Scheduled notification</summary>
|
|
||||||
private class Notification {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new notification</summary>
|
|
||||||
/// <param name="intervalTicks">
|
|
||||||
/// Interval in which the notification will re-executed
|
|
||||||
/// </param>
|
|
||||||
/// <param name="nextDueTicks">
|
|
||||||
/// Time source ticks the notification is next due at
|
|
||||||
/// </param>
|
|
||||||
/// <param name="absoluteUtcTime">
|
|
||||||
/// Absolute time in UTC at which the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback to be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
public Notification(
|
|
||||||
long intervalTicks,
|
|
||||||
long nextDueTicks,
|
|
||||||
DateTime absoluteUtcTime,
|
|
||||||
WaitCallback callback
|
|
||||||
) {
|
|
||||||
this.IntervalTicks = intervalTicks;
|
|
||||||
this.NextDueTicks = nextDueTicks;
|
|
||||||
this.AbsoluteUtcTime = absoluteUtcTime;
|
|
||||||
this.Callback = callback;
|
|
||||||
this.Cancelled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ticks specifying the interval in which the notification will be re-executed
|
|
||||||
/// </summary>
|
|
||||||
public long IntervalTicks;
|
|
||||||
|
|
||||||
/// <summary>Next due time for this notification</summary>
|
|
||||||
public long NextDueTicks;
|
|
||||||
/// <summary>Absolute time in UTC at which the notification is due</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Only stored for notifications scheduled in absolute time, meaning they
|
|
||||||
/// have to be adjusted if the system date/time changes
|
|
||||||
/// </remarks>
|
|
||||||
public DateTime AbsoluteUtcTime;
|
|
||||||
/// <summary>Callback that will be invoked when the notification is due</summary>
|
|
||||||
public WaitCallback Callback;
|
|
||||||
/// <summary>Whether the notification has been cancelled</summary>
|
|
||||||
public bool Cancelled;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class Notification
|
|
||||||
|
|
||||||
#region class NotificationComparer
|
|
||||||
|
|
||||||
/// <summary>Compares two notifications to each other</summary>
|
|
||||||
private class NotificationComparer : IComparer<Notification> {
|
|
||||||
|
|
||||||
/// <summary>The default instance of the notification comparer</summary>
|
|
||||||
public static readonly NotificationComparer Default = new NotificationComparer();
|
|
||||||
|
|
||||||
/// <summary>Compares two notifications to each other based on their time</summary>
|
|
||||||
/// <param name="left">Notification that will be compared on the left side</param>
|
|
||||||
/// <param name="right">Notification that will be comapred on the right side</param>
|
|
||||||
/// <returns>The relation of the two notification's times to each other</returns>
|
|
||||||
public int Compare(Notification left, Notification right) {
|
|
||||||
if(left.NextDueTicks > right.NextDueTicks) {
|
|
||||||
return -1;
|
|
||||||
} else if(left.NextDueTicks < right.NextDueTicks) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class NotificationComparer
|
|
||||||
|
|
||||||
/// <summary>Initializes a new scheduler using the default time source</summary>
|
|
||||||
public Scheduler() : this(DefaultTimeSource) { }
|
|
||||||
|
|
||||||
/// <summary>Initializes a new scheduler using the specified time source</summary>
|
|
||||||
/// <param name="timeSource">Time source the scheduler will use</param>
|
|
||||||
public Scheduler(ITimeSource timeSource) {
|
|
||||||
this.dateTimeAdjustedDelegate = new EventHandler(dateTimeAdjusted);
|
|
||||||
|
|
||||||
this.timeSource = timeSource;
|
|
||||||
this.timeSource.DateTimeAdjusted += this.dateTimeAdjustedDelegate;
|
|
||||||
|
|
||||||
this.notifications = new PriorityQueue<Notification>(NotificationComparer.Default);
|
|
||||||
this.notificationWaitEvent = new AutoResetEvent(false);
|
|
||||||
|
|
||||||
this.timerThread = new Thread(new ThreadStart(runTimerThread));
|
|
||||||
this.timerThread.Name = "Nuclex.Support.Scheduling.Scheduler";
|
|
||||||
#if WINDOWS
|
|
||||||
this.timerThread.Priority = ThreadPriority.Highest;
|
|
||||||
#endif
|
|
||||||
this.timerThread.IsBackground = true;
|
|
||||||
this.timerThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
|
||||||
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.
|
|
||||||
bool success = this.timerThread.Join(2500);
|
|
||||||
#if WINDOWS
|
|
||||||
Trace.Assert(success, "Scheduler timer thread did not exit in time");
|
|
||||||
#endif
|
|
||||||
// Unsubscribe from the time source to avoid surprise events during or
|
|
||||||
// after shutdown
|
|
||||||
if(this.timeSource != null) {
|
|
||||||
this.timeSource.DateTimeAdjusted -= this.dateTimeAdjustedDelegate;
|
|
||||||
this.timeSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Set to null so we don't attempt to end the thread again if Dispose() is
|
|
||||||
// called multiple times.
|
|
||||||
this.timerThread = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Time source being used by the scheduler</summary>
|
|
||||||
public ITimeSource TimeSource {
|
|
||||||
get { return this.timeSource; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Schedules a notification at the specified absolute time</summary>
|
|
||||||
/// <param name="notificationTime">
|
|
||||||
/// Absolute time at which the notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// 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.
|
|
||||||
/// </remarks>
|
|
||||||
public object NotifyAt(DateTime notificationTime, WaitCallback callback) {
|
|
||||||
if(notificationTime.Kind == DateTimeKind.Unspecified) {
|
|
||||||
throw new ArgumentException(
|
|
||||||
"Notification time is neither UTC or local", "notificationTime"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime notificationTimeUtc = notificationTime.ToUniversalTime();
|
|
||||||
DateTime now = this.timeSource.CurrentUtcTime;
|
|
||||||
|
|
||||||
long remainingTicks = notificationTimeUtc.Ticks - now.Ticks;
|
|
||||||
long nextDueTicks = this.timeSource.Ticks + remainingTicks;
|
|
||||||
|
|
||||||
return scheduleNotification(
|
|
||||||
new Notification(
|
|
||||||
0,
|
|
||||||
nextDueTicks,
|
|
||||||
notificationTimeUtc,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Schedules a notification after the specified time span</summary>
|
|
||||||
/// <param name="delay">Delay after which the notification will occur</param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
public object NotifyIn(TimeSpan delay, WaitCallback callback) {
|
|
||||||
return scheduleNotification(
|
|
||||||
new Notification(
|
|
||||||
0,
|
|
||||||
this.timeSource.Ticks + delay.Ticks,
|
|
||||||
DateTime.MinValue,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a notification after the specified amount of milliseconds
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delayMilliseconds">
|
|
||||||
/// Number of milliseconds after which the notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
public object NotifyIn(int delayMilliseconds, WaitCallback callback) {
|
|
||||||
return scheduleNotification(
|
|
||||||
new Notification(
|
|
||||||
0,
|
|
||||||
this.timeSource.Ticks + ((long)delayMilliseconds * TicksPerMillisecond),
|
|
||||||
DateTime.MinValue,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a recurring notification after the specified time span
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delay">Delay after which the first notification will occur</param>
|
|
||||||
/// <param name="interval">Interval at which the notification will be repeated</param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
public object NotifyEach(TimeSpan delay, TimeSpan interval, WaitCallback callback) {
|
|
||||||
return scheduleNotification(
|
|
||||||
new Notification(
|
|
||||||
interval.Ticks,
|
|
||||||
this.timeSource.Ticks + delay.Ticks,
|
|
||||||
DateTime.MinValue,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schedules a recurring notification after the specified amount of milliseconds
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delayMilliseconds">
|
|
||||||
/// Milliseconds after which the first notification will occur
|
|
||||||
/// </param>
|
|
||||||
/// <param name="intervalMilliseconds">
|
|
||||||
/// Interval in milliseconds at which the notification will be repeated
|
|
||||||
/// </param>
|
|
||||||
/// <param name="callback">
|
|
||||||
/// Callback that will be invoked when the notification is due
|
|
||||||
/// </param>
|
|
||||||
/// <returns>A handle that can be used to cancel the notification</returns>
|
|
||||||
public object NotifyEach(
|
|
||||||
int delayMilliseconds, int intervalMilliseconds, WaitCallback callback
|
|
||||||
) {
|
|
||||||
return scheduleNotification(
|
|
||||||
new Notification(
|
|
||||||
(long)intervalMilliseconds * TicksPerMillisecond,
|
|
||||||
this.timeSource.Ticks + ((long)delayMilliseconds * TicksPerMillisecond),
|
|
||||||
DateTime.MinValue,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Cancels a scheduled notification</summary>
|
|
||||||
/// <param name="notificationHandle">
|
|
||||||
/// Handle of the notification that will be cancelled
|
|
||||||
/// </param>
|
|
||||||
public void Cancel(object notificationHandle) {
|
|
||||||
Notification notification = notificationHandle as Notification;
|
|
||||||
if(notification != null) {
|
|
||||||
notification.Cancelled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the system date/time have been adjusted</summary>
|
|
||||||
/// <param name="sender">Time source which detected the adjustment</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
private void dateTimeAdjusted(object sender, EventArgs arguments) {
|
|
||||||
lock(this.timerThread) {
|
|
||||||
long currentTicks = this.timeSource.Ticks;
|
|
||||||
DateTime currentTime = this.timeSource.CurrentUtcTime;
|
|
||||||
|
|
||||||
PriorityQueue<Notification> updatedQueue = new PriorityQueue<Notification>(
|
|
||||||
NotificationComparer.Default
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy all notifications from the original queue to a new one, adjusting
|
|
||||||
// those with an absolute notification time along the way to a new due tick
|
|
||||||
while(this.notifications.Count > 0) {
|
|
||||||
Notification notification = this.notifications.Dequeue();
|
|
||||||
if(!notification.Cancelled) {
|
|
||||||
|
|
||||||
// If this notification has an absolute due time, adjust its due tick
|
|
||||||
if(notification.AbsoluteUtcTime != DateTime.MinValue) {
|
|
||||||
|
|
||||||
// Combining recurrent notifications with absolute time isn't allowed
|
|
||||||
Debug.Assert(notification.IntervalTicks == 0);
|
|
||||||
|
|
||||||
// Make the adjustment
|
|
||||||
long remainingTicks = (notification.AbsoluteUtcTime - currentTime).Ticks;
|
|
||||||
notification.NextDueTicks = currentTicks + remainingTicks;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification processed, move it over to the new priority queue
|
|
||||||
updatedQueue.Enqueue(notification);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the working queue with the updated queue
|
|
||||||
this.notifications = updatedQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notificationWaitEvent.Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Schedules a notification for processing by the timer thread</summary>
|
|
||||||
/// <param name="notification">Notification that will be scheduled</param>
|
|
||||||
/// <returns>The scheduled notification</returns>
|
|
||||||
private object scheduleNotification(Notification notification) {
|
|
||||||
lock(this.timerThread) {
|
|
||||||
this.notifications.Enqueue(notification);
|
|
||||||
|
|
||||||
// If this notification has become the 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Executes the timer thread</summary>
|
|
||||||
private void runTimerThread() {
|
|
||||||
Notification nextDueNotification;
|
|
||||||
lock(this.timerThread) {
|
|
||||||
nextDueNotification = getNextDueNotification();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep processing notifications until we're told to quit
|
|
||||||
for(; ; ) {
|
|
||||||
|
|
||||||
// Wait until the nextmost notification is due or something else wakes us up
|
|
||||||
if(nextDueNotification == null) {
|
|
||||||
this.notificationWaitEvent.WaitOne();
|
|
||||||
} else {
|
|
||||||
long remainingTicks = nextDueNotification.NextDueTicks - this.timeSource.Ticks;
|
|
||||||
if(remainingTicks > 0) {
|
|
||||||
this.timeSource.WaitOne(this.notificationWaitEvent, remainingTicks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have we been woken up because the Scheduler is being disposed?
|
|
||||||
if(this.endRequested) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all notifications that are due by handing them over to the thread pool.
|
|
||||||
// The notification queue might have been updated while we were sleeping, so
|
|
||||||
// look for the notification that is due next again
|
|
||||||
long ticks = this.timeSource.Ticks;
|
|
||||||
lock(this.timerThread) {
|
|
||||||
for(; ; ) {
|
|
||||||
nextDueNotification = getNextDueNotification();
|
|
||||||
if(nextDueNotification == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the next notification is more than a millisecond away, we've reached
|
|
||||||
// the end of the notifications we need to process.
|
|
||||||
long remainingTicks = (nextDueNotification.NextDueTicks - ticks);
|
|
||||||
if(remainingTicks >= TicksPerMillisecond) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!nextDueNotification.Cancelled) {
|
|
||||||
ThreadPool.QueueUserWorkItem(nextDueNotification.Callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notifications.Dequeue();
|
|
||||||
if(nextDueNotification.IntervalTicks != 0) {
|
|
||||||
nextDueNotification.NextDueTicks += nextDueNotification.IntervalTicks;
|
|
||||||
this.notifications.Enqueue(nextDueNotification);
|
|
||||||
}
|
|
||||||
} // for
|
|
||||||
} // lock
|
|
||||||
|
|
||||||
} // for
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Retrieves the notification that is due next</summary>
|
|
||||||
/// <returns>The notification that is due next</returns>
|
|
||||||
private Notification getNextDueNotification() {
|
|
||||||
while(this.notifications.Count > 0) {
|
|
||||||
Notification nextDueNotification = this.notifications.Peek();
|
|
||||||
if(nextDueNotification.Cancelled) {
|
|
||||||
this.notifications.Dequeue();
|
|
||||||
} else {
|
|
||||||
return nextDueNotification;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Time source used by the scheduler</summary>
|
|
||||||
private ITimeSource timeSource;
|
|
||||||
/// <summary>Thread that will wait for the next scheduled event</summary>
|
|
||||||
private Thread timerThread;
|
|
||||||
/// <summary>Notifications in the scheduler's queue</summary>
|
|
||||||
private PriorityQueue<Notification> notifications;
|
|
||||||
|
|
||||||
/// <summary>Event used by the timer thread to wait for the next notification</summary>
|
|
||||||
private AutoResetEvent notificationWaitEvent;
|
|
||||||
/// <summary>Whether the timer thread should end</summary>
|
|
||||||
private volatile bool endRequested;
|
|
||||||
|
|
||||||
/// <summary>Delegate for the dateTimeAdjusted() method</summary>
|
|
||||||
private EventHandler dateTimeAdjustedDelegate;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,153 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the thread callback operation class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class ThreadCallbackOperationTest {
|
|
||||||
|
|
||||||
#region class TestThreadOperation
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides a test callback for unit testing the thread callback operation
|
|
||||||
/// </summary>
|
|
||||||
private class TestCallbackProvider {
|
|
||||||
|
|
||||||
/// <summary>Method that can be invoked as a callback</summary>
|
|
||||||
public void Callback() {
|
|
||||||
this.called = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the callback has been called</summary>
|
|
||||||
public bool Called {
|
|
||||||
get { return this.called; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Set to true when the callback has been called</summary>
|
|
||||||
private bool called;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestOperation
|
|
||||||
|
|
||||||
/// <summary>Verifies that the default constructor for a thread operation works</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
ThreadCallbackOperation test = new ThreadCallbackOperation(
|
|
||||||
new ThreadStart(errorCallback)
|
|
||||||
);
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation can execute in a thread pool thread
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestExecutionInThreadPool() {
|
|
||||||
TestCallbackProvider callbackProvider = new TestCallbackProvider();
|
|
||||||
|
|
||||||
ThreadCallbackOperation test = new ThreadCallbackOperation(
|
|
||||||
new ThreadStart(callbackProvider.Callback), true
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
Assert.IsFalse(callbackProvider.Called);
|
|
||||||
test.Start();
|
|
||||||
test.Join();
|
|
||||||
Assert.IsTrue(test.Ended);
|
|
||||||
Assert.IsTrue(callbackProvider.Called);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation can execute in an explicit thread
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestExecutionInExplicitThread() {
|
|
||||||
TestCallbackProvider callbackProvider = new TestCallbackProvider();
|
|
||||||
|
|
||||||
ThreadCallbackOperation test = new ThreadCallbackOperation(
|
|
||||||
new ThreadStart(callbackProvider.Callback), false
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
Assert.IsFalse(callbackProvider.Called);
|
|
||||||
test.Start();
|
|
||||||
test.Join();
|
|
||||||
Assert.IsTrue(test.Ended);
|
|
||||||
Assert.IsTrue(callbackProvider.Called);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation forwards an exception that occurred in
|
|
||||||
/// a thread pool thread.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestForwardExceptionFromThreadPool() {
|
|
||||||
ThreadCallbackOperation test = new ThreadCallbackOperation(
|
|
||||||
new ThreadStart(errorCallback), false
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { test.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation forwards an exception that occurred in
|
|
||||||
/// an explicit thread.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestForwardExceptionFromExplicitThread() {
|
|
||||||
ThreadCallbackOperation test = new ThreadCallbackOperation(
|
|
||||||
new ThreadStart(errorCallback), false
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { test.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Callback which throws an exception to simulate an error during callback execution
|
|
||||||
/// </summary>
|
|
||||||
private static void errorCallback() {
|
|
||||||
throw new AbortedException("Hello World");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,69 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Operation that executes a method in a background thread</summary>
|
|
||||||
public class ThreadCallbackOperation : ThreadOperation {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new threaded method operation that will call back a
|
|
||||||
/// parameterless method from the background thread.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="method">Method to be invoked in a background thread</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Uses a ThreadPool thread to execute the method in
|
|
||||||
/// </remarks>
|
|
||||||
public ThreadCallbackOperation(ThreadStart method)
|
|
||||||
: this(method, true) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new threaded method operation that will call back a
|
|
||||||
/// parameterless method from the background thread and use the
|
|
||||||
/// thread pool optionally.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="method">Method to be invoked in a background thread</param>
|
|
||||||
/// <param name="useThreadPool">Whether to use a ThreadPool thread</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// If useThreadPool is false, a new thread will be created. This guarantees
|
|
||||||
/// that the method will be executed immediately but has an impact on
|
|
||||||
/// performance since the creation of new threads is not a cheap operation.
|
|
||||||
/// </remarks>
|
|
||||||
public ThreadCallbackOperation(ThreadStart method, bool useThreadPool)
|
|
||||||
: base(useThreadPool) {
|
|
||||||
|
|
||||||
this.method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Executes the thread callback in the background thread</summary>
|
|
||||||
protected override void Execute() {
|
|
||||||
this.method();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Method to be invoked in a background thread</summary>
|
|
||||||
private ThreadStart method;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,139 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
|
||||||
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the thread operation class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class ThreadOperationTest {
|
|
||||||
|
|
||||||
#region class TestThreadOperation
|
|
||||||
|
|
||||||
/// <summary>Dummy operation used to run the unit tests</summary>
|
|
||||||
private class TestThreadOperation : ThreadOperation {
|
|
||||||
|
|
||||||
/// <summary>Initializes a dummy operation</summary>
|
|
||||||
public TestThreadOperation() { }
|
|
||||||
|
|
||||||
/// <summary>Initializes a dummy operation</summary>
|
|
||||||
/// <param name="useThreadPool">Whether to use a ThreadPool thread.</param>
|
|
||||||
public TestThreadOperation(bool useThreadPool) : base(useThreadPool) { }
|
|
||||||
|
|
||||||
/// <summary>Contains the payload to be executed in the background thread</summary>
|
|
||||||
protected override void Execute() { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestOperation
|
|
||||||
|
|
||||||
#region class FailingThreadOperation
|
|
||||||
|
|
||||||
/// <summary>Dummy operation used to run the unit tests</summary>
|
|
||||||
private class FailingThreadOperation : ThreadOperation {
|
|
||||||
|
|
||||||
/// <summary>Initializes a dummy operation</summary>
|
|
||||||
/// <param name="useThreadPool">Whether to use a ThreadPool thread.</param>
|
|
||||||
public FailingThreadOperation(bool useThreadPool) : base(useThreadPool) { }
|
|
||||||
|
|
||||||
/// <summary>Contains the payload to be executed in the background thread</summary>
|
|
||||||
protected override void Execute() {
|
|
||||||
throw new AbortedException("Hello World");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class FailingThreadOperation
|
|
||||||
|
|
||||||
/// <summary>Verifies that the default constructor for a thread operation works</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
TestThreadOperation test = new TestThreadOperation();
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation can execute in a thread pool thread
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestExecutionInThreadPool() {
|
|
||||||
TestThreadOperation test = new TestThreadOperation(true);
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
test.Join();
|
|
||||||
Assert.IsTrue(test.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation can execute in an explicit thread
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestExecutionInExplicitThread() {
|
|
||||||
TestThreadOperation test = new TestThreadOperation(false);
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
test.Join();
|
|
||||||
Assert.IsTrue(test.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation forwards an exception that occurred in
|
|
||||||
/// a thread pool thread.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestForwardExceptionFromThreadPool() {
|
|
||||||
FailingThreadOperation test = new FailingThreadOperation(true);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { test.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the threaded operation forwards an exception that occurred in
|
|
||||||
/// an explicit thread.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestForwardExceptionFromExplicitThread() {
|
|
||||||
FailingThreadOperation test = new FailingThreadOperation(false);
|
|
||||||
|
|
||||||
Assert.IsFalse(test.Ended);
|
|
||||||
test.Start();
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { test.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,112 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Operation that executes a method in a background thread</summary>
|
|
||||||
public abstract class ThreadOperation : Operation {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new threaded operation.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Uses a ThreadPool thread to execute the method in a background thread.
|
|
||||||
/// </remarks>
|
|
||||||
public ThreadOperation() : this(true) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new threaded operation which optionally uses the ThreadPool.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useThreadPool">Whether to use a ThreadPool thread.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// If useThreadPool is false, a new thread will be created. This guarantees
|
|
||||||
/// that the method will be executed immediately but has an impact on
|
|
||||||
/// performance since the creation of new threads is not a cheap operation.
|
|
||||||
/// </remarks>
|
|
||||||
public ThreadOperation(bool useThreadPool) {
|
|
||||||
this.useThreadPool = useThreadPool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Launches the background operation</summary>
|
|
||||||
public override void Start() {
|
|
||||||
Debug.Assert(
|
|
||||||
!Ended,
|
|
||||||
"Tried to Start an Operation again that has already ended",
|
|
||||||
"Operations cannot be re-run"
|
|
||||||
);
|
|
||||||
if(useThreadPool) {
|
|
||||||
ThreadPool.QueueUserWorkItem(new WaitCallback(callMethod));
|
|
||||||
} else {
|
|
||||||
Thread thread = new Thread(new ThreadStart(callMethod));
|
|
||||||
thread.Name = "Nuclex.Support.Scheduling.ThreadOperation";
|
|
||||||
thread.IsBackground = true;
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Contains the payload to be executed in the background thread</summary>
|
|
||||||
protected abstract void Execute();
|
|
||||||
|
|
||||||
/// <summary>Invokes the delegate passed as an argument</summary>
|
|
||||||
/// <param name="state">Not used</param>
|
|
||||||
private void callMethod(object state) {
|
|
||||||
callMethod();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Invokes the delegate passed as an argument</summary>
|
|
||||||
private void callMethod() {
|
|
||||||
try {
|
|
||||||
Execute();
|
|
||||||
Debug.Assert(
|
|
||||||
!Ended,
|
|
||||||
"Operation unexpectedly ended during Execute()",
|
|
||||||
"Do not call OnAsyncEnded() yourself when deriving from ThreadOperation"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch(Exception exception) {
|
|
||||||
this.exception = exception;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override void ReraiseExceptions() {
|
|
||||||
if(this.exception != null)
|
|
||||||
throw this.exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether to use the ThreadPool for obtaining a background thread</summary>
|
|
||||||
private bool useThreadPool;
|
|
||||||
/// <summary>Exception that has occured in the background process</summary>
|
|
||||||
private volatile Exception exception;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,164 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
#if WINDOWS
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the windows time source</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class WindowsTimeSourceTest {
|
|
||||||
|
|
||||||
#region class TestWindowsTimeSource
|
|
||||||
|
|
||||||
/// <summary>Windows time source used for testing</summary>
|
|
||||||
private class TestWindowsTimeSource : WindowsTimeSource {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forces a time change notification even if the system time was not adjusted
|
|
||||||
/// </summary>
|
|
||||||
public void ForceTimeChange() {
|
|
||||||
OnDateTimeAdjusted(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestWindowsTimeSource
|
|
||||||
|
|
||||||
#region class TestTimeChangedSubscriber
|
|
||||||
|
|
||||||
/// <summary>Dummy subscriber used to test the time changed event</summary>
|
|
||||||
private class TestTimeChangedSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Callback subscribed to the TimeChanged event</summary>
|
|
||||||
/// <param name="sender">Not used</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
public void TimeChanged(object sender, EventArgs arguments) {
|
|
||||||
++this.CallCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Number of times the callback was invoked</summary>
|
|
||||||
public int CallCount;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestTimeChangedSubscriber
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source's default constructor is working
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultConstructor() {
|
|
||||||
using(WindowsTimeSource timeSource = new WindowsTimeSource()) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source can provide the current UTC time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCurrentUtcTime() {
|
|
||||||
using(WindowsTimeSource timeSource = new WindowsTimeSource()) {
|
|
||||||
|
|
||||||
Assert.That(
|
|
||||||
timeSource.CurrentUtcTime, Is.EqualTo(DateTime.UtcNow).Within(10).Seconds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source's tick property is working if
|
|
||||||
/// the Stopwatch class is used to measure time
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTicks() {
|
|
||||||
using(WindowsTimeSource timeSource = new WindowsTimeSource()) {
|
|
||||||
long ticks1 = timeSource.Ticks;
|
|
||||||
long ticks2 = timeSource.Ticks;
|
|
||||||
|
|
||||||
Assert.That(ticks2, Is.GreaterThanOrEqualTo(ticks1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the time source's WaitOne() method works correctly
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWaitOne() {
|
|
||||||
using(WindowsTimeSource timeSource = new WindowsTimeSource()) {
|
|
||||||
AutoResetEvent waitEvent = new AutoResetEvent(true);
|
|
||||||
|
|
||||||
Assert.IsTrue(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks));
|
|
||||||
Assert.IsFalse(timeSource.WaitOne(waitEvent, TimeSpan.FromMilliseconds(1).Ticks));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the default time source's WaitOne() method works correctly
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTimeChange() {
|
|
||||||
using(TestWindowsTimeSource timeSource = new TestWindowsTimeSource()) {
|
|
||||||
TestTimeChangedSubscriber subscriber = new TestTimeChangedSubscriber();
|
|
||||||
|
|
||||||
EventHandler callbackDelegate = new EventHandler(subscriber.TimeChanged);
|
|
||||||
timeSource.DateTimeAdjusted += callbackDelegate;
|
|
||||||
try {
|
|
||||||
timeSource.ForceTimeChange();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
timeSource.DateTimeAdjusted -= callbackDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using greater than because during the test run a real time change notification
|
|
||||||
// might have happened, increasing the counter to 2 or more.
|
|
||||||
Assert.That(subscriber.CallCount, Is.GreaterThanOrEqualTo(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the Windows-specific time source can reports its availability on
|
|
||||||
/// the current platform
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestAvailability() {
|
|
||||||
bool isAvailable = WindowsTimeSource.Available;
|
|
||||||
Assert.IsTrue(
|
|
||||||
(isAvailable == true) ||
|
|
||||||
(isAvailable == false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
||||||
|
|
||||||
#endif // WINDOWS
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,91 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
#if !NO_SYSTEMEVENTS
|
|
||||||
using Microsoft.Win32;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Scheduling {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time source that makes use of additional features only available on Windows
|
|
||||||
/// </summary>
|
|
||||||
public class WindowsTimeSource : GenericTimeSource, IDisposable {
|
|
||||||
|
|
||||||
/// <summary>Number of ticks (100 ns intervals) in a millisecond</summary>
|
|
||||||
private const long TicksPerMillisecond = 10000;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new Windows time source</summary>
|
|
||||||
public WindowsTimeSource() {
|
|
||||||
#if NO_SYSTEMEVENTS
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Windows time source is not available without the SystemEvents class"
|
|
||||||
);
|
|
||||||
#else
|
|
||||||
this.onDateTimeAdjustedDelegate = new EventHandler(OnDateTimeAdjusted);
|
|
||||||
SystemEvents.TimeChanged += this.onDateTimeAdjustedDelegate;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
|
||||||
public void Dispose() {
|
|
||||||
#if !NO_SYSTEMEVENTS
|
|
||||||
if (this.onDateTimeAdjustedDelegate != null) {
|
|
||||||
SystemEvents.TimeChanged -= this.onDateTimeAdjustedDelegate;
|
|
||||||
this.onDateTimeAdjustedDelegate = null;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits for an AutoResetEvent to become signalled</summary>
|
|
||||||
/// <param name="waitHandle">WaitHandle the method will wait for</param>
|
|
||||||
/// <param name="ticks">Number of ticks to wait</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the WaitHandle was signalled, false if the timeout was reached
|
|
||||||
/// </returns>
|
|
||||||
public override bool WaitOne(AutoResetEvent waitHandle, long ticks) {
|
|
||||||
#if WINDOWS
|
|
||||||
return waitHandle.WaitOne((int)(ticks / TicksPerMillisecond), false);
|
|
||||||
#else
|
|
||||||
return waitHandle.WaitOne((int)(ticks / TicksPerMillisecond));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the Windows time source can be used on the current platform
|
|
||||||
/// </summary>
|
|
||||||
public static bool Available {
|
|
||||||
get { return Environment.OSVersion.Platform == PlatformID.Win32NT; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !NO_SYSTEMEVENTS
|
|
||||||
|
|
||||||
/// <summary>Delegate for the timeChanged() callback method</summary>
|
|
||||||
private EventHandler onDateTimeAdjustedDelegate;
|
|
||||||
|
|
||||||
#endif // !NO_SYSTEMEVENTS
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Scheduling
|
|
|
@ -1,34 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Interface for processes that report their progress</summary>
|
|
||||||
public interface IProgressReporter {
|
|
||||||
|
|
||||||
/// <summary>Triggered when the status of the process changes</summary>
|
|
||||||
event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,34 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Interface for processes that report their status</summary>
|
|
||||||
public interface IStatusReporter {
|
|
||||||
|
|
||||||
/// <summary>Triggered when the status of the process changes</summary>
|
|
||||||
event EventHandler<StatusReportEventArgs> AsyncStatusChanged;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,58 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the "idle state" event argument container</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class IdleStateEventArgsTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the idle state event arguments correctly report a non-idle state
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestIdleStateChangedToFalse() {
|
|
||||||
IdleStateEventArgs idleStateFalse = new IdleStateEventArgs(false);
|
|
||||||
|
|
||||||
Assert.IsFalse(idleStateFalse.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the idle state event arguments correctly report an idle state
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestIdleStateChangedToTrue() {
|
|
||||||
IdleStateEventArgs idleStateFalse = new IdleStateEventArgs(true);
|
|
||||||
|
|
||||||
Assert.IsTrue(idleStateFalse.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,45 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Event arguments for an idle state change notification</summary>
|
|
||||||
public class IdleStateEventArgs : EventArgs {
|
|
||||||
|
|
||||||
/// <summary>Initializes the idle state change notification</summary>
|
|
||||||
/// <param name="idle">The new idle state</param>
|
|
||||||
public IdleStateEventArgs(bool idle) {
|
|
||||||
this.idle = idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Current idle state</summary>
|
|
||||||
public bool Idle {
|
|
||||||
get { return this.idle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Current idle state</summary>
|
|
||||||
private bool idle;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,160 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
#if UNITTEST
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NMock;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the observation wrapper of weighted transactions</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class ObservedWeightedTransactionTest {
|
|
||||||
|
|
||||||
#region interface IObservationSubscriber
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interface used to test the observation wrapper of weighted transactions
|
|
||||||
/// </summary>
|
|
||||||
public interface IObservationSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Will be invoked when an observed transaction's progress changes</summary>
|
|
||||||
void ProgressUpdated();
|
|
||||||
|
|
||||||
/// <summary>Will be invoked when an observed transaction completes</summary>
|
|
||||||
void Ended();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // interface IObservationSubscriber
|
|
||||||
|
|
||||||
#region class FunkyTransaction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transaction that goes into the 'ended' state as soon as someone registers for
|
|
||||||
/// state change notifications
|
|
||||||
/// </summary>
|
|
||||||
private class FunkyTransaction : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Manages registrations to the AsyncEnded event</summary>
|
|
||||||
public override event EventHandler AsyncEnded {
|
|
||||||
add {
|
|
||||||
base.AsyncEnded += value;
|
|
||||||
|
|
||||||
// To deterministically provoke an 'Ended' event just after registration we
|
|
||||||
// will switch the transaction into the 'ended' state right here
|
|
||||||
int oldValue = Interlocked.Exchange(ref this.alreadyEnded, 1);
|
|
||||||
if(oldValue != 1) {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove {
|
|
||||||
base.AsyncEnded -= value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the transaction has already been ended</summary>
|
|
||||||
private int alreadyEnded;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class FunkyTransaction
|
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() {
|
|
||||||
this.mockery = new MockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Verifies that the constructor of the observation wrapper works</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestConstructorWithAlreadyEndedTransaction() {
|
|
||||||
WeightedTransaction<Transaction> testTransaction = new WeightedTransaction<Transaction>(
|
|
||||||
Transaction.EndedDummy
|
|
||||||
);
|
|
||||||
|
|
||||||
Mock<IObservationSubscriber> subscriber = this.mockery.CreateMock<IObservationSubscriber>();
|
|
||||||
|
|
||||||
subscriber.Expects.AtLeast(0).Method(m => m.ProgressUpdated());
|
|
||||||
// This should no be called because otherwise, the 'Ended' event would be raised
|
|
||||||
// to the transaction group before all transactions have been added into
|
|
||||||
// the internal list, leading to an early ending or even multiple endings.
|
|
||||||
subscriber.Expects.No.Method(m => m.Ended());
|
|
||||||
|
|
||||||
using(
|
|
||||||
ObservedWeightedTransaction<Transaction> test =
|
|
||||||
new ObservedWeightedTransaction<Transaction>(
|
|
||||||
testTransaction,
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(
|
|
||||||
subscriber.MockObject.ProgressUpdated
|
|
||||||
),
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(
|
|
||||||
subscriber.MockObject.Ended
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the constructor of the observation wrapper can handle a transaction
|
|
||||||
/// entering the 'ended' state right on subscription
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestConstructorWithEndingTransaction() {
|
|
||||||
WeightedTransaction<Transaction> testTransaction = new WeightedTransaction<Transaction>(
|
|
||||||
new FunkyTransaction()
|
|
||||||
);
|
|
||||||
|
|
||||||
Mock<IObservationSubscriber> subscriber = this.mockery.CreateMock<IObservationSubscriber>();
|
|
||||||
|
|
||||||
subscriber.Expects.AtLeast(0).Method(m => m.ProgressUpdated());
|
|
||||||
subscriber.Expects.One.Method(m => m.Ended());
|
|
||||||
|
|
||||||
using(
|
|
||||||
ObservedWeightedTransaction<Transaction> test =
|
|
||||||
new ObservedWeightedTransaction<Transaction>(
|
|
||||||
testTransaction,
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(
|
|
||||||
subscriber.MockObject.ProgressUpdated
|
|
||||||
),
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(
|
|
||||||
subscriber.MockObject.Ended
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mock object factory</summary>
|
|
||||||
private MockFactory mockery;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,186 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Transaction being observed by another object</summary>
|
|
||||||
/// <typeparam name="TransactionType">
|
|
||||||
/// Type of the transaction that is being observed
|
|
||||||
/// </typeparam>
|
|
||||||
internal class ObservedWeightedTransaction<TransactionType> : IDisposable
|
|
||||||
where TransactionType : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Delegate for reporting progress updates</summary>
|
|
||||||
public delegate void ReportDelegate();
|
|
||||||
|
|
||||||
/// <summary>Initializes a new observed transaction</summary>
|
|
||||||
/// <param name="weightedTransaction">Weighted transaction being observed</param>
|
|
||||||
/// <param name="progressUpdateCallback">
|
|
||||||
/// Callback to invoke when the transaction's progress changes
|
|
||||||
/// </param>
|
|
||||||
/// <param name="endedCallback">
|
|
||||||
/// Callback to invoke when the transaction has ended
|
|
||||||
/// </param>
|
|
||||||
internal ObservedWeightedTransaction(
|
|
||||||
WeightedTransaction<TransactionType> weightedTransaction,
|
|
||||||
ReportDelegate progressUpdateCallback,
|
|
||||||
ReportDelegate endedCallback
|
|
||||||
) {
|
|
||||||
this.weightedTransaction = weightedTransaction;
|
|
||||||
|
|
||||||
// See if this transaction has already ended (initial check for performance)
|
|
||||||
if(weightedTransaction.Transaction.Ended) {
|
|
||||||
|
|
||||||
// Since we don't subscribe to the .Ended event (which would be fired immediately on
|
|
||||||
// subscription if the transaction was already finished), we will emulate this
|
|
||||||
// behavior here. There is no race condition here: The transition to .Ended occurs
|
|
||||||
// only once and will never happen in reverse. This is just a minor optimization to
|
|
||||||
// prevent object coupling where none is neccessary and to save some processing time.
|
|
||||||
this.progress = 1.0f;
|
|
||||||
progressUpdateCallback();
|
|
||||||
|
|
||||||
// Do not call the ended callback here. This constructor is called when the
|
|
||||||
// TransactionGroup constructs its list of transactions. If this is called and
|
|
||||||
// the first transaction to be added to the group happens to be in the ended
|
|
||||||
// state, the transactionGroup will immediately think it has ended!
|
|
||||||
//!DONT!endedCallback();
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.endedCallback = endedCallback;
|
|
||||||
this.progressUpdateCallback = progressUpdateCallback;
|
|
||||||
|
|
||||||
// This might trigger the event handler to be invoked right here if the transaction
|
|
||||||
// ended between our initial optimization attempt and this line. It's unlikely,
|
|
||||||
// however, so we'll not waste time with another optimization attempt.
|
|
||||||
this.weightedTransaction.Transaction.AsyncEnded += new EventHandler(asyncEnded);
|
|
||||||
|
|
||||||
// See whether this transaction implements the IProgressReporter interface and if
|
|
||||||
// so, connect to its progress report event in order to pass these reports on
|
|
||||||
// to whomever created ourselfes.
|
|
||||||
this.progressReporter = this.weightedTransaction.Transaction as IProgressReporter;
|
|
||||||
if(this.progressReporter != null) {
|
|
||||||
this.asyncProgressChangedEventHandler = new EventHandler<ProgressReportEventArgs>(
|
|
||||||
asyncProgressChanged
|
|
||||||
);
|
|
||||||
this.progressReporter.AsyncProgressChanged += this.asyncProgressChangedEventHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the object</summary>
|
|
||||||
public void Dispose() {
|
|
||||||
asyncDisconnectEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Weighted transaction being observed</summary>
|
|
||||||
public WeightedTransaction<TransactionType> WeightedTransaction {
|
|
||||||
get { return this.weightedTransaction; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Amount of progress this transaction has achieved so far</summary>
|
|
||||||
public float Progress {
|
|
||||||
get { return this.progress; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the observed transaction has ended</summary>
|
|
||||||
/// <param name="sender">Transaction that has ended</param>
|
|
||||||
/// <param name="e">Not used</param>
|
|
||||||
private void asyncEnded(object sender, EventArgs e) {
|
|
||||||
ReportDelegate savedEndedCallback = this.endedCallback;
|
|
||||||
ReportDelegate savedProgressUpdateCallback = this.progressUpdateCallback;
|
|
||||||
|
|
||||||
asyncDisconnectEvents(); // We don't need those anymore!
|
|
||||||
|
|
||||||
// If the progress hasn't reached 1.0 yet, make a fake report so that even
|
|
||||||
// when a transaction doesn't report any progress at all, the set or queue
|
|
||||||
// owning us will have a percentage of transactions completed.
|
|
||||||
//
|
|
||||||
// There is the possibility of a race condition here, as a final progress
|
|
||||||
// report could have been generated by a thread running the transaction
|
|
||||||
// that was preempted by this thread. This would cause the progress to
|
|
||||||
// jump to 1.0 and then back to whatever the waiting thread will report.
|
|
||||||
if(this.progress != 1.0f) {
|
|
||||||
this.progress = 1.0f;
|
|
||||||
savedProgressUpdateCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
savedEndedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the progress of the observed transaction changes</summary>
|
|
||||||
/// <param name="sender">Transaction whose progress has changed</param>
|
|
||||||
/// <param name="arguments">Contains the updated progress</param>
|
|
||||||
private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
|
|
||||||
this.progress = arguments.Progress;
|
|
||||||
|
|
||||||
ReportDelegate savedProgressUpdateCallback = this.progressUpdateCallback;
|
|
||||||
if(savedProgressUpdateCallback != null) {
|
|
||||||
savedProgressUpdateCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Unsubscribes from all events of the observed transaction</summary>
|
|
||||||
private void asyncDisconnectEvents() {
|
|
||||||
|
|
||||||
// Make use of the double check locking idiom to avoid the costly lock when
|
|
||||||
// the events have already been unsubscribed
|
|
||||||
if(this.endedCallback != null) {
|
|
||||||
|
|
||||||
// This is an internal class with special knowledge that there
|
|
||||||
// is no risk of deadlock involved, so we don't need a fancy syncRoot!
|
|
||||||
lock(this) {
|
|
||||||
if(this.endedCallback != null) {
|
|
||||||
this.weightedTransaction.Transaction.AsyncEnded -= new EventHandler(asyncEnded);
|
|
||||||
|
|
||||||
if(this.progressReporter != null) {
|
|
||||||
this.progressReporter.AsyncProgressChanged -=
|
|
||||||
this.asyncProgressChangedEventHandler;
|
|
||||||
|
|
||||||
this.asyncProgressChangedEventHandler = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.endedCallback = null;
|
|
||||||
this.progressUpdateCallback = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // endedCallback != null
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventHandler<ProgressReportEventArgs> asyncProgressChangedEventHandler;
|
|
||||||
/// <summary>The observed transaction's progress reporting interface</summary>
|
|
||||||
private IProgressReporter progressReporter;
|
|
||||||
/// <summary>The weighted wable that is being observed</summary>
|
|
||||||
private WeightedTransaction<TransactionType> weightedTransaction;
|
|
||||||
/// <summary>Callback to invoke when the progress updates</summary>
|
|
||||||
private volatile ReportDelegate progressUpdateCallback;
|
|
||||||
/// <summary>Callback to invoke when the transaction ends</summary>
|
|
||||||
private volatile ReportDelegate endedCallback;
|
|
||||||
/// <summary>Progress achieved so far</summary>
|
|
||||||
private volatile float progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,71 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
#if UNITTEST
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unit Test for the observation wrapper collection of weighted transactions
|
|
||||||
/// </summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class WeightedTransactionWrapperCollectionTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the wrapper collection is handing out the unwrapped transactions
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWrapperCollection() {
|
|
||||||
WeightedTransaction<Transaction> transaction = new WeightedTransaction<Transaction>(
|
|
||||||
Transaction.EndedDummy
|
|
||||||
);
|
|
||||||
|
|
||||||
ObservedWeightedTransaction<Transaction> observed =
|
|
||||||
new ObservedWeightedTransaction<Transaction>(
|
|
||||||
transaction,
|
|
||||||
endedCallback,
|
|
||||||
progressUpdatedCallback
|
|
||||||
);
|
|
||||||
|
|
||||||
WeightedTransactionWrapperCollection<Transaction> wrapper =
|
|
||||||
new WeightedTransactionWrapperCollection<Transaction>(
|
|
||||||
new ObservedWeightedTransaction<Transaction>[] { observed }
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.AreSame(transaction, wrapper[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Dummy callback used as event subscriber in the tests</summary>
|
|
||||||
private void endedCallback() { }
|
|
||||||
|
|
||||||
/// <summary>Dummy callback used as event subscriber in the tests</summary>
|
|
||||||
private void progressUpdatedCallback() { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,80 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
using Nuclex.Support.Collections;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Collection of transactions with a weighting value</summary>
|
|
||||||
/// <typeparam name="TransactionType">Type of transactions to manage</typeparam>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// This collection is exposed as a read-only collection to the user that
|
|
||||||
/// stores WeightedTransactions. Internally, it merely wraps a collection of
|
|
||||||
/// an internal type used to keep track of the individual transaction's
|
|
||||||
/// progress in the TransactionGroup and OperationQueue classes.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// It is read-only because the design requires a transaction to only ever finish
|
|
||||||
/// once. If it was possible eg. to add items after a TransactionGroup had signalled
|
|
||||||
/// itself as being finished, it would be moved into an unfinished state again.
|
|
||||||
/// Also, an empty TransactionGroup is, by definition, finished (simply because
|
|
||||||
/// there is no work to do) - unless the contents of the group are passed to the
|
|
||||||
/// TransactionGroup's constructor and never modified at all, the design would be
|
|
||||||
/// violated as soon as an instance of the TransactionGroup or OperationQueue
|
|
||||||
/// classes was created.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
internal class WeightedTransactionWrapperCollection<TransactionType> :
|
|
||||||
TransformingReadOnlyCollection<
|
|
||||||
ObservedWeightedTransaction<TransactionType>, WeightedTransaction<TransactionType>
|
|
||||||
>
|
|
||||||
where TransactionType : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new weighted transaction collection wrapper</summary>
|
|
||||||
/// <param name="items">Items to be exposed as weighted transactions</param>
|
|
||||||
internal WeightedTransactionWrapperCollection(
|
|
||||||
IList<ObservedWeightedTransaction<TransactionType>> items
|
|
||||||
)
|
|
||||||
: base(items) { }
|
|
||||||
|
|
||||||
/// <summary>Transforms an item into the exposed type</summary>
|
|
||||||
/// <param name="item">Item to be transformed</param>
|
|
||||||
/// <returns>The transformed item</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// This method is used to transform an item in the wrapped collection into
|
|
||||||
/// the exposed item type whenever the user accesses an item. Expect it to
|
|
||||||
/// be called frequently, because the TransformingReadOnlyCollection does
|
|
||||||
/// not cache otherwise store the transformed items.
|
|
||||||
/// </remarks>
|
|
||||||
protected override WeightedTransaction<TransactionType> Transform(
|
|
||||||
ObservedWeightedTransaction<TransactionType> item
|
|
||||||
) {
|
|
||||||
return item.WeightedTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,58 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the progress report event argument container</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class ProgressReportEventArgsTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the progress report event arguments correctly report zero progress
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestZeroProgress() {
|
|
||||||
ProgressReportEventArgs zeroProgress = new ProgressReportEventArgs(0.0f);
|
|
||||||
|
|
||||||
Assert.AreEqual(0.0f, zeroProgress.Progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the progress report event arguments correctly report complete progress
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestCompleteProgress() {
|
|
||||||
ProgressReportEventArgs zeroProgress = new ProgressReportEventArgs(1.0f);
|
|
||||||
|
|
||||||
Assert.AreEqual(1.0f, zeroProgress.Progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,45 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Event arguments for a progress update notification</summary>
|
|
||||||
public class ProgressReportEventArgs : EventArgs {
|
|
||||||
|
|
||||||
/// <summary>Initializes the progress update informations</summary>
|
|
||||||
/// <param name="progress">Achieved progress ranging from 0.0 to 1.0</param>
|
|
||||||
public ProgressReportEventArgs(float progress) {
|
|
||||||
this.progress = progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Currently achieved progress</summary>
|
|
||||||
public float Progress {
|
|
||||||
get { return this.progress; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Achieved progress</summary>
|
|
||||||
private float progress;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,509 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NMock;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the progress tracker class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class ProgressTrackerTest {
|
|
||||||
|
|
||||||
#region interface IProgressTrackerSubscriber
|
|
||||||
|
|
||||||
/// <summary>Interface used to test the progress tracker</summary>
|
|
||||||
public interface IProgressTrackerSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Called when the tracked progress changes</summary>
|
|
||||||
/// <param name="sender">Progress tracker whose progress has changed</param>
|
|
||||||
/// <param name="arguments">Contains the new progress achieved</param>
|
|
||||||
void ProgressChanged(object sender, ProgressReportEventArgs arguments);
|
|
||||||
|
|
||||||
/// <summary>Called when the progress tracker's idle state changes</summary>
|
|
||||||
/// <param name="sender">Progress tracker whose idle state has changed</param>
|
|
||||||
/// <param name="arguments">Contains the new idle state of the tracker</param>
|
|
||||||
void IdleStateChanged(object sender, IdleStateEventArgs arguments);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // interface IProgressTrackerSubscriber
|
|
||||||
|
|
||||||
#region class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
/// <summary>Compares two ProgressUpdateEventArgs instances</summary>
|
|
||||||
private class ProgressReportEventArgsMatcher : Matcher {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new ProgressUpdateEventArgsMatcher</summary>
|
|
||||||
/// <param name="expected">Expected progress update event arguments</param>
|
|
||||||
public ProgressReportEventArgsMatcher(ProgressReportEventArgs expected) {
|
|
||||||
this.expected = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actualAsObject">Actual value to compare to the expected value</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the actual value matches the expected value; otherwise false
|
|
||||||
/// </returns>
|
|
||||||
public override bool Matches(object actualAsObject) {
|
|
||||||
ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs);
|
|
||||||
if(actual == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return (actual.Progress == this.expected.Progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a string representation of the expected value</summary>
|
|
||||||
/// <param name="writer">Writer to write the string representation into</param>
|
|
||||||
public override void DescribeTo(TextWriter writer) {
|
|
||||||
writer.Write(this.expected.Progress.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Expected progress update event args value</summary>
|
|
||||||
private ProgressReportEventArgs expected;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
#region class TestTransaction
|
|
||||||
|
|
||||||
/// <summary>Transaction used for testing in this unit test</summary>
|
|
||||||
private class TestTransaction : Transaction, IProgressReporter {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Changes the testing transaction's indicated progress</summary>
|
|
||||||
/// <param name="progress">New progress to be reported by the testing transaction</param>
|
|
||||||
public void ChangeProgress(float progress) {
|
|
||||||
OnAsyncProgressChanged(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transitions the transaction into the ended state</summary>
|
|
||||||
public void End() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="progress">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(float progress) {
|
|
||||||
OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="eventArguments">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// Allows for classes derived from the transaction class to easily provide
|
|
||||||
/// a custom event arguments class that has been derived from the
|
|
||||||
/// transaction's ProgressUpdateEventArgs class.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, eventArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestTransaction
|
|
||||||
|
|
||||||
#region class EvilTransaction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transaction that tries to emulate a thread giving a progress report at
|
|
||||||
/// a very inconvenient time ;)
|
|
||||||
/// </summary>
|
|
||||||
private class EvilTransaction : Transaction, IProgressReporter {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged {
|
|
||||||
add { }
|
|
||||||
remove {
|
|
||||||
// Send a progress update right when the subscriber is trying to unsubscribe
|
|
||||||
value(this, new ProgressReportEventArgs(0.5f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class EvilTransaction
|
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() {
|
|
||||||
this.mockery = new MockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Validates that the tracker properly sums the progress</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSummedProgress() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
Mock<IProgressTrackerSubscriber> mockedSubscriber = mockSubscriber(tracker);
|
|
||||||
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
TestTransaction test2 = new TestTransaction();
|
|
||||||
|
|
||||||
// Step 1
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
// Since the progress is already at 0, these redundant reports are optional
|
|
||||||
mockedSubscriber.Expects.Between(0, 2).Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
tracker.Track(test1);
|
|
||||||
tracker.Track(test2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f))
|
|
||||||
);
|
|
||||||
|
|
||||||
test1.ChangeProgress(0.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the tracker only removes transactions when the whole
|
|
||||||
/// tracking list has reached the 'ended' state.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// If the tracker would remove ended transactions right when they finished,
|
|
||||||
/// the total progress would jump back each time. This is unwanted, of course.
|
|
||||||
/// </remarks>
|
|
||||||
[Test]
|
|
||||||
public void TestDelayedRemoval() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
Mock<IProgressTrackerSubscriber> mockedSubscriber = mockSubscriber(tracker);
|
|
||||||
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
TestTransaction test2 = new TestTransaction();
|
|
||||||
|
|
||||||
// Step 1
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
// This is optional. The tracker's progress is currently 0, so there's no need
|
|
||||||
// to send out superfluous progress reports.
|
|
||||||
mockedSubscriber.Expects.Between(0, 2).Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
tracker.Track(test1);
|
|
||||||
tracker.Track(test2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.25f))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Total progress should be 0.25 after this call (two transactions, one with
|
|
||||||
// 0% progress and one with 50% progress)
|
|
||||||
test1.ChangeProgress(0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.75f))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Total progress should be 0.75 after this call (one transaction at 100%,
|
|
||||||
// the other one at 50%). If the second transaction would be removed by the tracker,
|
|
||||||
// (which would be inappropriate!) the progress would falsely jump to 0.5 instead.
|
|
||||||
test2.End();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
test1.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the tracker behaves correctly if it is fed with transactions
|
|
||||||
/// that have already ended.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSoleEndedTransaction() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
Mock<IProgressTrackerSubscriber> mockedSubscriber = mockSubscriber(tracker);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.No.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
mockedSubscriber.Expects.No.Method(
|
|
||||||
m => m.ProgressChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
tracker.Track(Transaction.EndedDummy);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the tracker behaves correctly if it is fed with transactions
|
|
||||||
/// that have already ended in addition to transactions that are actively executing.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndedTransaction() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
Mock<IProgressTrackerSubscriber> mockedSubscriber = mockSubscriber(tracker);
|
|
||||||
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
|
|
||||||
// Step 1
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.AtMost(1).Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
tracker.Track(test1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(0.5f))
|
|
||||||
);
|
|
||||||
|
|
||||||
tracker.Track(Transaction.EndedDummy);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3
|
|
||||||
{
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(ProgressTracker)),
|
|
||||||
new ProgressReportEventArgsMatcher(new ProgressReportEventArgs(1.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.IdleStateChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
test1.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to provoke a deadlock by re-entering the tracker from one of its own events
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestProvokedDeadlock() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
tracker.Track(test1);
|
|
||||||
|
|
||||||
tracker.AsyncIdleStateChanged +=
|
|
||||||
(EventHandler<IdleStateEventArgs>)delegate(object sender, IdleStateEventArgs arguments) {
|
|
||||||
tracker.Track(Transaction.EndedDummy);
|
|
||||||
};
|
|
||||||
|
|
||||||
test1.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the progress tracker enters and leaves the idle state correctly
|
|
||||||
/// when a transaction is removed via Untrack()
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestIdleWithUntrack() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
|
|
||||||
Assert.IsTrue(tracker.Idle);
|
|
||||||
|
|
||||||
tracker.Track(test1);
|
|
||||||
|
|
||||||
Assert.IsFalse(tracker.Idle);
|
|
||||||
|
|
||||||
tracker.Untrack(test1);
|
|
||||||
|
|
||||||
Assert.IsTrue(tracker.Idle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the progress tracker enters and leaves the idle state correctly
|
|
||||||
/// when a transaction is removed the transaction finishing
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestIdleWithAutoRemoval() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
|
|
||||||
Assert.IsTrue(tracker.Idle);
|
|
||||||
|
|
||||||
tracker.Track(test1);
|
|
||||||
|
|
||||||
Assert.IsFalse(tracker.Idle);
|
|
||||||
|
|
||||||
test1.End();
|
|
||||||
|
|
||||||
Assert.IsTrue(tracker.Idle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the progress tracker enters and leaves the idle state correctly
|
|
||||||
/// when a transaction is removed via Untrack()
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestProgressWithUntrack() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
TestTransaction test2 = new TestTransaction();
|
|
||||||
tracker.Track(test1);
|
|
||||||
tracker.Track(test2);
|
|
||||||
|
|
||||||
Assert.AreEqual(0.0f, tracker.Progress);
|
|
||||||
|
|
||||||
test1.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
Assert.AreEqual(0.25f, tracker.Progress);
|
|
||||||
|
|
||||||
tracker.Untrack(test2);
|
|
||||||
|
|
||||||
Assert.AreEqual(0.5f, tracker.Progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the progress tracker throws an exception if it is instructed
|
|
||||||
/// to untrack a transaction it doesn't know about
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestThrowOnUntrackNonTrackedTransaction() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test1 = new TestTransaction();
|
|
||||||
|
|
||||||
Assert.Throws<ArgumentException>(
|
|
||||||
delegate() { tracker.Untrack(test1); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the progress tracker throws an exception if it is instructed
|
|
||||||
/// to untrack a transaction it doesn't know about
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestProgressReportDuringUnsubscribe() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
EvilTransaction evil = new EvilTransaction();
|
|
||||||
tracker.Track(evil);
|
|
||||||
tracker.Untrack(evil);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the progress tracker doesn't choke on a transaction being tracked
|
|
||||||
/// multiple times.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestMultiTrackedTransaction() {
|
|
||||||
using(ProgressTracker tracker = new ProgressTracker()) {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
tracker.Track(test);
|
|
||||||
tracker.Track(test);
|
|
||||||
tracker.Track(test);
|
|
||||||
tracker.Untrack(test);
|
|
||||||
tracker.Untrack(test);
|
|
||||||
tracker.Untrack(test);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mocks a subscriber for the events of a tracker</summary>
|
|
||||||
/// <param name="tracker">Tracker to mock an event subscriber for</param>
|
|
||||||
/// <returns>The mocked event subscriber</returns>
|
|
||||||
private Mock<IProgressTrackerSubscriber> mockSubscriber(ProgressTracker tracker) {
|
|
||||||
Mock<IProgressTrackerSubscriber> mockedSubscriber =
|
|
||||||
this.mockery.CreateMock<IProgressTrackerSubscriber>();
|
|
||||||
|
|
||||||
tracker.AsyncIdleStateChanged +=
|
|
||||||
new EventHandler<IdleStateEventArgs>(mockedSubscriber.MockObject.IdleStateChanged);
|
|
||||||
|
|
||||||
tracker.AsyncProgressChanged +=
|
|
||||||
new EventHandler<ProgressReportEventArgs>(mockedSubscriber.MockObject.ProgressChanged);
|
|
||||||
|
|
||||||
return mockedSubscriber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mock object factory</summary>
|
|
||||||
private MockFactory mockery;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,369 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helps tracking the progress of one or more background transactions
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// This is useful if you want to display a progress bar for multiple
|
|
||||||
/// transactions but can not guarantee that no additional transactions
|
|
||||||
/// will appear inmidst of execution.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// This class does not implement the <see cref="Transaction" /> interface itself
|
|
||||||
/// in order to not violate the design principles of transactions which
|
|
||||||
/// guarantee that a <see cref="Transaction" /> will only finish once (whereas the
|
|
||||||
/// progress tracker might 'finish' any number of times).
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public class ProgressTracker : IDisposable, IProgressReporter {
|
|
||||||
|
|
||||||
#region class TransactionMatcher
|
|
||||||
|
|
||||||
/// <summary>Matches a direct transaction to a fully wrapped one</summary>
|
|
||||||
private class TransactionMatcher {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new transaction matcher that matches against
|
|
||||||
/// the specified transaction
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="toMatch">Transaction to match against</param>
|
|
||||||
public TransactionMatcher(Transaction toMatch) {
|
|
||||||
this.toMatch = toMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the provided transaction matches the comparison
|
|
||||||
/// transaction of the instance
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">Transaction to match to the comparison transaction</param>
|
|
||||||
public bool Matches(ObservedWeightedTransaction<Transaction> other) {
|
|
||||||
return ReferenceEquals(other.WeightedTransaction.Transaction, this.toMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transaction this instance compares against</summary>
|
|
||||||
private Transaction toMatch;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TransactionMatcher
|
|
||||||
|
|
||||||
/// <summary>Triggered when the idle state of the tracker changes</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The tracker is idle when no transactions are being tracked in it. If you're
|
|
||||||
/// using this class to feed a progress bar, this would be the event to use for
|
|
||||||
/// showing or hiding the progress bar. The tracker starts off as idle because,
|
|
||||||
/// upon construction, its list of transactions will be empty.
|
|
||||||
/// </remarks>
|
|
||||||
public event EventHandler<IdleStateEventArgs> AsyncIdleStateChanged;
|
|
||||||
|
|
||||||
/// <summary>Triggered when the total progress has changed</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new transaction tracker</summary>
|
|
||||||
public ProgressTracker() {
|
|
||||||
|
|
||||||
this.trackedTransactions = new List<ObservedWeightedTransaction<Transaction>>();
|
|
||||||
this.idle = true;
|
|
||||||
|
|
||||||
this.asyncEndedDelegate =
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(asyncEnded);
|
|
||||||
this.asyncProgressUpdatedDelegate =
|
|
||||||
new ObservedWeightedTransaction<Transaction>.ReportDelegate(asyncProgressChanged);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
|
||||||
public void Dispose() {
|
|
||||||
lock(this.trackedTransactions) {
|
|
||||||
|
|
||||||
// Get rid of all transactions we're tracking. This unsubscribes the
|
|
||||||
// observers from the events of the transactions and stops us from
|
|
||||||
// being kept alive and receiving any further events if some of the
|
|
||||||
// tracked transactions are still executing.
|
|
||||||
for(int index = 0; index < this.trackedTransactions.Count; ++index)
|
|
||||||
this.trackedTransactions[index].Dispose();
|
|
||||||
|
|
||||||
// Help the GC a bit by untangling the references :)
|
|
||||||
this.trackedTransactions.Clear();
|
|
||||||
this.trackedTransactions = null;
|
|
||||||
|
|
||||||
} // lock
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Begins tracking the specified background transactions</summary>
|
|
||||||
/// <param name="transaction">Background transaction to be tracked</param>
|
|
||||||
public void Track(Transaction transaction) {
|
|
||||||
Track(transaction, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Begins tracking the specified background transaction</summary>
|
|
||||||
/// <param name="transaction">Background transaction to be tracked</param>
|
|
||||||
/// <param name="weight">Weight to assign to this background transaction</param>
|
|
||||||
public void Track(Transaction transaction, float weight) {
|
|
||||||
|
|
||||||
// Add the new transaction into the tracking list. This has to be done
|
|
||||||
// inside a lock to prevent issues with the progressUpdate callback, which could
|
|
||||||
// access the totalWeight field before it has been updated to reflect the
|
|
||||||
// new transaction added to the collection.
|
|
||||||
lock(this.trackedTransactions) {
|
|
||||||
|
|
||||||
bool wasEmpty = (this.trackedTransactions.Count == 0);
|
|
||||||
|
|
||||||
if(transaction.Ended) {
|
|
||||||
|
|
||||||
// If the ended transaction would become the only transaction in the list,
|
|
||||||
// there's no sense in doing anything at all because it would have to be
|
|
||||||
// thrown right out again. Only add the transaction when there are other
|
|
||||||
// running transactions to properly sum total progress for consistency.
|
|
||||||
if(!wasEmpty) {
|
|
||||||
|
|
||||||
// Construct a new observation wrapper. This is done inside the lock
|
|
||||||
// because as soon as we are subscribed to the events, we can potentially
|
|
||||||
// receive them. The lock eliminates the risk of processing a progress update
|
|
||||||
// before the transaction has been added to the tracked transactions list.
|
|
||||||
this.trackedTransactions.Add(
|
|
||||||
new ObservedWeightedTransaction<Transaction>(
|
|
||||||
new WeightedTransaction<Transaction>(transaction, weight),
|
|
||||||
this.asyncProgressUpdatedDelegate,
|
|
||||||
this.asyncEndedDelegate
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} else { // Not ended -- Transaction is still running
|
|
||||||
|
|
||||||
// Construct a new transation observer and add the transaction to our
|
|
||||||
// list of tracked transactions.
|
|
||||||
ObservedWeightedTransaction<Transaction> observedTransaction =
|
|
||||||
new ObservedWeightedTransaction<Transaction>(
|
|
||||||
new WeightedTransaction<Transaction>(transaction, weight),
|
|
||||||
this.asyncProgressUpdatedDelegate,
|
|
||||||
this.asyncEndedDelegate
|
|
||||||
);
|
|
||||||
|
|
||||||
this.trackedTransactions.Add(observedTransaction);
|
|
||||||
|
|
||||||
// If this is the first transaction to be added to the list, tell our
|
|
||||||
// owner that we're idle no longer!
|
|
||||||
if(wasEmpty) {
|
|
||||||
setIdle(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // if transaction ended
|
|
||||||
|
|
||||||
// This can be done after we registered the wrapper to our delegates because
|
|
||||||
// any incoming progress updates will be stopped from the danger of a
|
|
||||||
// division-by-zero from the potentially still zeroed totalWeight by the lock.
|
|
||||||
this.totalWeight += weight;
|
|
||||||
|
|
||||||
// All done, the total progress is different now, so force a recalculation and
|
|
||||||
// send out the AsyncProgressUpdated event.
|
|
||||||
recalculateProgress();
|
|
||||||
|
|
||||||
} // lock
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Stops tracking the specified background transaction</summary>
|
|
||||||
/// <param name="transaction">Background transaction to stop tracking of</param>
|
|
||||||
public void Untrack(Transaction transaction) {
|
|
||||||
lock(this.trackedTransactions) {
|
|
||||||
|
|
||||||
// Locate the object to be untracked in our collection
|
|
||||||
int index;
|
|
||||||
for(index = 0; index < this.trackedTransactions.Count; ++index) {
|
|
||||||
bool same = ReferenceEquals(
|
|
||||||
transaction,
|
|
||||||
this.trackedTransactions[index].WeightedTransaction.Transaction
|
|
||||||
);
|
|
||||||
if(same) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(index == this.trackedTransactions.Count) {
|
|
||||||
throw new ArgumentException("Specified transaction is not being tracked");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove and dispose the transaction the user wants to untrack
|
|
||||||
{
|
|
||||||
ObservedWeightedTransaction<Transaction> wrappedTransaction =
|
|
||||||
this.trackedTransactions[index];
|
|
||||||
|
|
||||||
this.trackedTransactions.RemoveAt(index);
|
|
||||||
wrappedTransaction.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the list is empty, then we're back in the idle state
|
|
||||||
if(this.trackedTransactions.Count == 0) {
|
|
||||||
|
|
||||||
this.totalWeight = 0.0f;
|
|
||||||
|
|
||||||
// If we entered the idle state with this call, report the state change!
|
|
||||||
setIdle(true);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Rebuild the total weight from scratch. Subtracting the removed transaction's
|
|
||||||
// weight would work, too, but we might accumulate rounding errors making the sum
|
|
||||||
// drift slowly away from the actual value.
|
|
||||||
float newTotalWeight = 0.0f;
|
|
||||||
for(index = 0; index < this.trackedTransactions.Count; ++index)
|
|
||||||
newTotalWeight += this.trackedTransactions[index].WeightedTransaction.Weight;
|
|
||||||
|
|
||||||
this.totalWeight = newTotalWeight;
|
|
||||||
|
|
||||||
recalculateProgress();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // lock
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the tracker is currently idle</summary>
|
|
||||||
public bool Idle {
|
|
||||||
get { return this.idle; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Current summed progress of the tracked transactions</summary>
|
|
||||||
public float Progress {
|
|
||||||
get { return this.progress; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the AsyncIdleStateChanged event</summary>
|
|
||||||
/// <param name="idle">New idle state to report</param>
|
|
||||||
protected virtual void OnAsyncIdleStateChanged(bool idle) {
|
|
||||||
EventHandler<IdleStateEventArgs> copy = AsyncIdleStateChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, new IdleStateEventArgs(idle));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the AsyncProgressUpdated event</summary>
|
|
||||||
/// <param name="progress">New progress to report</param>
|
|
||||||
protected virtual void OnAsyncProgressUpdated(float progress) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Recalculates the total progress of the tracker</summary>
|
|
||||||
private void recalculateProgress() {
|
|
||||||
bool progressChanged = false;
|
|
||||||
|
|
||||||
// Lock the collection to avoid trouble when someone tries to remove one
|
|
||||||
// of our tracked transactions while we're just doing a progress update
|
|
||||||
lock(this.trackedTransactions) {
|
|
||||||
|
|
||||||
// This is a safety measure. In theory, even after all transactions have
|
|
||||||
// ended and the collection of tracked transactions is cleared, a waiting
|
|
||||||
// thread might deliver another progress update causing this method to
|
|
||||||
// be entered. In this case, the right thing is to do nothing at all.
|
|
||||||
if(this.totalWeight != 0.0f) {
|
|
||||||
float totalProgress = 0.0f;
|
|
||||||
|
|
||||||
// Sum up the total progress
|
|
||||||
for(int index = 0; index < this.trackedTransactions.Count; ++index) {
|
|
||||||
float weight = this.trackedTransactions[index].WeightedTransaction.Weight;
|
|
||||||
totalProgress += this.trackedTransactions[index].Progress * weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This also needs to be in the lock to guarantee that the total weight
|
|
||||||
// corresponds to the number of transactions we just summed -- by design,
|
|
||||||
// the total weight always has to be updated at the same time as the collection.
|
|
||||||
totalProgress /= this.totalWeight;
|
|
||||||
|
|
||||||
if(totalProgress != this.progress) {
|
|
||||||
this.progress = totalProgress;
|
|
||||||
progressChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // lock
|
|
||||||
|
|
||||||
// Finally, trigger the event if the progress has changed
|
|
||||||
if(progressChanged) {
|
|
||||||
OnAsyncProgressUpdated(this.progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when one of the tracked transactions has ended</summary>
|
|
||||||
private void asyncEnded() {
|
|
||||||
lock(this.trackedTransactions) {
|
|
||||||
|
|
||||||
// If any transactions in the list are still going, keep the entire list.
|
|
||||||
// This behavior is intentional in order to prevent the tracker's progress from
|
|
||||||
// jumping back repeatedly when multiple tracked transactions come to an end.
|
|
||||||
for(int index = 0; index < this.trackedTransactions.Count; ++index)
|
|
||||||
if(!this.trackedTransactions[index].WeightedTransaction.Transaction.Ended)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// All transactions have finished, get rid of the wrappers and make a
|
|
||||||
// fresh start for future transactions to be tracked. No need to call
|
|
||||||
// Dispose() since, as a matter of fact, when the transaction
|
|
||||||
this.trackedTransactions.Clear();
|
|
||||||
this.totalWeight = 0.0f;
|
|
||||||
|
|
||||||
// Notify our owner that we're idle now. This line is only reached when all
|
|
||||||
// transactions were finished, so it's safe to trigger this here.
|
|
||||||
setIdle(true);
|
|
||||||
|
|
||||||
} // lock
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when one of the tracked transactions has achieved progress</summary>
|
|
||||||
private void asyncProgressChanged() {
|
|
||||||
recalculateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Changes the idle state</summary>
|
|
||||||
/// <param name="idle">Whether or not the tracker is currently idle</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// This method expects to be called during a lock() on trackedTransactions!
|
|
||||||
/// </remarks>
|
|
||||||
private void setIdle(bool idle) {
|
|
||||||
this.idle = idle;
|
|
||||||
|
|
||||||
OnAsyncIdleStateChanged(idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the tracker is currently idle</summary>
|
|
||||||
private volatile bool idle;
|
|
||||||
/// <summary>Current summed progress of the tracked transactions</summary>
|
|
||||||
private volatile float progress;
|
|
||||||
/// <summary>Total weight of all transactions being tracked</summary>
|
|
||||||
private volatile float totalWeight;
|
|
||||||
/// <summary>Transactions being tracked by this tracker</summary>
|
|
||||||
private List<ObservedWeightedTransaction<Transaction>> trackedTransactions;
|
|
||||||
/// <summary>Delegate for the asyncEnded() method</summary>
|
|
||||||
private ObservedWeightedTransaction<Transaction>.ReportDelegate asyncEndedDelegate;
|
|
||||||
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
|
|
||||||
private ObservedWeightedTransaction<Transaction>.ReportDelegate asyncProgressUpdatedDelegate;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,134 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using Nuclex.Support.Scheduling;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the request class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class RequestTest {
|
|
||||||
|
|
||||||
#region class CustomWaitRequest
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request with a custom wait implementation that completes the request instead
|
|
||||||
/// of waiting for it complete by outside means
|
|
||||||
/// </summary>
|
|
||||||
private class CustomWaitRequest : Request {
|
|
||||||
|
|
||||||
/// <summary>Waits until the background process finishes</summary>
|
|
||||||
public override void Wait() {
|
|
||||||
// This could be a race condition if this was used for anything but this simple
|
|
||||||
// unit test. Might be neccessary to refactor this when writing advanced tests.
|
|
||||||
if(!base.Ended) {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class CustomWaitRequest
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the SucceededDummy request is in the ended state
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSucceededDummy() {
|
|
||||||
Request dummy = Request.SucceededDummy;
|
|
||||||
|
|
||||||
Assert.IsTrue(dummy.Ended);
|
|
||||||
dummy.Join(); // should not throw
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the FailedDummy request is in the ended state and throws
|
|
||||||
/// an exception when Join()ing
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestFailedDummy() {
|
|
||||||
Request failedDummy = Request.CreateFailedDummy(
|
|
||||||
new AbortedException("Hello World")
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsTrue(failedDummy.Ended);
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { failedDummy.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the Request's Wait() method is invoked if the request is joined
|
|
||||||
/// before the request has finished.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestJoinWithWaiting() {
|
|
||||||
CustomWaitRequest waitRequest = new CustomWaitRequest();
|
|
||||||
waitRequest.Join();
|
|
||||||
Assert.IsTrue(waitRequest.Ended);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the generic request class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class GenericRequestTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the SucceededDummy request is in the ended state
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSucceededDummy() {
|
|
||||||
Request<int> dummy = Request<int>.CreateSucceededDummy(12345);
|
|
||||||
|
|
||||||
Assert.IsTrue(dummy.Ended);
|
|
||||||
Assert.AreEqual(12345, dummy.Join()); // should not throw
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the FailedDummy request is in the ended state and throws
|
|
||||||
/// an exception when Join()ing
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestFailedDummy() {
|
|
||||||
Request<int> failedDummy = Request<int>.CreateFailedDummy(
|
|
||||||
new AbortedException("Hello World")
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.IsTrue(failedDummy.Ended);
|
|
||||||
Assert.Throws<AbortedException>(
|
|
||||||
delegate() { failedDummy.Join(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,217 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Asynchronous request running in the background</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// If the background process fails, the exception that caused it to fail is
|
|
||||||
/// communicated to all parties waiting on the Request through the Join()
|
|
||||||
/// method. Implementers should store any errors occuring in the asynchronous
|
|
||||||
/// parts of their code in a try..catch block (or avoid throwing and just
|
|
||||||
/// store a new exception) and re-throw them when in ReraiseExceptions()
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Like in the transaction class, the contract requires you to always call
|
|
||||||
/// OnAsyncEnded(), no matter what the outcome of your operation is.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public abstract class Request : Transaction {
|
|
||||||
|
|
||||||
#region class EndedDummyRequest
|
|
||||||
|
|
||||||
/// <summary>Dummy request that is always in the ended state</summary>
|
|
||||||
private class EndedDummyRequest : Request {
|
|
||||||
/// <summary>Creates a new successfully completed dummy request</summary>
|
|
||||||
public EndedDummyRequest() : this(null) { }
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="exception">Exception that caused the dummy to fail</param>
|
|
||||||
public EndedDummyRequest(Exception exception) {
|
|
||||||
this.exception = exception;
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override void ReraiseExceptions() {
|
|
||||||
if(this.exception != null)
|
|
||||||
throw this.exception;
|
|
||||||
}
|
|
||||||
/// <summary>Exception that supposedly caused the request to fail</summary>
|
|
||||||
private Exception exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // EndedDummyRequest
|
|
||||||
|
|
||||||
/// <summary>Succeeded dummy request</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Use to indicate success if the request has already been completed at
|
|
||||||
/// the time you are asked to perform it.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Request SucceededDummy = new EndedDummyRequest();
|
|
||||||
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="exception">Exception that supposedly caused the request to fail</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A failed request that reports the provided exception as cause for its failure
|
|
||||||
/// </returns>
|
|
||||||
public static Request CreateFailedDummy(Exception exception) {
|
|
||||||
return new EndedDummyRequest(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits for the background operation to end</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Any exceptions raised in the background operation will be thrown
|
|
||||||
/// in this method. If you decide to override this method, you should
|
|
||||||
/// call Wait() first (and let any possible exception through to your
|
|
||||||
/// caller).
|
|
||||||
/// </remarks>
|
|
||||||
public virtual void Join() {
|
|
||||||
|
|
||||||
// If the request itself hasn't ended yet, block the caller until it has.
|
|
||||||
// We could just use WaitHandle.WaitOne() here, but since the WaitHandle is created
|
|
||||||
// on-the-fly only when it is requested, we can avoid the WaitHandle creation in
|
|
||||||
// case the request is already finished!
|
|
||||||
if(!Ended)
|
|
||||||
Wait();
|
|
||||||
|
|
||||||
// Allow the implementer to throw an exception in case an error has occured
|
|
||||||
ReraiseExceptions();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void ReraiseExceptions() { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Request providing a result that can be passed to the caller</summary>
|
|
||||||
/// <typeparam name="ResultType">
|
|
||||||
/// Type of the result being provided by the request
|
|
||||||
/// </typeparam>
|
|
||||||
public abstract class Request<ResultType> : Request {
|
|
||||||
|
|
||||||
#region class SucceededDummyRequest
|
|
||||||
|
|
||||||
/// <summary>Succeeded dummy request that is always in the ended state</summary>
|
|
||||||
private class SucceededDummyRequest : Request<ResultType> {
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="result">Result to return to the request's caller</param>
|
|
||||||
public SucceededDummyRequest(ResultType result) {
|
|
||||||
this.result = result;
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override ResultType GatherResults() {
|
|
||||||
return this.result;
|
|
||||||
}
|
|
||||||
/// <summary>Results the succeede dummy request will provide to the caller</summary>
|
|
||||||
private ResultType result;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // SucceededDummyRequest
|
|
||||||
|
|
||||||
#region class FailedDummyRequest
|
|
||||||
|
|
||||||
/// <summary>Failed dummy request that is always in the ended state</summary>
|
|
||||||
private class FailedDummyRequest : Request<ResultType> {
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="exception">Exception that caused the dummy to fail</param>
|
|
||||||
public FailedDummyRequest(Exception exception) {
|
|
||||||
this.exception = exception;
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override ResultType GatherResults() {
|
|
||||||
throw this.exception;
|
|
||||||
}
|
|
||||||
/// <summary>Exception that supposedly caused the request to fail</summary>
|
|
||||||
private Exception exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // FailedDummyRequest
|
|
||||||
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="result">Result to provide to the caller</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A succeeded request that returns the provided result to the caller
|
|
||||||
/// </returns>
|
|
||||||
public static Request<ResultType> CreateSucceededDummy(ResultType result) {
|
|
||||||
return new SucceededDummyRequest(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a new failed dummy request</summary>
|
|
||||||
/// <param name="exception">Exception that supposedly caused the request to fail</param>
|
|
||||||
/// <returns>
|
|
||||||
/// A failed request that reports the provided exception as cause for its failure
|
|
||||||
/// </returns>
|
|
||||||
public static new Request<ResultType> CreateFailedDummy(Exception exception) {
|
|
||||||
return new FailedDummyRequest(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits for the background operation to end</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Any exceptions raised in the background operation will be thrown
|
|
||||||
/// in this method. If you decide to override this method, you should
|
|
||||||
/// call End() first (and let any possible exception through to your
|
|
||||||
/// caller).
|
|
||||||
/// </remarks>
|
|
||||||
public new ResultType Join() {
|
|
||||||
base.Join();
|
|
||||||
|
|
||||||
// Return the results of the request
|
|
||||||
return GatherResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request implementation to re-throw an exception if
|
|
||||||
/// the background process finished unsuccessfully
|
|
||||||
/// </summary>
|
|
||||||
protected override void ReraiseExceptions() {
|
|
||||||
// Request and discard the result, so the implementor can do all error handling
|
|
||||||
// in the GatherResults() method. This is a good default implementation as long
|
|
||||||
// as the returned object does not require IDispose. It if does, this method
|
|
||||||
// needs to be overridden.
|
|
||||||
GatherResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the specific request to return the results of the Request to the
|
|
||||||
/// caller of the Join() method
|
|
||||||
/// </summary>
|
|
||||||
protected abstract ResultType GatherResults();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,59 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the status report event argument container</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class StatusReportEventArgsTest {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the status report event arguments correctly reports an empty status
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEmptyStatus() {
|
|
||||||
StatusReportEventArgs emptyStatus = new StatusReportEventArgs(string.Empty);
|
|
||||||
|
|
||||||
Assert.AreEqual(string.Empty, emptyStatus.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the status report event arguments correctly reports simple
|
|
||||||
/// status indications
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSimpleStatus() {
|
|
||||||
StatusReportEventArgs emptyStatus = new StatusReportEventArgs("hello world");
|
|
||||||
|
|
||||||
Assert.AreEqual("hello world", emptyStatus.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,51 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Event arguments for reporting a status to the subscriber</summary>
|
|
||||||
public class StatusReportEventArgs : EventArgs {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new status report event arguments container</summary>
|
|
||||||
/// <param name="status">Status to report to the event's subscribers</param>
|
|
||||||
public StatusReportEventArgs(string status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The currently reported status</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The contents of this string are up to the publisher of the event to
|
|
||||||
/// define. Though it is recommended to report the status as a human-readable
|
|
||||||
/// string, these strings might not in all cases be properly localized or
|
|
||||||
/// suitable for display in a GUI.
|
|
||||||
/// </remarks>
|
|
||||||
public string Status {
|
|
||||||
get { return this.status; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Reported status</summary>
|
|
||||||
private string status;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,251 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NMock;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the transaction class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class TransactionTest {
|
|
||||||
|
|
||||||
#region interface ITransactionSubscriber
|
|
||||||
|
|
||||||
/// <summary>Interface used to test the transaction</summary>
|
|
||||||
public interface ITransactionSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Called when the set transaction has ended</summary>
|
|
||||||
/// <param name="sender">Transaction group that as ended</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
void Ended(object sender, EventArgs arguments);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // interface ITransactionGroupSubscriber
|
|
||||||
|
|
||||||
#region class TestTransaction
|
|
||||||
|
|
||||||
/// <summary>Transaction used for testing in this unit test</summary>
|
|
||||||
private class TestTransaction : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Transitions the transaction into the ended state</summary>
|
|
||||||
public void End() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestWiatable
|
|
||||||
|
|
||||||
#region class UnsubscribingTransaction
|
|
||||||
|
|
||||||
/// <summary>Transaction that unsubscribes during an event callback</summary>
|
|
||||||
private class UnsubscribingTransaction : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new unsubscribing transaction</summary>
|
|
||||||
/// <param name="transactionToMonitor">
|
|
||||||
/// Transaction whose AsyncEnded event will be monitored to trigger
|
|
||||||
/// the this transaction unsubscribing from the event.
|
|
||||||
/// </param>
|
|
||||||
public UnsubscribingTransaction(Transaction transactionToMonitor) {
|
|
||||||
this.transactionToMonitor = transactionToMonitor;
|
|
||||||
this.monitoredTransactionEndedDelegate = new EventHandler(
|
|
||||||
monitoredTransactionEnded
|
|
||||||
);
|
|
||||||
|
|
||||||
this.transactionToMonitor.AsyncEnded += this.monitoredTransactionEndedDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Called when the monitored transaction has ended</summary>
|
|
||||||
/// <param name="sender">Monitored transaction that has ended</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
private void monitoredTransactionEnded(object sender, EventArgs arguments) {
|
|
||||||
this.transactionToMonitor.AsyncEnded -= this.monitoredTransactionEndedDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transitions the transaction into the ended state</summary>
|
|
||||||
public void End() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transaction whose ending in being monitored</summary>
|
|
||||||
private Transaction transactionToMonitor;
|
|
||||||
/// <summary>Delegate to the monitoredTransactionEnded() method</summary>
|
|
||||||
private EventHandler monitoredTransactionEndedDelegate;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestWiatable
|
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() {
|
|
||||||
this.mockery = new MockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the transaction throws an exception when it is ended multiple times
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestThrowOnRepeatedlyEndedTransaction() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
test.End();
|
|
||||||
Assert.Throws<InvalidOperationException>(
|
|
||||||
delegate() { test.End(); }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the Ended event of the transaction is correctly delivered if
|
|
||||||
/// the transaction ends after the subscription already took place
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndedEventAfterSubscription() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
|
|
||||||
Mock<ITransactionSubscriber> mockedSubscriber = mockSubscriber(test);
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.Ended(null, null)).WithAnyArguments();
|
|
||||||
|
|
||||||
test.End();
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the Ended event of the transaction is correctly delivered if
|
|
||||||
/// the transaction is already done when the subscription takes place
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndedEventDuingSubscription() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
test.End();
|
|
||||||
|
|
||||||
Mock<ITransactionSubscriber> mockedSubscriber =
|
|
||||||
this.mockery.CreateMock<ITransactionSubscriber>();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.Ended(null, null)).WithAnyArguments();
|
|
||||||
|
|
||||||
test.AsyncEnded += new EventHandler(mockedSubscriber.MockObject.Ended);
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the Wait() method of the transaction works as expected
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWaitUnlimited() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
|
|
||||||
// We can only do a positive test here without slowing down the unit test
|
|
||||||
ThreadPool.QueueUserWorkItem(
|
|
||||||
(WaitCallback)delegate(object state) { Thread.Sleep(1); test.End(); }
|
|
||||||
);
|
|
||||||
|
|
||||||
test.Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the Wait() method of the transaction works as expected using
|
|
||||||
/// a millisecond count as its argument
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWaitMilliseconds() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
|
|
||||||
// Wait 0 milliseconds for the transaction to end. Of course, this will not happen,
|
|
||||||
// so a timeout occurs and false is returned
|
|
||||||
Assert.IsFalse(test.Wait(0));
|
|
||||||
|
|
||||||
test.End();
|
|
||||||
|
|
||||||
// Wait another 0 milliseconds for the transaction to end. Now it has already ended
|
|
||||||
// and no timeout will occur, even with a wait time of 0 milliseconds.
|
|
||||||
Assert.IsTrue(test.Wait(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the Wait() method of the transaction works as expected using
|
|
||||||
/// a TimeSpan as its argument
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWaitTimeSpan() {
|
|
||||||
TestTransaction test = new TestTransaction();
|
|
||||||
|
|
||||||
// Wait 0 milliseconds for the transaction to end. Of course, this will not happen,
|
|
||||||
// so a timeout occurs and false is returned
|
|
||||||
Assert.IsFalse(test.Wait(TimeSpan.Zero));
|
|
||||||
|
|
||||||
test.End();
|
|
||||||
|
|
||||||
// Wait another 0 milliseconds for the transaction to end. Now it has already ended
|
|
||||||
// and no timeout will occur, even with a wait time of 0 milliseconds.
|
|
||||||
Assert.IsTrue(test.Wait(TimeSpan.Zero));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that no error occurs when an even subscriber to the AsyncEnded event
|
|
||||||
/// unsubscribes in the event callback handler
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestUnsubscribeInEndedCallback() {
|
|
||||||
TestTransaction monitored = new TestTransaction();
|
|
||||||
UnsubscribingTransaction test = new UnsubscribingTransaction(monitored);
|
|
||||||
|
|
||||||
Mock<ITransactionSubscriber> mockedSubscriber = mockSubscriber(monitored);
|
|
||||||
|
|
||||||
try {
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.Ended(null, null)).WithAnyArguments();
|
|
||||||
monitored.End();
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
test.End();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mocks a subscriber for the events of a transaction</summary>
|
|
||||||
/// <param name="transaction">Transaction to mock an event subscriber for</param>
|
|
||||||
/// <returns>The mocked event subscriber</returns>
|
|
||||||
private Mock<ITransactionSubscriber> mockSubscriber(Transaction transaction) {
|
|
||||||
Mock<ITransactionSubscriber> mockedSubscriber =
|
|
||||||
this.mockery.CreateMock<ITransactionSubscriber>();
|
|
||||||
|
|
||||||
transaction.AsyncEnded += new EventHandler(mockedSubscriber.MockObject.Ended);
|
|
||||||
|
|
||||||
return mockedSubscriber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mock object factory</summary>
|
|
||||||
private MockFactory mockery;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,269 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Base class for background processes the user can wait on</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// By encapsulating long-running operations which will ideally be running in
|
|
||||||
/// a background thread in a class that's derived from <see cref="Transaction" />
|
|
||||||
/// you can wait for the completion of the operation and optionally even receive
|
|
||||||
/// feedback on the achieved progress. This is useful for displaying a progress
|
|
||||||
/// bar, loading screen or some other means of entertaining the user while he
|
|
||||||
/// waits for the task to complete.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// You can register callbacks which will be fired once the <see cref="Transaction" />
|
|
||||||
/// task has completed. This class deliberately does not provide an Execute()
|
|
||||||
/// method or anything similar to clearly seperate the initiation of an operation
|
|
||||||
/// from just monitoring it. By omitting an Execute() method, it also becomes
|
|
||||||
/// possible to construct a transaction just-in-time when it is explicitely being
|
|
||||||
/// asked for.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
public abstract class Transaction {
|
|
||||||
|
|
||||||
#region class EndedDummyTransaction
|
|
||||||
|
|
||||||
/// <summary>Dummy transaction which always is in the 'ended' state</summary>
|
|
||||||
private class EndedDummyTransaction : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new ended dummy transaction</summary>
|
|
||||||
public EndedDummyTransaction() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class EndedDummyTransaction
|
|
||||||
|
|
||||||
/// <summary>A dummy transaction that's always in the 'ended' state</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Useful if an operation is already complete when it's being asked for or
|
|
||||||
/// when a transaction that's lazily created is accessed after the original
|
|
||||||
/// operation has ended already.
|
|
||||||
/// </remarks>
|
|
||||||
public static readonly Transaction EndedDummy = new EndedDummyTransaction();
|
|
||||||
|
|
||||||
/// <summary>Will be triggered when the transaction has ended</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// If the process is already finished when a client registers to this event,
|
|
||||||
/// the registered callback will be invoked synchronously right when the
|
|
||||||
/// registration takes place.
|
|
||||||
/// </remarks>
|
|
||||||
public virtual event EventHandler AsyncEnded {
|
|
||||||
add {
|
|
||||||
|
|
||||||
// If the background process has not yet ended, add the delegate to the
|
|
||||||
// list of subscribers. This uses the double-checked locking idiom to
|
|
||||||
// avoid taking the lock when the background process has already ended.
|
|
||||||
if(!this.ended) {
|
|
||||||
lock(this) {
|
|
||||||
if(!this.ended) {
|
|
||||||
|
|
||||||
// The subscriber list is also created lazily ;-)
|
|
||||||
if(ReferenceEquals(this.endedEventSubscribers, null)) {
|
|
||||||
this.endedEventSubscribers = new List<EventHandler>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe the event handler to the list
|
|
||||||
this.endedEventSubscribers.Add(value);
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this point is reached, the background process was already finished
|
|
||||||
// and we have to invoke the subscriber manually as promised.
|
|
||||||
value(this, EventArgs.Empty);
|
|
||||||
|
|
||||||
}
|
|
||||||
remove {
|
|
||||||
|
|
||||||
if(!this.ended) {
|
|
||||||
lock(this) {
|
|
||||||
if(!this.ended) {
|
|
||||||
|
|
||||||
// Only try to remove the event handler if the subscriber list was created,
|
|
||||||
// otherwise, we can be sure that no actual subscribers exist.
|
|
||||||
if(!ReferenceEquals(this.endedEventSubscribers, null)) {
|
|
||||||
int eventHandlerIndex = this.endedEventSubscribers.IndexOf(value);
|
|
||||||
|
|
||||||
// Unsubscribing a non-subscribed delegate from an event is allowed and
|
|
||||||
// should not throw an exception.
|
|
||||||
if(eventHandlerIndex != -1) {
|
|
||||||
this.endedEventSubscribers.RemoveAt(eventHandlerIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Waits until the background process finishes</summary>
|
|
||||||
public virtual void Wait() {
|
|
||||||
if(!this.ended) {
|
|
||||||
WaitHandle.WaitOne();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if WINDOWS
|
|
||||||
|
|
||||||
/// <summary>Waits until the background process finishes or a timeout occurs</summary>
|
|
||||||
/// <param name="timeout">
|
|
||||||
/// Time span after which to stop waiting and return immediately
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the background process completed, false if the timeout was reached
|
|
||||||
/// </returns>
|
|
||||||
public virtual bool Wait(TimeSpan timeout) {
|
|
||||||
if(this.ended) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WaitHandle.WaitOne(timeout, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // WINDOWS
|
|
||||||
|
|
||||||
/// <summary>Waits until the background process finishes or a timeout occurs</summary>
|
|
||||||
/// <param name="timeoutMilliseconds">
|
|
||||||
/// Number of milliseconds after which to stop waiting and return immediately
|
|
||||||
/// </param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the background process completed, false if the timeout was reached
|
|
||||||
/// </returns>
|
|
||||||
public virtual bool Wait(int timeoutMilliseconds) {
|
|
||||||
if(this.ended) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if WINDOWS
|
|
||||||
return WaitHandle.WaitOne(timeoutMilliseconds, false);
|
|
||||||
#else
|
|
||||||
return WaitHandle.WaitOne(timeoutMilliseconds);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the transaction has ended already</summary>
|
|
||||||
public virtual bool Ended {
|
|
||||||
get { return this.ended; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>WaitHandle that can be used to wait for the transaction to end</summary>
|
|
||||||
public virtual WaitHandle WaitHandle {
|
|
||||||
get {
|
|
||||||
|
|
||||||
// The WaitHandle will only be created when someone asks for it!
|
|
||||||
// We can *not* optimize this lock away since we absolutely must not create
|
|
||||||
// two doneEvents -- someone might call .WaitOne() on the first one when only
|
|
||||||
// the second one is referenced by this.doneEvent and thus gets set in the end.
|
|
||||||
if(this.doneEvent == null) {
|
|
||||||
lock(this) {
|
|
||||||
if(this.doneEvent == null) {
|
|
||||||
this.doneEvent = new ManualResetEvent(this.ended);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can be sure the doneEvent has been created now!
|
|
||||||
return this.doneEvent;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the AsyncEnded event</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// This event should be fired by the implementing class when its work is completed.
|
|
||||||
/// It's of no interest to this class whether the outcome of the process was
|
|
||||||
/// successfull or not, the outcome and results of the process taking place both
|
|
||||||
/// need to be communicated seperately.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// Calling this method is mandatory. Implementers need to take care that
|
|
||||||
/// the OnAsyncEnded() method is called on any instance of transaction that's
|
|
||||||
/// being created. This method also must not be called more than once.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncEnded() {
|
|
||||||
|
|
||||||
// Make sure the transaction is not ended more than once. By guaranteeing that
|
|
||||||
// a transaction can only be ended once, we allow users of this class to
|
|
||||||
// skip some safeguards against notifications arriving twice.
|
|
||||||
lock(this) {
|
|
||||||
|
|
||||||
// No double lock here, this is an exception that indicates an implementation
|
|
||||||
// error that will not be triggered under normal circumstances. We don't need
|
|
||||||
// to waste any effort optimizing the speed at which an implementation fault
|
|
||||||
// will be reported ;-)
|
|
||||||
if(this.ended)
|
|
||||||
throw new InvalidOperationException("The transaction has already been ended");
|
|
||||||
|
|
||||||
this.ended = true;
|
|
||||||
|
|
||||||
// Doesn't really need a lock: if another thread wins the race and creates
|
|
||||||
// the event after we just saw it being null, it would be created in an already
|
|
||||||
// set state due to the ended flag (see above) being set to true beforehand!
|
|
||||||
// But since we've got a lock ready, we can even avoid that 1 in a million
|
|
||||||
// performance loss and prevent the doneEvent from being signalled needlessly.
|
|
||||||
if(this.doneEvent != null)
|
|
||||||
this.doneEvent.Set();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire the ended events to all event subscribers. We can freely use the list
|
|
||||||
// without synchronization at this point on since once this.ended is set to true,
|
|
||||||
// the subscribers list will not be accessed any longer
|
|
||||||
if(!ReferenceEquals(this.endedEventSubscribers, null)) {
|
|
||||||
for(int index = 0; index < this.endedEventSubscribers.Count; ++index) {
|
|
||||||
this.endedEventSubscribers[index](this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
this.endedEventSubscribers = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Event handlers which have subscribed to the ended event</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Does not need to be volatile since it's only accessed inside
|
|
||||||
/// </remarks>
|
|
||||||
protected volatile List<EventHandler> endedEventSubscribers;
|
|
||||||
/// <summary>Whether the operation has completed yet</summary>
|
|
||||||
protected volatile bool ended;
|
|
||||||
/// <summary>Event that will be set when the transaction is completed</summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This event is will only be created when it is specifically asked for using
|
|
||||||
/// the WaitHandle property.
|
|
||||||
/// </remarks>
|
|
||||||
protected volatile ManualResetEvent doneEvent;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,386 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
#if UNITTEST
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NMock;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the transaction group class</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class TransactionGroupTest {
|
|
||||||
|
|
||||||
#region interface ITransactionGroupSubscriber
|
|
||||||
|
|
||||||
/// <summary>Interface used to test the transaction group</summary>
|
|
||||||
public interface ITransactionGroupSubscriber {
|
|
||||||
|
|
||||||
/// <summary>Called when the transaction group's progress changes</summary>
|
|
||||||
/// <param name="sender">Transaction group whose progress has changed</param>
|
|
||||||
/// <param name="arguments">Contains the new progress achieved</param>
|
|
||||||
void ProgressChanged(object sender, ProgressReportEventArgs arguments);
|
|
||||||
|
|
||||||
/// <summary>Called when the transaction group has ended</summary>
|
|
||||||
/// <param name="sender">Transaction group that as ended</param>
|
|
||||||
/// <param name="arguments">Not used</param>
|
|
||||||
void Ended(object sender, EventArgs arguments);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // interface ITransactionGroupSubscriber
|
|
||||||
|
|
||||||
#region class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
/// <summary>Compares two ProgressUpdateEventArgsInstances for NMock validation</summary>
|
|
||||||
private class ProgressUpdateEventArgsMatcher : Matcher {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new ProgressUpdateEventArgsMatcher </summary>
|
|
||||||
/// <param name="expected">Expected progress update event arguments</param>
|
|
||||||
public ProgressUpdateEventArgsMatcher(ProgressReportEventArgs expected) {
|
|
||||||
this.expected = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by NMock to verfiy the ProgressUpdateEventArgs match the expected value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actualAsObject">Actual value to compare to the expected value</param>
|
|
||||||
/// <returns>
|
|
||||||
/// True if the actual value matches the expected value; otherwise false
|
|
||||||
/// </returns>
|
|
||||||
public override bool Matches(object actualAsObject) {
|
|
||||||
ProgressReportEventArgs actual = (actualAsObject as ProgressReportEventArgs);
|
|
||||||
if(actual == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return (actual.Progress == this.expected.Progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates a string representation of the expected value</summary>
|
|
||||||
/// <param name="writer">Writer to write the string representation into</param>
|
|
||||||
public override void DescribeTo(TextWriter writer) {
|
|
||||||
writer.Write(this.expected.Progress.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Expected progress update event args value</summary>
|
|
||||||
private ProgressReportEventArgs expected;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class ProgressUpdateEventArgsMatcher
|
|
||||||
|
|
||||||
#region class TestTransaction
|
|
||||||
|
|
||||||
/// <summary>Transaction used for testing in this unit test</summary>
|
|
||||||
private class TestTransaction : Transaction, IProgressReporter {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Changes the testing transaction's indicated progress</summary>
|
|
||||||
/// <param name="progress">
|
|
||||||
/// New progress to be reported by the testing transaction
|
|
||||||
/// </param>
|
|
||||||
public void ChangeProgress(float progress) {
|
|
||||||
OnAsyncProgressChanged(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transitions the transaction into the ended state</summary>
|
|
||||||
public void End() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="progress">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(float progress) {
|
|
||||||
OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="eventArguments">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// Allows for classes derived from the transaction class to easily provide
|
|
||||||
/// a custom event arguments class that has been derived from the
|
|
||||||
/// transaction's ProgressUpdateEventArgs class.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, eventArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class TestTransaction
|
|
||||||
|
|
||||||
#region class ChainEndingTransaction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transaction that ends another transaction when its Ended property is called
|
|
||||||
/// </summary>
|
|
||||||
private class ChainEndingTransaction : Transaction {
|
|
||||||
|
|
||||||
/// <summary>Initializes a new chain ending transaction</summary>
|
|
||||||
public ChainEndingTransaction() {
|
|
||||||
this.chainedTransaction = new TestTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transitions the transaction into the ended state</summary>
|
|
||||||
public void End() {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transaction that will end when this transaction's ended property is accessed
|
|
||||||
/// </summary>
|
|
||||||
public TestTransaction ChainedTransaction {
|
|
||||||
get { return this.chainedTransaction; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the transaction has ended already</summary>
|
|
||||||
public override bool Ended {
|
|
||||||
get {
|
|
||||||
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
|
|
||||||
this.chainedTransaction.End();
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.Ended;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transaction that will end when this transaction's ended property is accessed
|
|
||||||
/// </summary>
|
|
||||||
private TestTransaction chainedTransaction;
|
|
||||||
|
|
||||||
/// <summary>Whether we already ended the chained transaction and ourselves</summary>
|
|
||||||
private int endedCalled;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion // class ChainEndingTransaction
|
|
||||||
|
|
||||||
/// <summary>Initialization routine executed before each test is run</summary>
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() {
|
|
||||||
this.mockery = new MockFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Validates that the transaction group correctly sums the progress</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestSummedProgress() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<TestTransaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<TestTransaction>(
|
|
||||||
new TestTransaction[] { new TestTransaction(), new TestTransaction() }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Mock<ITransactionGroupSubscriber> mockedSubscriber = mockSubscriber(testTransactionGroup);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(TransactionGroup<TestTransaction>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.25f))
|
|
||||||
);
|
|
||||||
|
|
||||||
testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Validates that the transaction group respects the weights</summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWeightedSummedProgress() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<TestTransaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<TestTransaction>(
|
|
||||||
new WeightedTransaction<TestTransaction>[] {
|
|
||||||
new WeightedTransaction<TestTransaction>(new TestTransaction(), 1.0f),
|
|
||||||
new WeightedTransaction<TestTransaction>(new TestTransaction(), 2.0f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Mock<ITransactionGroupSubscriber> mockedSubscriber = mockSubscriber(testTransactionGroup);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(TransactionGroup<TestTransaction>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f / 3.0f))
|
|
||||||
);
|
|
||||||
|
|
||||||
testTransactionGroup.Children[0].Transaction.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(m => m.ProgressChanged(null, null)).With(
|
|
||||||
new NMock.Matchers.TypeMatcher(typeof(TransactionGroup<TestTransaction>)),
|
|
||||||
new ProgressUpdateEventArgsMatcher(new ProgressReportEventArgs(0.5f))
|
|
||||||
);
|
|
||||||
|
|
||||||
testTransactionGroup.Children[1].Transaction.ChangeProgress(0.5f);
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the ended event is triggered when the last transaction out of
|
|
||||||
/// multiple transactions in the group ends.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndedEventWithTwoTransactions() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<TestTransaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<TestTransaction>(
|
|
||||||
new TestTransaction[] { new TestTransaction(), new TestTransaction() }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Mock<ITransactionGroupSubscriber> mockedSubscriber = mockSubscriber(testTransactionGroup);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.Exactly(2).Method(
|
|
||||||
m => m.ProgressChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.Ended(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
testTransactionGroup.Children[0].Transaction.End();
|
|
||||||
testTransactionGroup.Children[1].Transaction.End();
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that the ended event is triggered when a single transaction contained
|
|
||||||
/// in the group ends.
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestEndedEventWithSingleTransaction() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<TestTransaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<TestTransaction>(
|
|
||||||
new TestTransaction[] { new TestTransaction() }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Mock<ITransactionGroupSubscriber> mockedSubscriber = mockSubscriber(testTransactionGroup);
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.ProgressChanged(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
mockedSubscriber.Expects.One.Method(
|
|
||||||
m => m.Ended(null, null)
|
|
||||||
).WithAnyArguments();
|
|
||||||
|
|
||||||
|
|
||||||
testTransactionGroup.Children[0].Transaction.End();
|
|
||||||
|
|
||||||
this.mockery.VerifyAllExpectationsHaveBeenMet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the transaction group immediately enters the ended state when
|
|
||||||
/// the contained transactions have already ended before the constructor
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This was a bug at one time and should prevent a regression
|
|
||||||
/// </remarks>
|
|
||||||
[Test]
|
|
||||||
public void TestAlreadyEndedTransactions() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<Transaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<Transaction>(
|
|
||||||
new Transaction[] { Transaction.EndedDummy, Transaction.EndedDummy }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Assert.IsTrue(testTransactionGroup.Wait(1000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that the transaction group doesn't think it's already ended when
|
|
||||||
/// the first transaction being added is in the ended state
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This was a bug at one time and should prevent a regression
|
|
||||||
/// </remarks>
|
|
||||||
[Test]
|
|
||||||
public void TestAlreadyEndedTransactionAsFirstTransaction() {
|
|
||||||
using(
|
|
||||||
TransactionGroup<Transaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<Transaction>(
|
|
||||||
new Transaction[] { Transaction.EndedDummy, new TestTransaction() }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Assert.IsFalse(testTransactionGroup.Ended);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies that a transaction ending while the constructor is running doesn't
|
|
||||||
/// wreak havoc on the transaction group
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTransactionEndingDuringConstructor() {
|
|
||||||
ChainEndingTransaction chainTransaction = new ChainEndingTransaction();
|
|
||||||
using(
|
|
||||||
TransactionGroup<Transaction> testTransactionGroup =
|
|
||||||
new TransactionGroup<Transaction>(
|
|
||||||
new Transaction[] { chainTransaction.ChainedTransaction, chainTransaction }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Assert.IsFalse(testTransactionGroup.Ended);
|
|
||||||
chainTransaction.End();
|
|
||||||
Assert.IsTrue(testTransactionGroup.Ended);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mocks a subscriber for the events of a transaction</summary>
|
|
||||||
/// <param name="transaction">Transaction to mock an event subscriber for</param>
|
|
||||||
/// <returns>The mocked event subscriber</returns>
|
|
||||||
private Mock<ITransactionGroupSubscriber> mockSubscriber(Transaction transaction) {
|
|
||||||
Mock<ITransactionGroupSubscriber> mockedSubscriber =
|
|
||||||
this.mockery.CreateMock<ITransactionGroupSubscriber>();
|
|
||||||
|
|
||||||
transaction.AsyncEnded += new EventHandler(mockedSubscriber.MockObject.Ended);
|
|
||||||
(transaction as IProgressReporter).AsyncProgressChanged +=
|
|
||||||
new EventHandler<ProgressReportEventArgs>(mockedSubscriber.MockObject.ProgressChanged);
|
|
||||||
|
|
||||||
return mockedSubscriber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Mock object factory</summary>
|
|
||||||
private MockFactory mockery;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,235 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using Nuclex.Support.Collections;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Forms a single transaction from a group of transactions</summary>
|
|
||||||
/// <typeparam name="TransactionType">Type of transactions to manage as a set</typeparam>
|
|
||||||
public class TransactionGroup<TransactionType> : Transaction, IDisposable, IProgressReporter
|
|
||||||
where TransactionType : Transaction {
|
|
||||||
|
|
||||||
/// <summary>will be triggered to report when progress has been achieved</summary>
|
|
||||||
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
|
|
||||||
|
|
||||||
/// <summary>Initializes a new transaction group</summary>
|
|
||||||
/// <param name="children">Transactions to track with this group</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Uses a default weighting factor of 1.0 for all transactions.
|
|
||||||
/// </remarks>
|
|
||||||
public TransactionGroup(IEnumerable<TransactionType> children) {
|
|
||||||
List<ObservedWeightedTransaction<TransactionType>> childrenList =
|
|
||||||
new List<ObservedWeightedTransaction<TransactionType>>();
|
|
||||||
|
|
||||||
// Construct a WeightedTransaction with the default weight for each
|
|
||||||
// transaction and wrap it in an ObservedTransaction
|
|
||||||
foreach(TransactionType transaction in children) {
|
|
||||||
childrenList.Add(
|
|
||||||
new ObservedWeightedTransaction<TransactionType>(
|
|
||||||
new WeightedTransaction<TransactionType>(transaction),
|
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
|
||||||
asyncProgressUpdated
|
|
||||||
),
|
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
|
||||||
asyncChildEnded
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since all transactions have a weight of 1.0, the total weight is
|
|
||||||
// equal to the number of transactions in our list
|
|
||||||
this.totalWeight = (float)childrenList.Count;
|
|
||||||
// Thread.MemoryBarrier(); // not needed because children is volatile
|
|
||||||
this.children = childrenList;
|
|
||||||
|
|
||||||
// Any asyncEnded events being receiving from the transactions until now
|
|
||||||
// would have been ignored, so we need to check again here
|
|
||||||
asyncChildEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Initializes a new transaction group</summary>
|
|
||||||
/// <param name="children">Transactions to track with this group</param>
|
|
||||||
public TransactionGroup(
|
|
||||||
IEnumerable<WeightedTransaction<TransactionType>> children
|
|
||||||
) {
|
|
||||||
List<ObservedWeightedTransaction<TransactionType>> childrenList =
|
|
||||||
new List<ObservedWeightedTransaction<TransactionType>>();
|
|
||||||
|
|
||||||
// Construct an ObservedTransaction around each of the WeightedTransactions
|
|
||||||
foreach(WeightedTransaction<TransactionType> transaction in children) {
|
|
||||||
childrenList.Add(
|
|
||||||
new ObservedWeightedTransaction<TransactionType>(
|
|
||||||
transaction,
|
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
|
||||||
asyncProgressUpdated
|
|
||||||
),
|
|
||||||
new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
|
|
||||||
asyncChildEnded
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sum up the total weight
|
|
||||||
this.totalWeight += transaction.Weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.children = childrenList;
|
|
||||||
|
|
||||||
// Any asyncEnded events being receiving from the transactions until now
|
|
||||||
// would have been ignored, so we need to check again here
|
|
||||||
asyncChildEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Immediately releases all resources owned by the object</summary>
|
|
||||||
public void Dispose() {
|
|
||||||
|
|
||||||
if(this.children != null) {
|
|
||||||
|
|
||||||
// Dispose all the observed transactions, disconnecting the events from the
|
|
||||||
// actual transactions so the GC can more easily collect this class
|
|
||||||
for(int index = 0; index < this.children.Count; ++index)
|
|
||||||
this.children[index].Dispose();
|
|
||||||
|
|
||||||
this.children = null;
|
|
||||||
this.wrapper = null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Childs contained in the transaction set</summary>
|
|
||||||
public IList<WeightedTransaction<TransactionType>> Children {
|
|
||||||
get {
|
|
||||||
|
|
||||||
// The wrapper is constructed only when needed. Most of the time, users will
|
|
||||||
// just create a transaction group and monitor its progress without ever using
|
|
||||||
// the Childs collection.
|
|
||||||
if(this.wrapper == null) {
|
|
||||||
|
|
||||||
// This doesn't need a lock because it's a stateless wrapper.
|
|
||||||
// If it is constructed twice, then so be it, no problem at all.
|
|
||||||
this.wrapper = new WeightedTransactionWrapperCollection<TransactionType>(
|
|
||||||
this.children
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.wrapper;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="progress">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(float progress) {
|
|
||||||
OnAsyncProgressChanged(new ProgressReportEventArgs(progress));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Fires the progress update event</summary>
|
|
||||||
/// <param name="eventArguments">Progress to report (ranging from 0.0 to 1.0)</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// Informs the observers of this transaction about the achieved progress.
|
|
||||||
/// Allows for classes derived from the transaction class to easily provide
|
|
||||||
/// a custom event arguments class that has been derived from the
|
|
||||||
/// transaction's ProgressUpdateEventArgs class.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual void OnAsyncProgressChanged(ProgressReportEventArgs eventArguments) {
|
|
||||||
EventHandler<ProgressReportEventArgs> copy = AsyncProgressChanged;
|
|
||||||
if(copy != null)
|
|
||||||
copy(this, eventArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the progress of one of the observed transactions changes
|
|
||||||
/// </summary>
|
|
||||||
private void asyncProgressUpdated() {
|
|
||||||
if(this.children == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float totalProgress = 0.0f;
|
|
||||||
|
|
||||||
// Calculate the sum of the progress reported by our child transactions,
|
|
||||||
// scaled to the weight each transaction has assigned to it.
|
|
||||||
for(int index = 0; index < this.children.Count; ++index) {
|
|
||||||
totalProgress +=
|
|
||||||
this.children[index].Progress * this.children[index].WeightedTransaction.Weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the actual combined progress
|
|
||||||
if(this.totalWeight > 0.0f)
|
|
||||||
totalProgress /= this.totalWeight;
|
|
||||||
|
|
||||||
// Send out the progress update
|
|
||||||
OnAsyncProgressChanged(totalProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when an observed transaction ends
|
|
||||||
/// </summary>
|
|
||||||
private void asyncChildEnded() {
|
|
||||||
|
|
||||||
// If a transaction reports its end durign the constructor, it will end up here
|
|
||||||
// where the collection has not been assigned yet, allowing us to skip the
|
|
||||||
// check until all transactions are there (otherwise, we might invoke
|
|
||||||
// OnAsyncended() early, because all transactions in the list seem to have ended
|
|
||||||
// despite the fact that the constructor hasn't finished adding transactions yet)
|
|
||||||
if(this.children == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's still at least one transaction going, don't report that
|
|
||||||
// the transaction group has finished yet.
|
|
||||||
for(int index = 0; index < this.children.Count; ++index)
|
|
||||||
if(!this.children[index].WeightedTransaction.Transaction.Ended)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// All child transactions have ended, so the set has now ended as well
|
|
||||||
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
|
|
||||||
OnAsyncEnded();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transactions being managed in the set</summary>
|
|
||||||
private volatile List<ObservedWeightedTransaction<TransactionType>> children;
|
|
||||||
/// <summary>
|
|
||||||
/// Wrapper collection for exposing the child transactions under the
|
|
||||||
/// WeightedTransaction interface
|
|
||||||
/// </summary>
|
|
||||||
private volatile WeightedTransactionWrapperCollection<TransactionType> wrapper;
|
|
||||||
/// <summary>Summed weight of all transactions in the set</summary>
|
|
||||||
private float totalWeight;
|
|
||||||
/// <summary>Whether we already called OnAsyncEnded</summary>
|
|
||||||
private int endedCalled;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
|
@ -1,87 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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.IO;
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Unit Test for the weighted transaction wrapper</summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class WeightedTransactionTest {
|
|
||||||
|
|
||||||
#region class TestTransaction
|
|
||||||
|
|
||||||
/// <summary>Transaction used for testing in this unit test</summary>
|
|
||||||
private class TestTransaction : Transaction { }
|
|
||||||
|
|
||||||
#endregion // class TestTransaction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the weighted transaction wrapper correctly stores the transaction
|
|
||||||
/// it was given in the constructor
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestTransactionStorage() {
|
|
||||||
TestTransaction transaction = new TestTransaction();
|
|
||||||
WeightedTransaction<Transaction> testWrapper = new WeightedTransaction<Transaction>(
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.AreSame(transaction, testWrapper.Transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the weighted transaction wrapper correctly applies the default
|
|
||||||
/// unit weight to the transaction if no explicit weight was specified
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestDefaultWeight() {
|
|
||||||
TestTransaction transaction = new TestTransaction();
|
|
||||||
WeightedTransaction<Transaction> testWrapper = new WeightedTransaction<Transaction>(
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.AreEqual(1.0f, testWrapper.Weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tests whether the weighted transaction wrapper correctly stores the weight
|
|
||||||
/// it was given in the constructor
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void TestWeightStorage() {
|
|
||||||
TestTransaction transaction = new TestTransaction();
|
|
||||||
WeightedTransaction<Transaction> testWrapper = new WeightedTransaction<Transaction>(
|
|
||||||
transaction, 12.0f
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.AreEqual(12.0f, testWrapper.Weight);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
||||||
|
|
||||||
#endif // UNITTEST
|
|
|
@ -1,60 +0,0 @@
|
||||||
#region CPL License
|
|
||||||
/*
|
|
||||||
Nuclex Framework
|
|
||||||
Copyright (C) 2002-2010 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
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Nuclex.Support.Tracking {
|
|
||||||
|
|
||||||
/// <summary>Transaction with an associated weight for the total progress</summary>
|
|
||||||
public class WeightedTransaction<TransactionType> where TransactionType : Transaction {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new weighted transaction with a default weight of 1.0
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="transaction">Transaction whose progress to monitor</param>
|
|
||||||
public WeightedTransaction(TransactionType transaction) : this(transaction, 1.0f) { }
|
|
||||||
|
|
||||||
/// <summary>Initializes a new weighted transaction</summary>
|
|
||||||
/// <param name="transaction">transaction whose progress to monitor</param>
|
|
||||||
/// <param name="weight">Weighting of the transaction's progress</param>
|
|
||||||
public WeightedTransaction(TransactionType transaction, float weight) {
|
|
||||||
this.transaction = transaction;
|
|
||||||
this.weight = weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transaction being wrapped by this weighted transaction</summary>
|
|
||||||
public TransactionType Transaction {
|
|
||||||
get { return this.transaction; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The contribution of this transaction to the total progress</summary>
|
|
||||||
public float Weight {
|
|
||||||
get { return this.weight; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Transaction whose progress we're tracking</summary>
|
|
||||||
private TransactionType transaction;
|
|
||||||
/// <summary>Weighting of this transaction in the total progress</summary>
|
|
||||||
private float weight;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Tracking
|
|
Loading…
Reference in New Issue
Block a user