From 147cc92ac554e294ad2ac2a69336bf19c8cdabfc Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Apr 2015 16:28:42 +1000 Subject: [PATCH] Updates BackgroundTaskRunner & tests: removes the need for most Thread.Sleep which is error prone, this is done by updating the GetAwaiter() method to be a custom awaiter so we can await the entire background task running operations. We then add a property called CurrentThraedingTask which then allows awaiting the current Task instance. Adds error handling to all event handlers for the task runner. Changes events to be TypedEventHandlers --- .../Scheduling/BackgroundTaskRunnerTests.cs | 193 ++++++++++-------- .../Scheduling/BackgroundTaskRunner.cs | 116 ++++++++--- .../Scheduling/BackgroundTaskRunnerAwaiter.cs | 65 ++++++ .../Scheduling/ThreadingTaskAwaiter.cs | 42 ++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 5 files changed, 300 insertions(+), 118 deletions(-) create mode 100644 src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs create mode 100644 src/Umbraco.Web/Scheduling/ThreadingTaskAwaiter.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index ab83294496..8bc70fd137 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Tests.TestHelpers; using Umbraco.Web.Scheduling; namespace Umbraco.Tests.Scheduling @@ -13,20 +14,14 @@ namespace Umbraco.Tests.Scheduling [TestFixture] public class BackgroundTaskRunnerTests { - private static void AssertRunnerStopsRunning(BackgroundTaskRunner runner, int timeoutMilliseconds = 2000) - where T : class, IBackgroundTask + [TestFixtureSetUp] + public void InitializeFixture() { - const int period = 200; - - var i = 0; - var m = timeoutMilliseconds/period; - while (runner.IsRunning && i++ < m) - Thread.Sleep(period); - Assert.IsFalse(runner.IsRunning, "Runner is still running."); + TestHelper.SetupLog4NetForTests(); } [Test] - public void ShutdownWaitWhenRunning() + public async void ShutdownWaitWhenRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) { @@ -34,36 +29,41 @@ namespace Umbraco.Tests.Scheduling Thread.Sleep(800); // for long Assert.IsTrue(runner.IsRunning); runner.Shutdown(false, true); // -force +wait - AssertRunnerStopsRunning(runner); + await runner; // wait for the entire runner operation to complete Assert.IsTrue(runner.IsCompleted); } } [Test] - public void ShutdownWhenRunning() + public async void ShutdownWhenRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - // do NOT try to do this because the code must run on the UI thread which - // is not availably, and so the thread never actually starts - wondering - // what it means for ASP.NET? - //runner.TaskStarting += (sender, args) => Console.WriteLine("starting {0:c}", DateTime.Now); - //runner.TaskCompleted += (sender, args) => Console.WriteLine("completed {0:c}", DateTime.Now); + Console.WriteLine("Begin {0}", DateTime.Now); + + runner.TaskStarting += (sender, args) => Console.WriteLine("starting {0}", DateTime.Now); + runner.TaskCompleted += (sender, args) => Console.WriteLine("completed {0}", DateTime.Now); Assert.IsFalse(runner.IsRunning); + + Console.WriteLine("Adding task {0}", DateTime.Now); runner.Add(new MyTask(5000)); Assert.IsTrue(runner.IsRunning); // is running the task + Console.WriteLine("Shutting down {0}", DateTime.Now); runner.Shutdown(false, false); // -force -wait Assert.IsTrue(runner.IsCompleted); Assert.IsTrue(runner.IsRunning); // still running that task - Thread.Sleep(3000); + Thread.Sleep(3000); // wait slightly less than the task takes to complete Assert.IsTrue(runner.IsRunning); // still running that task - AssertRunnerStopsRunning(runner, 10000); + + await runner; // wait for the entire runner operation to complete + + Console.WriteLine("End {0}", DateTime.Now); } } [Test] - public void ShutdownFlushesTheQueue() + public async void ShutdownFlushesTheQueue() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -74,13 +74,13 @@ namespace Umbraco.Tests.Scheduling runner.Add(t); Assert.IsTrue(runner.IsRunning); // is running the first task runner.Shutdown(false, false); // -force -wait - AssertRunnerStopsRunning(runner, 10000); + await runner; // wait for the entire runner operation to complete Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run } } [Test] - public void ShutdownForceTruncatesTheQueue() + public async void ShutdownForceTruncatesTheQueue() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -91,13 +91,13 @@ namespace Umbraco.Tests.Scheduling runner.Add(t); Assert.IsTrue(runner.IsRunning); // is running the first task runner.Shutdown(true, false); // +force -wait - AssertRunnerStopsRunning(runner, 10000); + await runner; // wait for the entire runner operation to complete Assert.AreEqual(DateTime.MinValue, t.Ended); // t has not run } } [Test] - public void ShutdownThenForce() + public async void ShutdownThenForce() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -112,7 +112,7 @@ namespace Umbraco.Tests.Scheduling Thread.Sleep(3000); Assert.IsTrue(runner.IsRunning); // still running that task runner.Shutdown(true, false); // +force -wait - AssertRunnerStopsRunning(runner, 20000); + await runner; // wait for the entire runner operation to complete } } @@ -126,12 +126,12 @@ namespace Umbraco.Tests.Scheduling } [Test] - public void Create_AutoStart_IsRunning() + public async void Create_AutoStart_IsRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true })) { Assert.IsTrue(runner.IsRunning); - AssertRunnerStopsRunning(runner); // though not for long + await runner; // wait for the entire runner operation to complete } } @@ -148,7 +148,7 @@ namespace Umbraco.Tests.Scheduling } [Test] - public void Dispose_IsRunning() + public async void Dispose_IsRunning() { BackgroundTaskRunner runner; using (runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) @@ -157,19 +157,19 @@ namespace Umbraco.Tests.Scheduling // dispose will stop it } - AssertRunnerStopsRunning(runner); + await runner; // wait for the entire runner operation to complete Assert.Throws(() => runner.Add(new MyTask())); } [Test] - public void Startup_IsRunning() + public async void Startup_IsRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { Assert.IsFalse(runner.IsRunning); runner.StartUp(); Assert.IsTrue(runner.IsRunning); - AssertRunnerStopsRunning(runner); // though not for long + await runner; // wait for the entire runner operation to complete } } @@ -186,13 +186,19 @@ namespace Umbraco.Tests.Scheduling } [Test] - public void Create_AddTask_IsRunning() + public async void Create_AddTask_IsRunning() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { + var waitHandle = new ManualResetEvent(false); + runner.TaskCompleted += (sender, args) => + { + waitHandle.Set(); + }; runner.Add(new MyTask()); Assert.IsTrue(runner.IsRunning); - Thread.Sleep(800); // task takes 500ms + waitHandle.WaitOne(); + await runner; //since we are not being kept alive, it will quit Assert.IsFalse(runner.IsRunning); } } @@ -202,11 +208,16 @@ namespace Umbraco.Tests.Scheduling { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true })) { + var waitHandle = new ManualResetEvent(false); + runner.TaskCompleted += (sender, args) => + { + Assert.IsTrue(sender.IsRunning); + waitHandle.Set(); + }; runner.Add(new MyTask()); + waitHandle.WaitOne(); + Thread.Sleep(1000); // we are waiting a second just to prove that it's still running and hasn't been shut off Assert.IsTrue(runner.IsRunning); - Thread.Sleep(800); // task takes 500ms - Assert.IsTrue(runner.IsRunning); - // dispose will stop it } } @@ -218,9 +229,9 @@ namespace Umbraco.Tests.Scheduling var task = new MyTask(); Assert.IsTrue(task.Ended == default(DateTime)); runner.Add(task); - await runner; // wait 'til it's not running anymore + await runner.CurrentThreadingTask; // wait for the Task operation to complete Assert.IsTrue(task.Ended != default(DateTime)); // task is done - AssertRunnerStopsRunning(runner); // though not for long + await runner; // wait for the entire runner operation to complete } } @@ -235,19 +246,19 @@ namespace Umbraco.Tests.Scheduling { tasks.ForEach(runner.Add); - await runner; // wait 'til it's not running anymore + await runner; // wait for the entire runner operation to complete // check that tasks are done Assert.IsTrue(tasks.All(x => x.Ended != default(DateTime))); - Assert.AreEqual(TaskStatus.RanToCompletion, runner.TaskStatus); + Assert.AreEqual(TaskStatus.RanToCompletion, runner.CurrentThreadingTask.Status); Assert.IsFalse(runner.IsRunning); Assert.IsFalse(runner.IsDisposed); } } [Test] - public void WaitOnTask() + public async void WaitOnTask() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -258,12 +269,12 @@ namespace Umbraco.Tests.Scheduling runner.Add(task); waitHandle.WaitOne(); // wait 'til task is done Assert.IsTrue(task.Ended != default(DateTime)); // task is done - AssertRunnerStopsRunning(runner); // though not for long + await runner; // wait for the entire runner operation to complete } } [Test] - public void WaitOnTasks() + public async void WaitOnTasks() { var tasks = new Dictionary(); for (var i = 0; i < 10; i++) @@ -278,7 +289,7 @@ namespace Umbraco.Tests.Scheduling WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); Assert.IsTrue(tasks.All(x => x.Key.Ended != default(DateTime))); - AssertRunnerStopsRunning(runner); // though not for long + await runner; // wait for the entire runner operation to complete } } @@ -349,9 +360,9 @@ namespace Umbraco.Tests.Scheduling tasks.ForEach(tManager.Add); //wait till the thread is done - await tManager; + await tManager.CurrentThreadingTask; - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); + Assert.AreEqual(TaskStatus.RanToCompletion, tManager.CurrentThreadingTask.Status); Assert.IsFalse(tManager.IsRunning); Assert.IsFalse(tManager.IsDisposed); @@ -367,14 +378,14 @@ namespace Umbraco.Tests.Scheduling tasks.ForEach(tManager.Add); //wait till the thread is done - await tManager; + await tManager.CurrentThreadingTask; foreach (var task in tasks) { Assert.IsTrue(task.Ended != default(DateTime)); } - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); + Assert.AreEqual(TaskStatus.RanToCompletion, tManager.CurrentThreadingTask.Status); Assert.IsFalse(tManager.IsRunning); Assert.IsFalse(tManager.IsDisposed); } @@ -383,31 +394,39 @@ namespace Umbraco.Tests.Scheduling [Test] public void RecurringTaskTest() { - // note: can have BackgroundTaskRunner and use it in MyRecurringTask ctor - // because that ctor wants IBackgroundTaskRunner and the generic type - // parameter is contravariant ie defined as IBackgroundTaskRunner so doing the - // following is legal: - // var IBackgroundTaskRunner b = ...; - // var IBackgroundTaskRunner d = b; // legal - + var runCount = 0; + var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { + runner.TaskCompleted += (sender, args) => runCount++; + runner.TaskStarting += async (sender, args) => + { + //wait for each task to finish once it's started + await sender.CurrentThreadingTask; + if (runCount > 3) + { + waitHandle.Set(); + } + }; + var task = new MyRecurringTask(runner, 200, 500); - MyRecurringTask.RunCount = 0; + runner.Add(task); - Thread.Sleep(5000); - Assert.GreaterOrEqual(MyRecurringTask.RunCount, 2); // keeps running, count >= 2 + + Assert.IsTrue(runner.IsRunning); // waiting on delay + Assert.AreEqual(0, runCount); + + waitHandle.WaitOne(); + + Assert.AreEqual(4, runCount); // stops recurring runner.Shutdown(false, false); - AssertRunnerStopsRunning(runner); - - // timer may try to add a task but it won't work because runner is completed } } [Test] - public void DelayedTaskRuns() + public async void DelayedTaskRuns() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -418,14 +437,14 @@ namespace Umbraco.Tests.Scheduling Assert.IsTrue(runner.IsRunning); // still waiting for the task to release Assert.IsFalse(task.HasRun); task.Release(); - Thread.Sleep(500); + await runner.CurrentThreadingTask; //wait for current task to complete Assert.IsTrue(task.HasRun); - AssertRunnerStopsRunning(runner); // runs task & exit + await runner; // wait for the entire runner operation to complete } } [Test] - public void DelayedTaskStops() + public async void DelayedTaskStops() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -436,7 +455,7 @@ namespace Umbraco.Tests.Scheduling Assert.IsTrue(runner.IsRunning); // still waiting for the task to release Assert.IsFalse(task.HasRun); runner.Shutdown(false, false); - AssertRunnerStopsRunning(runner); // runs task & exit + await runner; // wait for the entire runner operation to complete Assert.IsTrue(task.HasRun); } } @@ -444,29 +463,36 @@ namespace Umbraco.Tests.Scheduling [Test] public void DelayedRecurring() { + var runCount = 0; + var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { + runner.TaskCompleted += (sender, args) => runCount++; + runner.TaskStarting += async (sender, args) => + { + //wait for each task to finish once it's started + await sender.CurrentThreadingTask; + if (runCount > 3) + { + waitHandle.Set(); + } + }; + var task = new MyDelayedRecurringTask(runner, 2000, 1000); - MyDelayedRecurringTask.RunCount = 0; runner.Add(task); - Thread.Sleep(1000); Assert.IsTrue(runner.IsRunning); // waiting on delay - Assert.AreEqual(0, MyDelayedRecurringTask.RunCount); - Thread.Sleep(1000); - Assert.AreEqual(1, MyDelayedRecurringTask.RunCount); - Thread.Sleep(5000); - Assert.GreaterOrEqual(MyDelayedRecurringTask.RunCount, 2); // keeps running, count >= 2 + Assert.AreEqual(0, runCount); + + waitHandle.WaitOne(); + Assert.AreEqual(4, runCount); // stops recurring runner.Shutdown(false, false); - AssertRunnerStopsRunning(runner); - - // timer may try to add a task but it won't work because runner is completed } } [Test] - public void FailingTaskSync() + public async void FailingTaskSync() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -476,14 +502,14 @@ namespace Umbraco.Tests.Scheduling var task = new MyFailingTask(false); // -async runner.Add(task); Assert.IsTrue(runner.IsRunning); - AssertRunnerStopsRunning(runner); // runs task & exit + await runner; // wait for the entire runner operation to complete Assert.AreEqual(1, exceptions.Count); // traced and reported } } [Test] - public void FailingTaskAsync() + public async void FailingTaskAsync() { using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { @@ -493,7 +519,7 @@ namespace Umbraco.Tests.Scheduling var task = new MyFailingTask(true); // +async runner.Add(task); Assert.IsTrue(runner.IsRunning); - AssertRunnerStopsRunning(runner); // runs task & exit + await runner; // wait for the entire runner operation to complete Assert.AreEqual(1, exceptions.Count); // traced and reported } @@ -540,8 +566,6 @@ namespace Umbraco.Tests.Scheduling : base(source) { } - public static int RunCount { get; set; } - public override bool IsAsync { get { return false; } @@ -550,7 +574,6 @@ namespace Umbraco.Tests.Scheduling public override void PerformRun() { // nothing to do at the moment - RunCount += 1; } public override Task PerformRunAsync() @@ -621,7 +644,6 @@ namespace Umbraco.Tests.Scheduling { private readonly int _runMilliseconds; - public static int RunCount { get; set; } public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) : base(runner, periodMilliseconds) @@ -636,8 +658,7 @@ namespace Umbraco.Tests.Scheduling } public override void PerformRun() - { - RunCount += 1; + { Thread.Sleep(_runMilliseconds); } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 3a5ace8af8..d72af8bccd 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Concurrent; +using System.Globalization; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Web.Hosting; using Umbraco.Core.Logging; +using Umbraco.Core.Events; namespace Umbraco.Web.Scheduling { @@ -21,6 +23,7 @@ namespace Umbraco.Web.Scheduling private readonly BlockingCollection _tasks = new BlockingCollection(); private readonly object _locker = new object(); private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(false); + private BackgroundTaskRunnerAwaiter _awaiter; private volatile bool _isRunning; // is running private volatile bool _isCompleted; // does not accept tasks anymore, may still be running @@ -28,10 +31,11 @@ namespace Umbraco.Web.Scheduling private CancellationTokenSource _tokenSource; - internal event EventHandler> TaskError; - internal event EventHandler> TaskStarting; - internal event EventHandler> TaskCompleted; - internal event EventHandler> TaskCancelled; + internal event TypedEventHandler, TaskEventArgs> TaskError; + internal event TypedEventHandler, TaskEventArgs> TaskStarting; + internal event TypedEventHandler, TaskEventArgs> TaskCompleted; + internal event TypedEventHandler, TaskEventArgs> TaskCancelled; + internal event TypedEventHandler, EventArgs> Completed; /// /// Initializes a new instance of the class. @@ -48,7 +52,7 @@ namespace Umbraco.Web.Scheduling { if (options == null) throw new ArgumentNullException("options"); _options = options; - + HostingEnvironment.RegisterObject(this); if (options.AutoStart) @@ -80,42 +84,40 @@ namespace Umbraco.Web.Scheduling } /// - /// Gets the status of the running task. + /// Gets an awaiter used to await the running Threading.Task. /// /// There is no running task. - /// Unless the AutoStart option is true, there will be no running task until + /// + /// Unless the AutoStart option is true, there will be no running task until /// a background task is added to the queue. Unless the KeepAlive option is true, there - /// will be no running task when the queue is empty. - public TaskStatus TaskStatus + /// will be no running task when the queue is empty. + /// + public ThreadingTaskAwaiter CurrentThreadingTask { get { if (_runningTask == null) - throw new InvalidOperationException("There is no current task."); - return _runningTask.Status; + throw new InvalidOperationException("There is no current Threading.Task."); + return new ThreadingTaskAwaiter(_runningTask); } } - + /// - /// Gets an awaiter used to await the running task. + /// Gets an awaiter used to await the BackgroundTaskRunner running operation /// - /// An awaiter for the running task. + /// An awaiter for the BackgroundTaskRunner running operation /// - /// This is just the coolest thing ever, check this article out: - /// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx - /// - /// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion - /// we can await anything! :) - /// - /// There is no running task. - /// Unless the AutoStart option is true, there will be no running task until - /// a background task is added to the queue. Unless the KeepAlive option is true, there - /// will be no running task when the queue is empty. - public TaskAwaiter GetAwaiter() + /// This is used to wait until the background task runner is no longer running (IsRunning == false) + /// + /// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion + /// we can await anything. In this case we are awaiting with a custom BackgroundTaskRunnerAwaiter + /// which waits for the Completed event to be raised. + /// ref: http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx + /// + /// + public BackgroundTaskRunnerAwaiter GetAwaiter() { - if (_runningTask == null) - throw new InvalidOperationException("There is no current task."); - return _runningTask.GetAwaiter(); + return _awaiter ?? (_awaiter = new BackgroundTaskRunnerAwaiter(this)); } /// @@ -261,6 +263,8 @@ namespace Umbraco.Web.Scheduling _isRunning = false; // done if (_options.PreserveRunningTask == false) _runningTask = null; + //raise event + OnCompleted(); return; } } @@ -388,7 +392,7 @@ namespace Umbraco.Web.Scheduling catch (Exception ex) { LogHelper.Error>("Task has failed.", ex); - } + } } #region Events @@ -402,24 +406,70 @@ namespace Umbraco.Web.Scheduling protected virtual void OnTaskStarting(TaskEventArgs e) { var handler = TaskStarting; - if (handler != null) handler(this, e); + if (handler != null) + { + try + { + handler(this, e); + } + catch (Exception ex) + { + LogHelper.Error>("TaskStarting exception occurred", ex); + } + } } protected virtual void OnTaskCompleted(TaskEventArgs e) { var handler = TaskCompleted; - if (handler != null) handler(this, e); + if (handler != null) + { + try + { + handler(this, e); + } + catch (Exception ex) + { + LogHelper.Error>("TaskCompleted exception occurred", ex); + } + } } protected virtual void OnTaskCancelled(TaskEventArgs e) { var handler = TaskCancelled; - if (handler != null) handler(this, e); + if (handler != null) + { + try + { + handler(this, e); + } + catch (Exception ex) + { + LogHelper.Error>("TaskCancelled exception occurred", ex); + } + } //dispose it e.Task.Dispose(); } + protected virtual void OnCompleted() + { + var handler = Completed; + if (handler != null) + { + try + { + handler(this, EventArgs.Empty); + } + catch (Exception ex) + { + LogHelper.Error>("OnCompleted exception occurred", ex); + } + } + } + #endregion #region IDisposable @@ -509,5 +559,7 @@ namespace Umbraco.Web.Scheduling LogHelper.Info>("Down."); } } + + } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs new file mode 100644 index 0000000000..5719afae25 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Custom awaiter used to await when the BackgroundTaskRunner is completed (IsRunning == false) + /// + /// + /// + /// This custom awaiter simply uses a TaskCompletionSource to set the result when the Completed event of the + /// BackgroundTaskRunner executes. + /// A custom awaiter requires implementing INotifyCompletion as well as IsCompleted, OnCompleted and GetResult + /// see: http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx + /// + internal class BackgroundTaskRunnerAwaiter : INotifyCompletion where T : class, IBackgroundTask + { + private readonly BackgroundTaskRunner _runner; + private readonly TaskCompletionSource _tcs; + private readonly TaskAwaiter _awaiter; + + public BackgroundTaskRunnerAwaiter(BackgroundTaskRunner runner) + { + if (runner == null) throw new ArgumentNullException("runner"); + _runner = runner; + + _tcs = new TaskCompletionSource(); + + if (_runner.IsRunning) + { + _runner.Completed += (s, e) => _tcs.SetResult(0); + } + else + { + //not running, just set the result + _tcs.SetResult(0); + } + _awaiter = _tcs.Task.GetAwaiter(); + } + + public BackgroundTaskRunnerAwaiter GetAwaiter() + { + return this; + } + + /// + /// This is completed when the runner is finished running + /// + public bool IsCompleted + { + get { return _runner.IsRunning == false && _tcs.Task.IsCompleted; } + } + + public void OnCompleted(Action continuation) + { + _awaiter.OnCompleted(continuation); + } + + public void GetResult() + { + _awaiter.GetResult(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ThreadingTaskAwaiter.cs b/src/Umbraco.Web/Scheduling/ThreadingTaskAwaiter.cs new file mode 100644 index 0000000000..36c00dfa9e --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ThreadingTaskAwaiter.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + /// + /// This is used to return an awaitable instance from a Task without actually returning the + /// underlying Task instance since it shouldn't be mutable. + /// + internal class ThreadingTaskAwaiter + { + private readonly Task _task; + + public ThreadingTaskAwaiter(Task task) + { + if (task == null) throw new ArgumentNullException("task"); + _task = task; + } + + /// + /// With a GetAwaiter declared it means that this instance can be awaited on with the await keyword + /// + /// + public TaskAwaiter GetAwaiter() + { + return _task.GetAwaiter(); + } + + /// + /// Gets the status of the running task. + /// + /// There is no running task. + /// Unless the AutoStart option is true, there will be no running task until + /// a background task is added to the queue. Unless the KeepAlive option is true, there + /// will be no running task when the queue is empty. + public TaskStatus Status + { + get { return _task.Status; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2d9077bdc8..532630bc8b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -274,6 +274,7 @@ + @@ -498,6 +499,7 @@ +