diff --git a/src/Umbraco.Core/XmlExtensions.cs b/src/Umbraco.Core/XmlExtensions.cs index 02ebc07490..8784cbfb30 100644 --- a/src/Umbraco.Core/XmlExtensions.cs +++ b/src/Umbraco.Core/XmlExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; @@ -13,6 +16,31 @@ namespace Umbraco.Core /// internal static class XmlExtensions { + /// + /// Saves the xml document async + /// + /// + /// + /// + public static async Task SaveAsync(this XmlDocument xdoc, string filename) + { + if (xdoc.DocumentElement == null) + throw new XmlException("Cannot save xml document, there is no root element"); + + using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) + using (var xmlWriter = XmlWriter.Create(fs, new XmlWriterSettings + { + Async = true, + Encoding = Encoding.UTF8, + Indent = true + })) + { + //NOTE: There are no nice methods to write it async, only flushing it async. We + // could implement this ourselves but it'd be a very manual process. + xdoc.WriteTo(xmlWriter); + await xmlWriter.FlushAsync(); + } + } public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) { diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 8b1981bc9e..ab83294496 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,73 +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)); - } + AssertRunnerStopsRunning(runner); // though not for long } } @@ -99,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(); @@ -111,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 @@ -125,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() { @@ -205,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; @@ -226,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; @@ -241,41 +379,339 @@ namespace Umbraco.Tests.Scheduling Assert.IsFalse(tManager.IsDisposed); } } - + + [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 + + 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(CancellationToken token) + { + 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() + { + // 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 : ILatchedBackgroundTask + { + 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 Latch + { + get { return _gate; } + } + + public bool IsLatched + { + get { return true; } + } + + public bool RunsOnShutdown + { + get { return true; } + } + + public void Run() + { + Thread.Sleep(_runMilliseconds); + HasRun = true; + } + + public void Release() + { + _gate.Set(); + } + + public Task RunAsync(CancellationToken token) + { + 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 Run() + public override void PerformRun() { - Thread.Sleep(500); - } - - public override void Cancel() - { - + Thread.Sleep(_milliseconds); } } public abstract class BaseTask : IBackgroundTask { + public bool WasCancelled { get; set; } + public Guid UniqueId { get; protected set; } - public abstract void Run(); - public abstract void Cancel(); + public abstract void PerformRun(); + + public void Run() + { + PerformRun(); + Ended = DateTime.Now; + } + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + //return Task.Delay(500); + } + + public bool IsAsync + { + get { return false; } + } + + public virtual void Cancel() + { + WasCancelled = true; + } public DateTime Queued { get; set; } public DateTime Started { get; set; } public DateTime Ended { get; set; } public virtual void Dispose() - { - Ended = DateTime.Now; - } + { } } - } } diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 006dea76b0..246bff7cac 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - +