diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 11ba81b585..e83ce400d9 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -13,97 +13,272 @@ namespace Umbraco.Tests.Scheduling [TestFixture] public class BackgroundTaskRunnerTests { - - - [Test] - public void Startup_And_Shutdown() + private static void AssertRunnerStopsRunning(BackgroundTaskRunner runner, int timeoutMilliseconds = 2000) + where T : class, IBackgroundTask { - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) - { - tManager.StartUp(); - } + const int period = 200; - NUnit.Framework.Assert.IsFalse(tManager.IsRunning); + var i = 0; + var m = timeoutMilliseconds/period; + while (runner.IsRunning && i++ < m) + Thread.Sleep(period); + Assert.IsFalse(runner.IsRunning, "Runner is still running."); } [Test] - public void Startup_Starts_Automatically() + public void ShutdownWaitWhenRunning() { - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) { - tManager.Add(new MyTask()); - NUnit.Framework.Assert.IsTrue(tManager.IsRunning); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // for long + Assert.IsTrue(runner.IsRunning); + runner.Shutdown(false, true); // -force +wait + AssertRunnerStopsRunning(runner); + Assert.IsTrue(runner.IsCompleted); } } [Test] - public void Task_Runs() + public void ShutdownWhenRunning() { - var myTask = new MyTask(); - var waitHandle = new ManualResetEvent(false); - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - tManager.TaskCompleted += (sender, task) => waitHandle.Set(); + // 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); - tManager.Add(myTask); - - //wait for ITasks to complete - waitHandle.WaitOne(); - - NUnit.Framework.Assert.IsTrue(myTask.Ended != default(DateTime)); + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + Assert.IsTrue(runner.IsRunning); // is running the task + runner.Shutdown(false, false); // -force -wait + Assert.IsTrue(runner.IsCompleted); + Assert.IsTrue(runner.IsRunning); // still running that task + Thread.Sleep(3000); + Assert.IsTrue(runner.IsRunning); // still running that task + AssertRunnerStopsRunning(runner, 10000); } } [Test] - public void Many_Tasks_Run() + public void ShutdownFlushesTheQueue() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + var t = new MyTask(); + runner.Add(t); + Assert.IsTrue(runner.IsRunning); // is running the first task + runner.Shutdown(false, false); // -force -wait + AssertRunnerStopsRunning(runner, 10000); + Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run + } + } + + [Test] + public void ShutdownForceTruncatesTheQueue() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + var t = new MyTask(); + runner.Add(t); + Assert.IsTrue(runner.IsRunning); // is running the first task + runner.Shutdown(true, false); // +force -wait + AssertRunnerStopsRunning(runner, 10000); + Assert.AreEqual(DateTime.MinValue, t.Ended); // t has not run + } + } + + [Test] + public void ShutdownThenForce() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); // is running the task + runner.Shutdown(false, false); // -force -wait + Assert.IsTrue(runner.IsCompleted); + Assert.IsTrue(runner.IsRunning); // still running that task + Thread.Sleep(3000); + Assert.IsTrue(runner.IsRunning); // still running that task + runner.Shutdown(true, false); // +force -wait + AssertRunnerStopsRunning(runner, 20000); + } + } + + [Test] + public void Create_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + } + } + + [Test] + public void Create_AutoStart_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true })) + { + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public void Create_AutoStartAndKeepAlive_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) + { + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // for long + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public void Dispose_IsRunning() + { + BackgroundTaskRunner runner; + using (runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) + { + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + + AssertRunnerStopsRunning(runner); + Assert.Throws(() => runner.Add(new MyTask())); + } + + [Test] + public 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 + } + } + + [Test] + public void Startup_KeepAlive_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true })) + { + Assert.IsFalse(runner.IsRunning); + runner.StartUp(); + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public void Create_AddTask_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // task takes 500ms + Assert.IsFalse(runner.IsRunning); + } + } + + [Test] + public void Create_KeepAliveAndAddTask_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true })) + { + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // task takes 500ms + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public async void WaitOnRunner_OneTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyTask(); + Assert.IsTrue(task.Ended == default(DateTime)); + runner.Add(task); + await runner; // wait 'til it's not running anymore + Assert.IsTrue(task.Ended != default(DateTime)); // task is done + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public async void WaitOnRunner_Tasks() + { + var tasks = new List(); + for (var i = 0; i < 10; i++) + tasks.Add(new MyTask()); + + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = false, LongRunning = true, PreserveRunningTask = true })) + { + tasks.ForEach(runner.Add); + + await runner; // wait 'til it's not running anymore + + // check that tasks are done + Assert.IsTrue(tasks.All(x => x.Ended != default(DateTime))); + + Assert.AreEqual(TaskStatus.RanToCompletion, runner.TaskStatus); + Assert.IsFalse(runner.IsRunning); + Assert.IsFalse(runner.IsDisposed); + } + } + + [Test] + public void WaitOnTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyTask(); + var waitHandle = new ManualResetEvent(false); + runner.TaskCompleted += (sender, t) => waitHandle.Set(); + Assert.IsTrue(task.Ended == default(DateTime)); + 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 + } + } + + [Test] + public void WaitOnTasks() { var tasks = new Dictionary(); for (var i = 0; i < 10; i++) - { tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + runner.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + foreach (var t in tasks) runner.Add(t.Key); - tasks.ForEach(t => tManager.Add(t.Key)); - - //wait for all ITasks to complete + // wait 'til tasks are done, check that tasks are done WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + Assert.IsTrue(tasks.All(x => x.Key.Ended != default(DateTime))); - foreach (var task in tasks) - { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); - } - } - } - - [Test] - public async void Many_Tasks_Added_Only_Last_Task_Executes_With_Option() - { - var tasks = new Dictionary(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions{OnlyProcessLastItem = true})) - { - - tasks.ForEach(t => tManager.Add(t.Key)); - - //wait till the thread is done - await tManager; - - var countExecuted = tasks.Count(x => x.Key.Ended != default(DateTime)); - - Assert.AreEqual(1, countExecuted); + AssertRunnerStopsRunning(runner); // though not for long } } @@ -123,7 +298,7 @@ namespace Umbraco.Tests.Scheduling IDictionary tasks = getTasks(); BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, KeepAlive = true })) { tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); @@ -135,7 +310,7 @@ namespace Umbraco.Tests.Scheduling foreach (var task in tasks) { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + Assert.IsTrue(task.Key.Ended != default(DateTime)); } //execute another batch after a bit @@ -149,71 +324,11 @@ namespace Umbraco.Tests.Scheduling foreach (var task in tasks) { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + Assert.IsTrue(task.Key.Ended != default(DateTime)); } } } - [Test] - public void Task_Queue_Will_Be_Completed_Before_Shutdown() - { - var tasks = new Dictionary(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) - { - tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); - - tasks.ForEach(t => tManager.Add(t.Key)); - - ////wait for all ITasks to complete - //WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); - - tManager.Stop(false); - //immediate stop will block until complete - but since we are running on - // a single thread this doesn't really matter as the above will just process - // until complete. - tManager.Stop(true); - - NUnit.Framework.Assert.AreEqual(0, tManager.TaskCount); - } - } - - //NOTE: These tests work in .Net 4.5 but in this current version we don't have the correct - // async/await signatures with GetAwaiter, so am just commenting these out in this version - - [Test] - public async void Non_Persistent_Runner_Will_End_After_Queue_Empty() - { - var tasks = new List(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask()); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread:true)) - { - tasks.ForEach(t => tManager.Add(t)); - - //wait till the thread is done - await tManager; - - foreach (var task in tasks) - { - Assert.IsTrue(task.Ended != default(DateTime)); - } - - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); - Assert.IsFalse(tManager.IsRunning); - Assert.IsFalse(tManager.IsDisposed); - } - } - [Test] public async void Non_Persistent_Runner_Will_Start_New_Threads_When_Required() { @@ -229,10 +344,9 @@ namespace Umbraco.Tests.Scheduling List tasks = getTasks(); - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread: true)) + using (var tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, PreserveRunningTask = true })) { - tasks.ForEach(t => tManager.Add(t)); + tasks.ForEach(tManager.Add); //wait till the thread is done await tManager; @@ -250,7 +364,7 @@ namespace Umbraco.Tests.Scheduling tasks = getTasks(); //add more tasks - tasks.ForEach(t => tManager.Add(t)); + tasks.ForEach(tManager.Add); //wait till the thread is done await tManager; @@ -265,19 +379,296 @@ namespace Umbraco.Tests.Scheduling Assert.IsFalse(tManager.IsDisposed); } } - - private class MyTask : BaseTask + [Test] + public void RecurringTaskTest() { - public MyTask() + // 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 + + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { + 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 + + // 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() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyDelayedTask(200); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(5000); + Assert.IsTrue(runner.IsRunning); // still waiting for the task to release + Assert.IsFalse(task.HasRun); + task.Release(); + Thread.Sleep(500); + Assert.IsTrue(task.HasRun); + AssertRunnerStopsRunning(runner); // runs task & exit + } + } + + [Test] + public void DelayedTaskStops() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyDelayedTask(200); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(5000); + Assert.IsTrue(runner.IsRunning); // still waiting for the task to release + Assert.IsFalse(task.HasRun); + runner.Shutdown(false, false); + AssertRunnerStopsRunning(runner); // runs task & exit + Assert.IsTrue(task.HasRun); + } + } + + [Test] + public void DelayedRecurring() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + 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 + + // 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() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var exceptions = new ConcurrentQueue(); + runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); + + var task = new MyFailingTask(false); // -async + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // runs task & exit + + Assert.AreEqual(1, exceptions.Count); // traced and reported + } + } + + [Test] + public void FailingTaskAsync() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var exceptions = new ConcurrentQueue(); + runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); + + var task = new MyFailingTask(true); // +async + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // runs task & exit + + Assert.AreEqual(1, exceptions.Count); // traced and reported + } + } + + private class MyFailingTask : IBackgroundTask + { + private readonly bool _isAsync; + + public MyFailingTask(bool isAsync) + { + _isAsync = isAsync; + } + + public void Run() + { + Thread.Sleep(1000); + throw new Exception("Task has thrown."); + } + + public async Task RunAsync() + { + await Task.Delay(1000); + throw new Exception("Task has thrown."); + } + + public bool IsAsync + { + get { return _isAsync; } + } + + // fixme - must also test what happens if we throw on dispose! + public void Dispose() + { } + } + + private class MyDelayedRecurringTask : DelayedRecurringTaskBase + { + public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + : base(runner, delayMilliseconds, periodMilliseconds) + { } + + private MyDelayedRecurringTask(MyDelayedRecurringTask source) + : base(source) + { } + + public static int RunCount { get; set; } + + public override bool IsAsync + { + get { return false; } } public override void PerformRun() { - Thread.Sleep(500); + // nothing to do at the moment + RunCount += 1; } + public override Task PerformRunAsync() + { + throw new NotImplementedException(); + } + + protected override MyDelayedRecurringTask GetRecurring() + { + return new MyDelayedRecurringTask(this); + } + } + + private class MyDelayedTask : IDelayedBackgroundTask + { + private readonly int _runMilliseconds; + private readonly ManualResetEvent _gate; + + public bool HasRun { get; private set; } + + public MyDelayedTask(int runMilliseconds) + { + _runMilliseconds = runMilliseconds; + _gate = new ManualResetEvent(false); + } + + public WaitHandle DelayWaitHandle + { + get { return _gate; } + } + + public bool IsDelayed + { + get { return true; } + } + + public void Run() + { + Thread.Sleep(_runMilliseconds); + HasRun = true; + } + + public void Release() + { + _gate.Set(); + } + + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } + + public void Dispose() + { } + } + + private class MyRecurringTask : RecurringTaskBase + { + private readonly int _runMilliseconds; + + public static int RunCount { get; set; } + + public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) + : base(runner, periodMilliseconds) + { + _runMilliseconds = runMilliseconds; + } + + private MyRecurringTask(MyRecurringTask source, int runMilliseconds) + : base(source) + { + _runMilliseconds = runMilliseconds; + } + + public override void PerformRun() + { + RunCount += 1; + Thread.Sleep(_runMilliseconds); + } + + public override Task PerformRunAsync() + { + throw new NotImplementedException(); + } + + public override bool IsAsync + { + get { return false; } + } + + protected override MyRecurringTask GetRecurring() + { + return new MyRecurringTask(this, _runMilliseconds); + } + } + + private class MyTask : BaseTask + { + private readonly int _milliseconds; + + public MyTask() + : this(500) + { } + + public MyTask(int milliseconds) + { + _milliseconds = milliseconds; + } + + public override void PerformRun() + { + Thread.Sleep(_milliseconds); + } } public abstract class BaseTask : IBackgroundTask @@ -287,14 +678,17 @@ namespace Umbraco.Tests.Scheduling public Guid UniqueId { get; protected set; } public abstract void PerformRun(); + public void Run() { PerformRun(); Ended = DateTime.Now; } + public Task RunAsync() { throw new NotImplementedException(); + //return Task.Delay(500); // fixme } public bool IsAsync @@ -312,10 +706,7 @@ namespace Umbraco.Tests.Scheduling public DateTime Ended { get; set; } public virtual void Dispose() - { - - } + { } } - } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index ffba7a1e0a..c511a0af53 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -9,60 +10,97 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { /// - /// This is used to create a background task runner which will stay alive in the background of and complete - /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain - /// is shutdown. + /// Manages a queue of tasks of type and runs them in the background. /// - /// - internal class BackgroundTaskRunner : IDisposable, IRegisteredObject - where T : IBackgroundTask + /// The type of the managed tasks. + /// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain + /// shuts down (ie is unloaded). FIXME WHAT DOES THAT MEAN? + internal class BackgroundTaskRunner : IBackgroundTaskRunner + where T : class, IBackgroundTask { private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); - private Task _consumer; + private readonly object _locker = new object(); + private readonly ManualResetEvent _completedEvent = new ManualResetEvent(false); + + private volatile bool _isRunning; // is running + private volatile bool _isCompleted; // does not accept tasks anymore, may still be running + private Task _runningTask; - private volatile bool _isRunning = false; - private static readonly object Locker = new object(); private CancellationTokenSource _tokenSource; + internal event EventHandler> TaskError; internal event EventHandler> TaskStarting; internal event EventHandler> TaskCompleted; internal event EventHandler> TaskCancelled; - public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false) - : this(new BackgroundTaskRunnerOptions{DedicatedThread = dedicatedThread, PersistentThread = persistentThread}) - { - } + /// + /// Initializes a new instance of the class. + /// + public BackgroundTaskRunner() + : this(new BackgroundTaskRunnerOptions()) + { } + /// + /// Initializes a new instance of the class with a set of options. + /// + /// The set of options. public BackgroundTaskRunner(BackgroundTaskRunnerOptions options) { if (options == null) throw new ArgumentNullException("options"); _options = options; HostingEnvironment.RegisterObject(this); + + if (options.AutoStart) + StartUp(); } + /// + /// Gets the number of tasks in the queue. + /// public int TaskCount { get { return _tasks.Count; } } + /// + /// Gets a value indicating whether a task is currently running. + /// public bool IsRunning { get { return _isRunning; } } - public TaskStatus TaskStatus + /// + /// Gets a value indicating whether the runner has completed and cannot accept tasks anymore. + /// + public bool IsCompleted { - get { return _consumer.Status; } + get { return _isCompleted; } } + /// + /// 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 TaskStatus + { + get + { + if (_runningTask == null) + throw new InvalidOperationException("There is no current task."); + return _runningTask.Status; + } + } /// - /// Returns the task awaiter so that consumers of the BackgroundTaskManager can await - /// the threading operation. + /// Gets an awaiter used to await the running task. /// - /// + /// An awaiter for the running task. /// /// This is just the coolest thing ever, check this article out: /// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx @@ -70,267 +108,287 @@ namespace Umbraco.Web.Scheduling /// 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() { - return _consumer.GetAwaiter(); + if (_runningTask == null) + throw new InvalidOperationException("There is no current task."); + return _runningTask.GetAwaiter(); } + /// + /// Adds a task to the queue. + /// + /// The task to add. + /// The task runner has completed. public void Add(T task) { - //add any tasks first - LogHelper.Debug>(" Task added {0}", () => task.GetType()); - _tasks.Add(task); + lock (_locker) + { + if (_isCompleted) + throw new InvalidOperationException("The task runner has completed."); - //ensure's everything is started - StartUp(); + // add task + LogHelper.Debug>("Task added {0}", task.GetType); + _tasks.Add(task); + + // start + StartUpLocked(); + } } + /// + /// Tries to add a task to the queue. + /// + /// The task to add. + /// true if the task could be added to the queue; otherwise false. + /// Returns false if the runner is completed. + public bool TryAdd(T task) + { + lock (_locker) + { + if (_isCompleted) return false; + + // add task + LogHelper.Debug>("Task added {0}", task.GetType); + _tasks.Add(task); + + // start + StartUpLocked(); + + return true; + } + } + + /// + /// Starts the tasks runner, if not already running. + /// + /// Is invoked each time a task is added, to ensure it is going to be processed. + /// The task runner has completed. public void StartUp() { - if (!_isRunning) + if (_isRunning) return; + + lock (_locker) { - lock (Locker) - { - //double check - if (!_isRunning) - { - _isRunning = true; - //Create a new token source since this is a new proces - _tokenSource = new CancellationTokenSource(); - StartConsumer(); - LogHelper.Debug>("Starting"); - } - } - } - } + if (_isCompleted) + throw new InvalidOperationException("The task runner has completed."); - public void ShutDown() - { - lock (Locker) - { - _isRunning = false; - - try - { - if (_consumer != null) - { - //cancel all operations - _tokenSource.Cancel(); - - try - { - _consumer.Wait(); - } - catch (AggregateException e) - { - //NOTE: We are logging Debug because we are expecting these errors - - LogHelper.Debug>("AggregateException thrown with the following inner exceptions:"); - // Display information about each exception. - foreach (var v in e.InnerExceptions) - { - var exception = v as TaskCanceledException; - if (exception != null) - { - LogHelper.Debug>(" .Net TaskCanceledException: .Net Task ID {0}", () => exception.Task.Id); - } - else - { - LogHelper.Debug>(" Exception: {0}", () => v.GetType().Name); - } - } - } - } - - if (_tasks.Count > 0) - { - LogHelper.Debug>("Processing remaining tasks before shutdown: {0}", () => _tasks.Count); - - //now we need to ensure the remaining queue is processed if there's any remaining, - // this will all be processed on the current/main thread. - T remainingTask; - while (_tasks.TryTake(out remainingTask)) - { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - //NOTE: don't raise canceled event, we're shutting down, just dispose - remainingTask.Dispose(); - continue; - } - - ConsumeTaskInternalAsync(remainingTask) - .Wait(); //block until it completes - } - } - - LogHelper.Debug>("Shutdown"); - - //disposing these is really optional since they'll be disposed immediately since they are no longer running - //but we'll put this here anyways. - if (_consumer != null && (_consumer.IsCompleted || _consumer.IsCanceled)) - { - _consumer.Dispose(); - } - } - catch (Exception ex) - { - LogHelper.Error>("Error occurred shutting down task runner", ex); - } - finally - { - HostingEnvironment.UnregisterObject(this); - } + StartUpLocked(); } } /// - /// Starts the consumer task + /// Starts the tasks runner, if not already running. /// - private void StartConsumer() + /// Must be invoked within lock(_locker) and with _isCompleted being false. + private void StartUpLocked() { - var token = _tokenSource.Token; - - _consumer = Task.Factory.StartNew(() => - StartThreadAsync(token), - token, - _options.DedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, - TaskScheduler.Default); - - //if this is not a persistent thread, wait till it's done and shut ourselves down - // thus ending the thread or giving back to the thread pool. If another task is added - // another thread will spawn or be taken from the pool to process. - if (!_options.PersistentThread) - { - _consumer.ContinueWith(task => ShutDown()); - } + // double check + if (_isRunning) return; + _isRunning = true; + // create a new token source since this is a new process + _tokenSource = new CancellationTokenSource(); + _runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token); + LogHelper.Debug>("Starting"); } /// - /// Invokes a new worker thread to consume tasks + /// Shuts the taks runner down. /// - /// - private async Task StartThreadAsync(CancellationToken token) + /// True for force the runner to stop. + /// True to wait until the runner has stopped. + /// If is false, no more tasks can be queued but all queued tasks + /// will run. If it is true, then only the current one (if any) will end and no other task will run. + public void Shutdown(bool force, bool wait) { - // Was cancellation already requested? - if (token.IsCancellationRequested) + lock (_locker) { - LogHelper.Info>("Thread {0} was cancelled before it got started.", () => Thread.CurrentThread.ManagedThreadId); - token.ThrowIfCancellationRequested(); + _isCompleted = true; // do not accept new tasks + if (_isRunning == false) return; // done already } - await TakeAndConsumeTaskAsync(token); - } + // try to be nice + // assuming multiple threads can do these without problems + _completedEvent.Set(); + _tasks.CompleteAdding(); - /// - /// Trys to get a task from the queue, if there isn't one it will wait a second and try again - /// - /// - private async Task TakeAndConsumeTaskAsync(CancellationToken token) - { - if (token.IsCancellationRequested) + if (force) { - LogHelper.Info>("Thread {0} was cancelled.", () => Thread.CurrentThread.ManagedThreadId); - token.ThrowIfCancellationRequested(); - } - - //If this is true, the thread will stay alive and just wait until there is anything in the queue - // and process it. When there is nothing in the queue, the thread will just block until there is - // something to process. - //When this is false, the thread will process what is currently in the queue and once that is - // done, the thread will end and we will shutdown the process - - if (_options.PersistentThread) - { - //This will iterate over the collection, if there is nothing to take - // the thread will block until there is something available. - //We need to pass our cancellation token so that the thread will - // cancel when we shutdown - foreach (var t in _tasks.GetConsumingEnumerable(token)) + // we must bring everything down, now + Thread.Sleep(100); // give time to CompleAdding() + lock (_locker) { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - OnTaskCancelled(new TaskEventArgs(t)); - continue; - } - - await ConsumeTaskCancellableAsync(t, token); + // was CompleteAdding() enough? + if (_isRunning == false) return; } - - //recurse and keep going - await TakeAndConsumeTaskAsync(token); + // try to cancel running async tasks (cannot do much about sync tasks) + // break delayed tasks delay + // truncate running queues + _tokenSource.Cancel(false); // false is the default } else { - T t; - while (_tasks.TryTake(out t)) - { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - OnTaskCancelled(new TaskEventArgs(t)); - continue; - } - - await ConsumeTaskCancellableAsync(t, token); - } - - //the task will end here + // tasks in the queue will be executed... + if (wait == false) return; + _runningTask.Wait(); // wait for whatever is running to end... } } - internal async Task ConsumeTaskCancellableAsync(T task, CancellationToken token) + /// + /// Runs background tasks for as long as there are background tasks in the queue, with an asynchronous operation. + /// + /// The supporting . + /// A cancellation token. + /// The asynchronous operation. + private Task PumpIBackgroundTasks(TaskFactory factory, CancellationToken token) + { + var taskSource = new TaskCompletionSource(factory.CreationOptions); + var enumerator = _options.KeepAlive ? _tasks.GetConsumingEnumerable(token).GetEnumerator() : null; + + // ReSharper disable once MethodSupportsCancellation // always run + var taskSourceContinuing = taskSource.Task.ContinueWith(t => + { + // because the pump does not lock, there's a race condition, + // the pump may stop and then we still have tasks to process, + // and then we must restart the pump - lock to avoid race cond + lock (_locker) + { + if (token.IsCancellationRequested || _tasks.Count == 0) + { + _isRunning = false; // done + if (_options.PreserveRunningTask == false) + _runningTask = null; + return; + } + } + + // if _runningTask is taskSource.Task then we must keep continuing it, + // not starting a new taskSource, else _runningTask would complete and + // something may be waiting on it + //PumpIBackgroundTasks(factory, token); // restart + // ReSharper disable once MethodSupportsCancellation // always run + t.ContinueWithTask(_ => PumpIBackgroundTasks(factory, token)); // restart + }); + + Action pump = null; + pump = task => + { + // RunIBackgroundTaskAsync does NOT throw exceptions, just raises event + // so if we have an exception here, really, wtf? - must read the exception + // anyways so it does not bubble up and kill everything + if (task != null && task.IsFaulted) + { + var exception = task.Exception; + LogHelper.Error>("Task runner exception.", exception); + } + + // is it ok to run? + if (TaskSourceCanceled(taskSource, token)) return; + + // try to get a task + // the blocking MoveNext will end if token is cancelled or collection is completed + T bgTask; + var hasBgTask = _options.KeepAlive + ? (bgTask = enumerator.MoveNext() ? enumerator.Current : null) != null // blocking + : _tasks.TryTake(out bgTask); // non-blocking + + // no task, signal the runner we're done + if (hasBgTask == false) + { + TaskSourceCompleted(taskSource, token); + return; + } + + // wait for delayed task, supporting cancellation + var dbgTask = bgTask as IDelayedBackgroundTask; + if (dbgTask != null && dbgTask.IsDelayed) + { + WaitHandle.WaitAny(new[] { dbgTask.DelayWaitHandle, token.WaitHandle, _completedEvent }); + if (TaskSourceCanceled(taskSource, token)) return; + // else run now, either because delay is ok or runner is completed + } + + // run the task as first task, or a continuation + task = task == null + ? RunIBackgroundTaskAsync(bgTask, token) + // ReSharper disable once MethodSupportsCancellation // always run + : task.ContinueWithTask(_ => RunIBackgroundTaskAsync(bgTask, token)); + + // and pump + // ReSharper disable once MethodSupportsCancellation // always run + task.ContinueWith(t => pump(t)); + }; + + // start it all + factory.StartNew(() => pump(null), + token, + _options.LongRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, + TaskScheduler.Default); + + return taskSourceContinuing; + } + + private bool TaskSourceCanceled(TaskCompletionSource taskSource, CancellationToken token) { if (token.IsCancellationRequested) { - OnTaskCancelled(new TaskEventArgs(task)); - - //NOTE: Since the task hasn't started this is pretty pointless so leaving it out. - LogHelper.Info>("Task {0}) was cancelled.", - () => task.GetType()); - - token.ThrowIfCancellationRequested(); + taskSource.SetCanceled(); + return true; } - - await ConsumeTaskInternalAsync(task); + return false; } - private async Task ConsumeTaskInternalAsync(T task) + private void TaskSourceCompleted(TaskCompletionSource taskSource, CancellationToken token) + { + if (token.IsCancellationRequested) + taskSource.SetCanceled(); + else + taskSource.SetResult(null); + } + + /// + /// Runs a background task asynchronously. + /// + /// The background task. + /// A cancellation token. + /// The asynchronous operation. + internal async Task RunIBackgroundTaskAsync(T bgTask, CancellationToken token) { try { - OnTaskStarting(new TaskEventArgs(task)); + OnTaskStarting(new TaskEventArgs(bgTask)); try { - using (task) + using (bgTask) // ensure it's disposed { - if (task.IsAsync) - { - await task.RunAsync(); - } + if (bgTask.IsAsync) + await bgTask.RunAsync(); // fixme should pass the token along?! else - { - task.Run(); - } + bgTask.Run(); } } catch (Exception e) { - OnTaskError(new TaskEventArgs(task, e)); + OnTaskError(new TaskEventArgs(bgTask, e)); throw; } - - OnTaskCompleted(new TaskEventArgs(task)); + Console.WriteLine("!1"); + OnTaskCompleted(new TaskEventArgs(bgTask)); } catch (Exception ex) { - LogHelper.Error>("An error occurred consuming task", ex); + LogHelper.Error>("Task has failed.", ex); } } + #region Events + protected virtual void OnTaskError(TaskEventArgs e) { var handler = TaskError; @@ -358,8 +416,10 @@ namespace Umbraco.Web.Scheduling e.Task.Dispose(); } + #endregion + + #region IDisposable - #region Disposal private readonly object _disposalLocker = new object(); public bool IsDisposed { get; private set; } @@ -376,8 +436,9 @@ namespace Umbraco.Web.Scheduling protected virtual void Dispose(bool disposing) { - if (this.IsDisposed || !disposing) + if (this.IsDisposed || disposing == false) return; + lock (_disposalLocker) { if (IsDisposed) @@ -389,31 +450,60 @@ namespace Umbraco.Web.Scheduling protected virtual void DisposeResources() { - ShutDown(); + // just make sure we eventually go down + Shutdown(true, false); } + #endregion + /// + /// Requests a registered object to unregister. + /// + /// true to indicate the registered object should unregister from the hosting + /// environment before returning; otherwise, false. + /// + /// "When the application manager needs to stop a registered object, it will call the Stop method." + /// The application manager will call the Stop method to ask a registered object to unregister. During + /// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method. + /// public void Stop(bool immediate) { if (immediate == false) { - LogHelper.Debug>("Application is shutting down, waiting for tasks to complete"); - Dispose(); + // The Stop method is first called with the immediate parameter set to false. The object can either complete + // processing, call the UnregisterObject method, and then return or it can return immediately and complete + // processing asynchronously before calling the UnregisterObject method. + + LogHelper.Debug>("Shutting down, waiting for tasks to complete."); + Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait + + lock (_locker) + { + if (_runningTask != null) + _runningTask.ContinueWith(_ => + { + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down, tasks completed."); + }); + else + { + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down, tasks completed."); + } + } } else { - //NOTE: this will thread block the current operation if the manager - // is still shutting down because the Shutdown operation is also locked - // by this same lock instance. This would only matter if Stop is called by ASP.Net - // on two different threads though, otherwise the current thread will just block normally - // until the app is shutdown - lock (Locker) - { - LogHelper.Info>("Application is shutting down immediately"); - } + // If the registered object does not complete processing before the application manager's time-out + // period expires, the Stop method is called again with the immediate parameter set to true. When the + // immediate parameter is true, the registered object must call the UnregisterObject method before returning; + // otherwise, its registration will be removed by the application manager. + + LogHelper.Info>("Shutting down immediately."); + Shutdown(true, true); // cancel all tasks, wait for the current one to end + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down."); } - } - } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs index c42fcd681a..4688ff37d6 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -1,23 +1,44 @@ namespace Umbraco.Web.Scheduling { + /// + /// Provides options to the class. + /// internal class BackgroundTaskRunnerOptions { //TODO: Could add options for using a stack vs queue if required + /// + /// Initializes a new instance of the class. + /// public BackgroundTaskRunnerOptions() { - DedicatedThread = false; - PersistentThread = false; - OnlyProcessLastItem = false; + LongRunning = false; + KeepAlive = false; + AutoStart = false; } - public bool DedicatedThread { get; set; } - public bool PersistentThread { get; set; } + /// + /// Gets or sets a value indicating whether the running task should be a long-running, + /// coarse grained operation. + /// + public bool LongRunning { get; set; } /// - /// If this is true, the task runner will skip over all items and only process the last/final - /// item registered + /// Gets or sets a value indicating whether the running task should block and wait + /// on the queue, or end, when the queue is empty. /// - public bool OnlyProcessLastItem { get; set; } + public bool KeepAlive { get; set; } + + /// + /// Gets or sets a value indicating whether the running task should start immediately + /// or only once a task has been added to the queue. + /// + public bool AutoStart { get; set; } + + /// + /// Gets or setes a value indicating whether the running task should be preserved + /// once completed, or reset to null. For unit tests. + /// + public bool PreserveRunningTask { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs new file mode 100644 index 0000000000..573adeda3d --- /dev/null +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Provides a base class for recurring background tasks. + /// + /// The type of the managed tasks. + internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, IDelayedBackgroundTask + where T : class, IBackgroundTask + { + private readonly int _delayMilliseconds; + private ManualResetEvent _gate; + private Timer _timer; + + protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + : base(runner, periodMilliseconds) + { + _delayMilliseconds = delayMilliseconds; + } + + protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) + : base(source) + { + _delayMilliseconds = 0; + } + + public WaitHandle DelayWaitHandle + { + get + { + if (_delayMilliseconds == 0) return new ManualResetEvent(true); + + if (_gate != null) return _gate; + _gate = new ManualResetEvent(false); + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _gate.Set(); + }, null, _delayMilliseconds, 0); + return _gate; + } + } + + public bool IsDelayed + { + get { return _delayMilliseconds > 0; } + } + } +} diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs index 48522aeb5f..9be2512d01 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -3,10 +3,26 @@ using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { + /// + /// Represents a background task. + /// internal interface IBackgroundTask : IDisposable { + /// + /// Runs the background task. + /// void Run(); + + /// + /// Runs the task asynchronously. + /// + /// A instance representing the execution of the background task. + /// The background task cannot run asynchronously. Task RunAsync(); + + /// + /// Indicates whether the background task can run asynchronously. + /// bool IsAsync { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs new file mode 100644 index 0000000000..c4e2dab35d --- /dev/null +++ b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs @@ -0,0 +1,20 @@ +using System; +using System.Web.Hosting; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Defines a service managing a queue of tasks of type and running them in the background. + /// + /// The type of the managed tasks. + /// The interface is not complete and exists only to have the contravariance on T. + internal interface IBackgroundTaskRunner : IDisposable, IRegisteredObject + where T : class, IBackgroundTask + { + bool IsCompleted { get; } + void Add(T task); + bool TryAdd(T task); + + // fixme - complete the interface? + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs new file mode 100644 index 0000000000..01f8a5e01a --- /dev/null +++ b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs @@ -0,0 +1,23 @@ +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Represents a delayed background task. + /// + /// Delayed background tasks can suspend their execution until + /// a condition is met. However if the tasks runner has to terminate, + /// delayed background tasks are executed immediately. + internal interface IDelayedBackgroundTask : IBackgroundTask + { + /// + /// Gets a wait handle on the task condition. + /// + WaitHandle DelayWaitHandle { get; } + + /// + /// Gets a value indicating whether the task is delayed. + /// + bool IsDelayed { get; } + } +} diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs new file mode 100644 index 0000000000..91d86d97b4 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -0,0 +1,108 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Provides a base class for recurring background tasks. + /// + /// The type of the managed tasks. + internal abstract class RecurringTaskBase : IBackgroundTask + where T : class, IBackgroundTask + { + private readonly IBackgroundTaskRunner _runner; + private readonly int _periodMilliseconds; + private Timer _timer; + + /// + /// Initializes a new instance of the class with a tasks runner and a period. + /// + /// The task runner. + /// The period. + /// The task will repeat itself periodically. Use this constructor to create a new task. + protected RecurringTaskBase(IBackgroundTaskRunner runner, int periodMilliseconds) + { + _runner = runner; + _periodMilliseconds = periodMilliseconds; + } + + /// + /// Initializes a new instance of the class with a source task. + /// + /// The source task. + /// Use this constructor to create a new task from a source task in GetRecurring. + protected RecurringTaskBase(RecurringTaskBase source) + { + _runner = source._runner; + _periodMilliseconds = source._periodMilliseconds; + } + + /// + /// Implements IBackgroundTask.Run(). + /// + /// Classes inheriting from RecurringTaskBase must implement PerformRun. + public void Run() + { + PerformRun(); + Repeat(); + } + + /// + /// Implements IBackgroundTask.RunAsync(). + /// + /// Classes inheriting from RecurringTaskBase must implement PerformRun. + public async Task RunAsync() + { + await PerformRunAsync(); + Repeat(); + } + + private void Repeat() + { + // again? + if (_runner.IsCompleted) return; // fail fast + + if (_periodMilliseconds == 0) return; + + var recur = GetRecurring(); + if (recur == null) return; // done + + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _runner.TryAdd(recur); + }, null, _periodMilliseconds, 0); + } + + /// + /// Indicates whether the background task can run asynchronously. + /// + public abstract bool IsAsync { get; } + + /// + /// Runs the background task. + /// + public abstract void PerformRun(); + + /// + /// Runs the task asynchronously. + /// + /// A instance representing the execution of the background task. + public abstract Task PerformRunAsync(); + + /// + /// Gets a new occurence of the recurring task. + /// + /// A new task instance to be queued, or null to terminate the recurring task. + /// The new task instance must be created via the RecurringTaskBase(RecurringTaskBase{T} source) constructor, + /// where source is the current task, eg: return new MyTask(this); + protected abstract T GetRecurring(); + + /// + /// Dispose the task. + /// + public virtual void Dispose() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs b/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs new file mode 100644 index 0000000000..a57ea904b0 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + internal static class TaskAndFactoryExtensions + { + #region Task Extensions + + static void SetCompletionSource(TaskCompletionSource completionSource, Task task) + { + if (task.IsFaulted) + completionSource.SetException(task.Exception.InnerException); + else + completionSource.SetResult(default(TResult)); + } + + static void SetCompletionSource(TaskCompletionSource completionSource, Task task) + { + if (task.IsFaulted) + completionSource.SetException(task.Exception.InnerException); + else + completionSource.SetResult(task.Result); + } + + public static Task ContinueWithTask(this Task task, Func continuation) + { + var completionSource = new TaskCompletionSource(); + task.ContinueWith(atask => continuation(atask).ContinueWith(atask2 => SetCompletionSource(completionSource, atask2))); + return completionSource.Task; + } + + public static Task ContinueWithTask(this Task task, Func continuation, CancellationToken token) + { + var completionSource = new TaskCompletionSource(); + task.ContinueWith(atask => continuation(atask).ContinueWith(atask2 => SetCompletionSource(completionSource, atask2), token), token); + return completionSource.Task; + } + + #endregion + + #region TaskFactory Extensions + + public static Task Completed(this TaskFactory factory) + { + var taskSource = new TaskCompletionSource(); + taskSource.SetResult(null); + return taskSource.Task; + } + + public static Task Sync(this TaskFactory factory, Action action) + { + var taskSource = new TaskCompletionSource(); + action(); + taskSource.SetResult(null); + return taskSource.Task; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 23ba69cf31..58821bec6b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -499,6 +499,11 @@ + + + + + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index cd736b98ff..cbf296115a 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -33,7 +33,7 @@ namespace umbraco public class content { private static readonly BackgroundTaskRunner FilePersister - = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions {DedicatedThread = true, OnlyProcessLastItem = true}); + = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true }); #region Declarations