Merge remote-tracking branch 'origin/dev-v7-7.1.3-BetterXmlCacheFilePersisting' into 7.3.0
This commit is contained in:
@@ -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
|
||||
/// </summary>
|
||||
internal static class XmlExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the xml document async
|
||||
/// </summary>
|
||||
/// <param name="xdoc"></param>
|
||||
/// <param name="filename"></param>
|
||||
/// <returns></returns>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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<T>(BackgroundTaskRunner<T> runner, int timeoutMilliseconds = 2000)
|
||||
where T : class, IBackgroundTask
|
||||
{
|
||||
BackgroundTaskRunner<IBackgroundTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<IBackgroundTask>(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<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(true, true))
|
||||
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(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<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(true, true))
|
||||
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(new BackgroundTaskRunnerOptions()))
|
||||
{
|
||||
Assert.IsFalse(runner.IsRunning);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Create_AutoStart_IsRunning()
|
||||
{
|
||||
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask> runner;
|
||||
using (runner = new BackgroundTaskRunner<IBackgroundTask>(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true }))
|
||||
{
|
||||
Assert.IsTrue(runner.IsRunning);
|
||||
// dispose will stop it
|
||||
}
|
||||
|
||||
AssertRunnerStopsRunning(runner);
|
||||
Assert.Throws<InvalidOperationException>(() => runner.Add(new MyTask()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Startup_IsRunning()
|
||||
{
|
||||
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(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<IBackgroundTask>(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<BaseTask>(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<BaseTask>(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<BaseTask>(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<BaseTask>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
tasks.Add(new MyTask());
|
||||
|
||||
using (var runner = new BackgroundTaskRunner<BaseTask>(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<BaseTask>(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<BaseTask, ManualResetEvent>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(new MyTask(), new ManualResetEvent(false));
|
||||
}
|
||||
|
||||
BackgroundTaskRunner<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(true, true))
|
||||
using (var runner = new BackgroundTaskRunner<BaseTask>(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<BaseTask, ManualResetEvent> tasks = getTasks();
|
||||
|
||||
BackgroundTaskRunner<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(true, true))
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(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<BaseTask, ManualResetEvent>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(new MyTask(), new ManualResetEvent(false));
|
||||
}
|
||||
|
||||
BackgroundTaskRunner<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(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<BaseTask>();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(new MyTask());
|
||||
}
|
||||
|
||||
BackgroundTaskRunner<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(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<BaseTask> tasks = getTasks();
|
||||
|
||||
BackgroundTaskRunner<BaseTask> tManager;
|
||||
using (tManager = new BackgroundTaskRunner<BaseTask>(persistentThread: false, dedicatedThread: true))
|
||||
using (var tManager = new BackgroundTaskRunner<BaseTask>(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<IBackgroundTask> and use it in MyRecurringTask ctor
|
||||
// because that ctor wants IBackgroundTaskRunner<MyRecurringTask> and the generic type
|
||||
// parameter is contravariant ie defined as IBackgroundTaskRunner<in T> so doing the
|
||||
// following is legal:
|
||||
// var IBackgroundTaskRunner<Base> b = ...;
|
||||
// var IBackgroundTaskRunner<Derived> d = b; // legal
|
||||
|
||||
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(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<IBackgroundTask>(new BackgroundTaskRunnerOptions()))
|
||||
{
|
||||
var exceptions = new ConcurrentQueue<Exception>();
|
||||
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<IBackgroundTask>(new BackgroundTaskRunnerOptions()))
|
||||
{
|
||||
var exceptions = new ConcurrentQueue<Exception>();
|
||||
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<MyDelayedRecurringTask>
|
||||
{
|
||||
public MyDelayedRecurringTask(IBackgroundTaskRunner<MyDelayedRecurringTask> 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<MyRecurringTask>
|
||||
{
|
||||
private readonly int _runMilliseconds;
|
||||
|
||||
public static int RunCount { get; set; }
|
||||
|
||||
public MyRecurringTask(IBackgroundTaskRunner<MyRecurringTask> 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;
|
||||
}
|
||||
{ }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
-->
|
||||
<clientDependency version="68899455" fileDependencyExtensions=".js,.css" loggerType="Umbraco.Web.UI.CdfLogger, umbraco">
|
||||
<clientDependency version="366385329" fileDependencyExtensions=".js,.css" loggerType="Umbraco.Web.UI.CdfLogger, umbraco">
|
||||
|
||||
<!--
|
||||
This section is used for Web Forms only, the enableCompositeFiles="true" is optional and by default is set to true.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using umbraco;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Web.Scheduling;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the background task runner that persists the xml file to the file system
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used so that all file saving is done on a web aware worker background thread and all logic is performed async so this
|
||||
/// process will not interfere with any web requests threads. This is also done as to not require any global locks and to ensure that
|
||||
/// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting
|
||||
/// xml structure since the file writes are queued.
|
||||
/// </remarks>
|
||||
internal class XmlCacheFilePersister : ILatchedBackgroundTask
|
||||
{
|
||||
private readonly IBackgroundTaskRunner<XmlCacheFilePersister> _runner;
|
||||
private readonly string _xmlFileName;
|
||||
private readonly ProfilingLogger _logger;
|
||||
private readonly content _content;
|
||||
private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false);
|
||||
private readonly object _locko = new object();
|
||||
private bool _released;
|
||||
private Timer _timer;
|
||||
private DateTime _initialTouch;
|
||||
|
||||
private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min)
|
||||
private const int MaxWaitMilliseconds = 10000; // save the cache after some time (ie no more than 10s of changes)
|
||||
|
||||
// save the cache when the app goes down
|
||||
public bool RunsOnShutdown { get { return true; } }
|
||||
|
||||
public XmlCacheFilePersister(IBackgroundTaskRunner<XmlCacheFilePersister> runner, content content, string xmlFileName, ProfilingLogger logger, bool touched = false)
|
||||
{
|
||||
_runner = runner;
|
||||
_content = content;
|
||||
_xmlFileName = xmlFileName;
|
||||
_logger = logger;
|
||||
|
||||
if (touched == false) return;
|
||||
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Create new touched, start.");
|
||||
|
||||
_initialTouch = DateTime.Now;
|
||||
_timer = new Timer(_ => Release());
|
||||
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Save in {0}ms.", () => WaitMilliseconds);
|
||||
_timer.Change(WaitMilliseconds, 0);
|
||||
}
|
||||
|
||||
public XmlCacheFilePersister Touch()
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
if (_released)
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Touched, was released, create new.");
|
||||
|
||||
// released, has run or is running, too late, add & return a new task
|
||||
var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, _logger, true);
|
||||
_runner.Add(persister);
|
||||
return persister;
|
||||
}
|
||||
|
||||
if (_timer == null)
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Touched, was idle, start.");
|
||||
|
||||
// not started yet, start
|
||||
_initialTouch = DateTime.Now;
|
||||
_timer = new Timer(_ => Release());
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Save in {0}ms.", () => WaitMilliseconds);
|
||||
_timer.Change(WaitMilliseconds, 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
// set the timer to trigger in WaitMilliseconds unless we've been touched first more
|
||||
// than MaxWaitMilliseconds ago and then release now
|
||||
|
||||
if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds))
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Touched, was waiting, wait.", () => WaitMilliseconds);
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Save in {0}ms.", () => WaitMilliseconds);
|
||||
_timer.Change(WaitMilliseconds, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Save now, release.");
|
||||
ReleaseLocked();
|
||||
}
|
||||
|
||||
return this; // still available
|
||||
}
|
||||
}
|
||||
|
||||
private void Release()
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
ReleaseLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseLocked()
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Timer: save now, release.");
|
||||
if (_timer != null)
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
_released = true;
|
||||
_latch.Set();
|
||||
}
|
||||
|
||||
public WaitHandle Latch
|
||||
{
|
||||
get { return _latch.WaitHandle; }
|
||||
}
|
||||
|
||||
public bool IsLatched
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken token)
|
||||
{
|
||||
LogHelper.Debug<XmlCacheFilePersister>("Run now.");
|
||||
var doc = _content.XmlContentInternal;
|
||||
await PersistXmlToFileAsync(doc);
|
||||
}
|
||||
|
||||
public bool IsAsync
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist a XmlDocument to the Disk Cache
|
||||
/// </summary>
|
||||
/// <param name="xmlDoc"></param>
|
||||
internal async Task PersistXmlToFileAsync(XmlDocument xmlDoc)
|
||||
{
|
||||
if (xmlDoc != null)
|
||||
{
|
||||
using (_logger.DebugDuration<XmlCacheFilePersister>(
|
||||
string.Format("Saving content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread),
|
||||
string.Format("Saved content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to create directory for cache path if it doesn't yet exist
|
||||
var directoryName = Path.GetDirectoryName(_xmlFileName);
|
||||
// create dir if it is not there, if it's there, this will proceed as normal
|
||||
Directory.CreateDirectory(directoryName);
|
||||
|
||||
await xmlDoc.SaveAsync(_xmlFileName);
|
||||
}
|
||||
catch (Exception ee)
|
||||
{
|
||||
// If for whatever reason something goes wrong here, invalidate disk cache
|
||||
DeleteXmlCache();
|
||||
|
||||
LogHelper.Error<XmlCacheFilePersister>("Error saving content to disk", ee);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteXmlCache()
|
||||
{
|
||||
if (File.Exists(_xmlFileName) == false) return;
|
||||
|
||||
// Reset file attributes, to make sure we can delete file
|
||||
try
|
||||
{
|
||||
File.SetAttributes(_xmlFileName, FileAttributes.Normal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(_xmlFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{ }
|
||||
|
||||
public void Run()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,55 +9,97 @@ using Umbraco.Core.Logging;
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <typeparamref name="T"/> and runs them in the background.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal class BackgroundTaskRunner<T> : IDisposable, IRegisteredObject
|
||||
where T : IBackgroundTask
|
||||
/// <typeparam name="T">The type of the managed tasks.</typeparam>
|
||||
/// <remarks>The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain
|
||||
/// shuts down (ie is unloaded).</remarks>
|
||||
internal class BackgroundTaskRunner<T> : IBackgroundTaskRunner<T>
|
||||
where T : class, IBackgroundTask
|
||||
{
|
||||
private readonly bool _dedicatedThread;
|
||||
private readonly bool _persistentThread;
|
||||
private readonly BackgroundTaskRunnerOptions _options;
|
||||
private readonly BlockingCollection<T> _tasks = new BlockingCollection<T>();
|
||||
private Task _consumer;
|
||||
private readonly object _locker = new object();
|
||||
private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(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<TaskEventArgs<T>> TaskError;
|
||||
internal event EventHandler<TaskEventArgs<T>> TaskStarting;
|
||||
internal event EventHandler<TaskEventArgs<T>> TaskCompleted;
|
||||
internal event EventHandler<TaskEventArgs<T>> TaskCancelled;
|
||||
|
||||
public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class.
|
||||
/// </summary>
|
||||
public BackgroundTaskRunner()
|
||||
: this(new BackgroundTaskRunnerOptions())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundTaskRunner{T}"/> class with a set of options.
|
||||
/// </summary>
|
||||
/// <param name="options">The set of options.</param>
|
||||
public BackgroundTaskRunner(BackgroundTaskRunnerOptions options)
|
||||
{
|
||||
_dedicatedThread = dedicatedThread;
|
||||
_persistentThread = persistentThread;
|
||||
if (options == null) throw new ArgumentNullException("options");
|
||||
_options = options;
|
||||
|
||||
HostingEnvironment.RegisterObject(this);
|
||||
|
||||
if (options.AutoStart)
|
||||
StartUp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of tasks in the queue.
|
||||
/// </summary>
|
||||
public int TaskCount
|
||||
{
|
||||
get { return _tasks.Count; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a task is currently running.
|
||||
/// </summary>
|
||||
public bool IsRunning
|
||||
{
|
||||
get { return _isRunning; }
|
||||
}
|
||||
|
||||
public TaskStatus TaskStatus
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the runner has completed and cannot accept tasks anymore.
|
||||
/// </summary>
|
||||
public bool IsCompleted
|
||||
{
|
||||
get { return _consumer.Status; }
|
||||
get { return _isCompleted; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status of the running task.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">There is no running task.</exception>
|
||||
/// <remarks>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.</remarks>
|
||||
public TaskStatus TaskStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_runningTask == null)
|
||||
throw new InvalidOperationException("There is no current task.");
|
||||
return _runningTask.Status;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the task awaiter so that consumers of the BackgroundTaskManager can await
|
||||
/// the threading operation.
|
||||
/// Gets an awaiter used to await the running task.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>An awaiter for the running task.</returns>
|
||||
/// <remarks>
|
||||
/// This is just the coolest thing ever, check this article out:
|
||||
/// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx
|
||||
@@ -65,237 +107,292 @@ namespace Umbraco.Web.Scheduling
|
||||
/// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion
|
||||
/// we can await anything! :)
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException">There is no running task.</exception>
|
||||
/// <remarks>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.</remarks>
|
||||
public TaskAwaiter GetAwaiter()
|
||||
{
|
||||
return _consumer.GetAwaiter();
|
||||
if (_runningTask == null)
|
||||
throw new InvalidOperationException("There is no current task.");
|
||||
return _runningTask.GetAwaiter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a task to the queue.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to add.</param>
|
||||
/// <exception cref="InvalidOperationException">The task runner has completed.</exception>
|
||||
public void Add(T task)
|
||||
{
|
||||
//add any tasks first
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>(" 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<BackgroundTaskRunner<T>>("Task added {0}", task.GetType);
|
||||
_tasks.Add(task);
|
||||
|
||||
// start
|
||||
StartUpLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a task to the queue.
|
||||
/// </summary>
|
||||
/// <param name="task">The task to add.</param>
|
||||
/// <returns>true if the task could be added to the queue; otherwise false.</returns>
|
||||
/// <remarks>Returns false if the runner is completed.</remarks>
|
||||
public bool TryAdd(T task)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
if (_isCompleted) return false;
|
||||
|
||||
// add task
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>("Task added {0}", task.GetType);
|
||||
_tasks.Add(task);
|
||||
|
||||
// start
|
||||
StartUpLocked();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the tasks runner, if not already running.
|
||||
/// </summary>
|
||||
/// <remarks>Is invoked each time a task is added, to ensure it is going to be processed.</remarks>
|
||||
/// <exception cref="InvalidOperationException">The task runner has completed.</exception>
|
||||
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<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>(" .Net TaskCanceledException: .Net Task ID {0}", () => exception.Task.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>(" Exception: {0}", () => v.GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_tasks.Count > 0)
|
||||
{
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>("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))
|
||||
{
|
||||
ConsumeTaskInternal(remainingTask);
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("Error occurred shutting down task runner", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
HostingEnvironment.UnregisterObject(this);
|
||||
}
|
||||
StartUpLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the consumer task
|
||||
/// Starts the tasks runner, if not already running.
|
||||
/// </summary>
|
||||
private void StartConsumer()
|
||||
/// <remarks>Must be invoked within lock(_locker) and with _isCompleted being false.</remarks>
|
||||
private void StartUpLocked()
|
||||
{
|
||||
var token = _tokenSource.Token;
|
||||
// double check
|
||||
if (_isRunning) return;
|
||||
_isRunning = true;
|
||||
|
||||
_consumer = Task.Factory.StartNew(() =>
|
||||
StartThread(token),
|
||||
// create a new token source since this is a new process
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token);
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>("Starting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts the taks runner down.
|
||||
/// </summary>
|
||||
/// <param name="force">True for force the runner to stop.</param>
|
||||
/// <param name="wait">True to wait until the runner has stopped.</param>
|
||||
/// <remarks>If <paramref name="force"/> 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.</remarks>
|
||||
public void Shutdown(bool force, bool wait)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
_isCompleted = true; // do not accept new tasks
|
||||
if (_isRunning == false) return; // done already
|
||||
}
|
||||
|
||||
// try to be nice
|
||||
// assuming multiple threads can do these without problems
|
||||
_completedEvent.Set();
|
||||
_tasks.CompleteAdding();
|
||||
|
||||
if (force)
|
||||
{
|
||||
// we must bring everything down, now
|
||||
Thread.Sleep(100); // give time to CompleAdding()
|
||||
lock (_locker)
|
||||
{
|
||||
// was CompleteAdding() enough?
|
||||
if (_isRunning == false) return;
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// tasks in the queue will be executed...
|
||||
if (wait == false) return;
|
||||
_runningTask.Wait(); // wait for whatever is running to end...
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs background tasks for as long as there are background tasks in the queue, with an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="factory">The supporting <see cref="TaskFactory"/>.</param>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
/// <returns>The asynchronous operation.</returns>
|
||||
private Task PumpIBackgroundTasks(TaskFactory factory, CancellationToken token)
|
||||
{
|
||||
var taskSource = new TaskCompletionSource<object>(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<Task> 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<BackgroundTaskRunner<T>>("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 latched task, supporting cancellation
|
||||
var dbgTask = bgTask as ILatchedBackgroundTask;
|
||||
if (dbgTask != null && dbgTask.IsLatched)
|
||||
{
|
||||
WaitHandle.WaitAny(new[] { dbgTask.Latch, token.WaitHandle, _completedEvent.WaitHandle });
|
||||
if (TaskSourceCanceled(taskSource, token)) return;
|
||||
// else run now, either because latch ok or runner is completed
|
||||
// still latched & not running on shutdown = stop here
|
||||
if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false)
|
||||
{
|
||||
TaskSourceCompleted(taskSource, token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
_dedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None,
|
||||
_options.LongRunning ? 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 (!_persistentThread)
|
||||
{
|
||||
_consumer.ContinueWith(task => ShutDown());
|
||||
}
|
||||
|
||||
return taskSourceContinuing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes a new worker thread to consume tasks
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
private void StartThread(CancellationToken token)
|
||||
{
|
||||
// Was cancellation already requested?
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
LogHelper.Info<BackgroundTaskRunner<T>>("Thread {0} was cancelled before it got started.", () => Thread.CurrentThread.ManagedThreadId);
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
TakeAndConsumeTask(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trys to get a task from the queue, if there isn't one it will wait a second and try again
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
private void TakeAndConsumeTask(CancellationToken token)
|
||||
private bool TaskSourceCanceled(TaskCompletionSource<object> taskSource, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
LogHelper.Info<BackgroundTaskRunner<T>>("Thread {0} was cancelled.", () => Thread.CurrentThread.ManagedThreadId);
|
||||
token.ThrowIfCancellationRequested();
|
||||
taskSource.SetCanceled();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//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 (_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))
|
||||
{
|
||||
ConsumeTaskCancellable(t, token);
|
||||
}
|
||||
|
||||
//recurse and keep going
|
||||
TakeAndConsumeTask(token);
|
||||
}
|
||||
private void TaskSourceCompleted(TaskCompletionSource<object> taskSource, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
taskSource.SetCanceled();
|
||||
else
|
||||
{
|
||||
T repositoryTask;
|
||||
while (_tasks.TryTake(out repositoryTask))
|
||||
{
|
||||
ConsumeTaskCancellable(repositoryTask, token);
|
||||
}
|
||||
|
||||
//the task will end here
|
||||
}
|
||||
taskSource.SetResult(null);
|
||||
}
|
||||
|
||||
internal void ConsumeTaskCancellable(T task, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
OnTaskCancelled(new TaskEventArgs<T>(task));
|
||||
|
||||
//NOTE: Since the task hasn't started this is pretty pointless so leaving it out.
|
||||
LogHelper.Info<BackgroundTaskRunner<T>>("Task {0}) was cancelled.",
|
||||
() => task.GetType());
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
ConsumeTaskInternal(task);
|
||||
}
|
||||
|
||||
private void ConsumeTaskInternal(T task)
|
||||
/// <summary>
|
||||
/// Runs a background task asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="bgTask">The background task.</param>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
/// <returns>The asynchronous operation.</returns>
|
||||
internal async Task RunIBackgroundTaskAsync(T bgTask, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnTaskStarting(new TaskEventArgs<T>(task));
|
||||
OnTaskStarting(new TaskEventArgs<T>(bgTask));
|
||||
|
||||
try
|
||||
{
|
||||
using (task)
|
||||
using (bgTask) // ensure it's disposed
|
||||
{
|
||||
task.Run();
|
||||
if (bgTask.IsAsync)
|
||||
await bgTask.RunAsync(token);
|
||||
else
|
||||
bgTask.Run();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
OnTaskError(new TaskEventArgs<T>(task, e));
|
||||
OnTaskError(new TaskEventArgs<T>(bgTask, e));
|
||||
throw;
|
||||
}
|
||||
|
||||
OnTaskCompleted(new TaskEventArgs<T>(task));
|
||||
OnTaskCompleted(new TaskEventArgs<T>(bgTask));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogHelper.Error<BackgroundTaskRunner<T>>("An error occurred consuming task", ex);
|
||||
LogHelper.Error<BackgroundTaskRunner<T>>("Task has failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
protected virtual void OnTaskError(TaskEventArgs<T> e)
|
||||
{
|
||||
var handler = TaskError;
|
||||
@@ -318,10 +415,15 @@ namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
var handler = TaskCancelled;
|
||||
if (handler != null) handler(this, e);
|
||||
|
||||
//dispose it
|
||||
e.Task.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
#region Disposal
|
||||
private readonly object _disposalLocker = new object();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
@@ -338,8 +440,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)
|
||||
@@ -351,31 +454,60 @@ namespace Umbraco.Web.Scheduling
|
||||
|
||||
protected virtual void DisposeResources()
|
||||
{
|
||||
ShutDown();
|
||||
// just make sure we eventually go down
|
||||
Shutdown(true, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Requests a registered object to unregister.
|
||||
/// </summary>
|
||||
/// <param name="immediate">true to indicate the registered object should unregister from the hosting
|
||||
/// environment before returning; otherwise, false.</param>
|
||||
/// <remarks>
|
||||
/// <para>"When the application manager needs to stop a registered object, it will call the Stop method."</para>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
public void Stop(bool immediate)
|
||||
{
|
||||
if (immediate == false)
|
||||
{
|
||||
LogHelper.Debug<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("Down, tasks completed.");
|
||||
});
|
||||
else
|
||||
{
|
||||
HostingEnvironment.UnregisterObject(this);
|
||||
LogHelper.Info<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("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<BackgroundTaskRunner<T>>("Shutting down immediately.");
|
||||
Shutdown(true, true); // cancel all tasks, wait for the current one to end
|
||||
HostingEnvironment.UnregisterObject(this);
|
||||
LogHelper.Info<BackgroundTaskRunner<T>>("Down.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides options to the <see cref="BackgroundTaskRunner{T}"/> class.
|
||||
/// </summary>
|
||||
internal class BackgroundTaskRunnerOptions
|
||||
{
|
||||
//TODO: Could add options for using a stack vs queue if required
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundTaskRunnerOptions"/> class.
|
||||
/// </summary>
|
||||
public BackgroundTaskRunnerOptions()
|
||||
{
|
||||
LongRunning = false;
|
||||
KeepAlive = false;
|
||||
AutoStart = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the running task should be a long-running,
|
||||
/// coarse grained operation.
|
||||
/// </summary>
|
||||
public bool LongRunning { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the running task should block and wait
|
||||
/// on the queue, or end, when the queue is empty.
|
||||
/// </summary>
|
||||
public bool KeepAlive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the running task should start immediately
|
||||
/// or only once a task has been added to the queue.
|
||||
/// </summary>
|
||||
public bool AutoStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or setes a value indicating whether the running task should be preserved
|
||||
/// once completed, or reset to null. For unit tests.
|
||||
/// </summary>
|
||||
public bool PreserveRunningTask { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base class for recurring background tasks.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the managed tasks.</typeparam>
|
||||
internal abstract class DelayedRecurringTaskBase<T> : RecurringTaskBase<T>, ILatchedBackgroundTask
|
||||
where T : class, IBackgroundTask
|
||||
{
|
||||
private readonly ManualResetEventSlim _latch;
|
||||
private Timer _timer;
|
||||
|
||||
protected DelayedRecurringTaskBase(IBackgroundTaskRunner<T> runner, int delayMilliseconds, int periodMilliseconds)
|
||||
: base(runner, periodMilliseconds)
|
||||
{
|
||||
if (delayMilliseconds > 0)
|
||||
{
|
||||
_latch = new ManualResetEventSlim(false);
|
||||
_timer = new Timer(_ =>
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
_latch.Set();
|
||||
});
|
||||
_timer.Change(delayMilliseconds, 0);
|
||||
}
|
||||
}
|
||||
|
||||
protected DelayedRecurringTaskBase(DelayedRecurringTaskBase<T> source)
|
||||
: base(source)
|
||||
{
|
||||
// no latch on recurring instances
|
||||
_latch = null;
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
if (_latch != null)
|
||||
_latch.Dispose();
|
||||
base.Run();
|
||||
}
|
||||
|
||||
public override async Task RunAsync(CancellationToken token)
|
||||
{
|
||||
if (_latch != null)
|
||||
_latch.Dispose();
|
||||
await base.RunAsync(token);
|
||||
}
|
||||
|
||||
public WaitHandle Latch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_latch == null)
|
||||
throw new InvalidOperationException("The task is not latched.");
|
||||
return _latch.WaitHandle;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLatched
|
||||
{
|
||||
get { return _latch != null; }
|
||||
}
|
||||
|
||||
public virtual bool RunsOnShutdown
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,30 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a background task.
|
||||
/// </summary>
|
||||
internal interface IBackgroundTask : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the background task.
|
||||
/// </summary>
|
||||
void Run();
|
||||
|
||||
/// <summary>
|
||||
/// Runs the task asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
/// <returns>A <see cref="Task"/> instance representing the execution of the background task.</returns>
|
||||
/// <exception cref="NotImplementedException">The background task cannot run asynchronously.</exception>
|
||||
Task RunAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the background task can run asynchronously.
|
||||
/// </summary>
|
||||
bool IsAsync { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Web.Hosting;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a service managing a queue of tasks of type <typeparamref name="T"/> and running them in the background.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the managed tasks.</typeparam>
|
||||
/// <remarks>The interface is not complete and exists only to have the contravariance on T.</remarks>
|
||||
internal interface IBackgroundTaskRunner<in T> : IDisposable, IRegisteredObject
|
||||
where T : class, IBackgroundTask
|
||||
{
|
||||
bool IsCompleted { get; }
|
||||
void Add(T task);
|
||||
bool TryAdd(T task);
|
||||
|
||||
// fixme - complete the interface?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a latched background task.
|
||||
/// </summary>
|
||||
/// <remarks>Latched background tasks can suspend their execution until
|
||||
/// a condition is met. However if the tasks runner has to terminate,
|
||||
/// latched background tasks can be executed immediately, depending on
|
||||
/// the value returned by RunsOnShutdown.</remarks>
|
||||
internal interface ILatchedBackgroundTask : IBackgroundTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a wait handle on the task condition.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">The task is not latched.</exception>
|
||||
WaitHandle Latch { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the task is latched.
|
||||
/// </summary>
|
||||
bool IsLatched { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the task can be executed immediately if the task runner has to terminate.
|
||||
/// </summary>
|
||||
bool RunsOnShutdown { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Caching;
|
||||
using umbraco.BusinessLogic;
|
||||
@@ -8,17 +9,31 @@ using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
internal class LogScrubber : DisposableObject, IBackgroundTask
|
||||
internal class LogScrubber : DelayedRecurringTaskBase<LogScrubber>
|
||||
{
|
||||
private readonly ApplicationContext _appContext;
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
|
||||
public LogScrubber(ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
public LogScrubber(IBackgroundTaskRunner<LogScrubber> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_appContext = appContext;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public LogScrubber(LogScrubber source)
|
||||
: base(source)
|
||||
{
|
||||
_appContext = source._appContext;
|
||||
_settings = source._settings;
|
||||
}
|
||||
|
||||
protected override LogScrubber GetRecurring()
|
||||
{
|
||||
return new LogScrubber(this);
|
||||
}
|
||||
|
||||
private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings)
|
||||
{
|
||||
int maximumAge = 24 * 60 * 60;
|
||||
@@ -35,19 +50,42 @@ namespace Umbraco.Web.Scheduling
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
|
||||
/// </summary>
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
public static int GetLogScrubbingInterval(IUmbracoSettingsSection settings)
|
||||
{
|
||||
int interval = 24 * 60 * 60; //24 hours
|
||||
try
|
||||
{
|
||||
if (settings.Logging.CleaningMiliseconds > -1)
|
||||
interval = settings.Logging.CleaningMiliseconds;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogHelper.Error<LogScrubber>("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e);
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
public override void PerformRun()
|
||||
{
|
||||
using (DisposableTimer.DebugDuration<LogScrubber>(() => "Log scrubbing executing", () => "Log scrubbing complete"))
|
||||
{
|
||||
Log.CleanLogs(GetLogScrubbingMaximumAge(_settings));
|
||||
}
|
||||
}
|
||||
|
||||
public override Task PerformRunAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool IsAsync
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool RunsOnShutdown
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base class for recurring background tasks.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the managed tasks.</typeparam>
|
||||
internal abstract class RecurringTaskBase<T> : IBackgroundTask
|
||||
where T : class, IBackgroundTask
|
||||
{
|
||||
private readonly IBackgroundTaskRunner<T> _runner;
|
||||
private readonly int _periodMilliseconds;
|
||||
private Timer _timer;
|
||||
private T _recurrent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecurringTaskBase{T}"/> class with a tasks runner and a period.
|
||||
/// </summary>
|
||||
/// <param name="runner">The task runner.</param>
|
||||
/// <param name="periodMilliseconds">The period.</param>
|
||||
/// <remarks>The task will repeat itself periodically. Use this constructor to create a new task.</remarks>
|
||||
protected RecurringTaskBase(IBackgroundTaskRunner<T> runner, int periodMilliseconds)
|
||||
{
|
||||
_runner = runner;
|
||||
_periodMilliseconds = periodMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecurringTaskBase{T}"/> class with a source task.
|
||||
/// </summary>
|
||||
/// <param name="source">The source task.</param>
|
||||
/// <remarks>Use this constructor to create a new task from a source task in <c>GetRecurring</c>.</remarks>
|
||||
protected RecurringTaskBase(RecurringTaskBase<T> source)
|
||||
{
|
||||
_runner = source._runner;
|
||||
_timer = source._timer;
|
||||
_periodMilliseconds = source._periodMilliseconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IBackgroundTask.Run().
|
||||
/// </summary>
|
||||
/// <remarks>Classes inheriting from <c>RecurringTaskBase</c> must implement <c>PerformRun</c>.</remarks>
|
||||
public virtual void Run()
|
||||
{
|
||||
PerformRun();
|
||||
Repeat();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IBackgroundTask.RunAsync().
|
||||
/// </summary>
|
||||
/// <remarks>Classes inheriting from <c>RecurringTaskBase</c> must implement <c>PerformRun</c>.</remarks>
|
||||
public virtual async Task RunAsync(CancellationToken token)
|
||||
{
|
||||
await PerformRunAsync();
|
||||
Repeat();
|
||||
}
|
||||
|
||||
private void Repeat()
|
||||
{
|
||||
// again?
|
||||
if (_runner.IsCompleted) return; // fail fast
|
||||
|
||||
if (_periodMilliseconds == 0) return;
|
||||
|
||||
_recurrent = GetRecurring();
|
||||
if (_recurrent == null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
return; // done
|
||||
}
|
||||
|
||||
// note
|
||||
// must use the single-parameter constructor on Timer to avoid it from being GC'd
|
||||
// read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer
|
||||
|
||||
_timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent));
|
||||
_timer.Change(_periodMilliseconds, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the background task can run asynchronously.
|
||||
/// </summary>
|
||||
public abstract bool IsAsync { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs the background task.
|
||||
/// </summary>
|
||||
public abstract void PerformRun();
|
||||
|
||||
/// <summary>
|
||||
/// Runs the task asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> instance representing the execution of the background task.</returns>
|
||||
public abstract Task PerformRunAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a new occurence of the recurring task.
|
||||
/// </summary>
|
||||
/// <returns>A new task instance to be queued, or <c>null</c> to terminate the recurring task.</returns>
|
||||
/// <remarks>The new task instance must be created via the <c>RecurringTaskBase(RecurringTaskBase{T} source)</c> constructor,
|
||||
/// where <c>source</c> is the current task, eg: <c>return new MyTask(this);</c></remarks>
|
||||
protected abstract T GetRecurring();
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the task.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Logging;
|
||||
@@ -11,30 +12,41 @@ using Umbraco.Web.Mvc;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
internal class ScheduledPublishing : DisposableObject, IBackgroundTask
|
||||
internal class ScheduledPublishing : DelayedRecurringTaskBase<ScheduledPublishing>
|
||||
{
|
||||
private readonly ApplicationContext _appContext;
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
|
||||
private static bool _isPublishingRunning = false;
|
||||
private static bool _isPublishingRunning;
|
||||
|
||||
public ScheduledPublishing(ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
public ScheduledPublishing(IBackgroundTaskRunner<ScheduledPublishing> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_appContext = appContext;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
|
||||
/// </summary>
|
||||
protected override void DisposeResources()
|
||||
private ScheduledPublishing(ScheduledPublishing source)
|
||||
: base(source)
|
||||
{
|
||||
_appContext = source._appContext;
|
||||
_settings = source._settings;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
protected override ScheduledPublishing GetRecurring()
|
||||
{
|
||||
return new ScheduledPublishing(this);
|
||||
}
|
||||
|
||||
public override void PerformRun()
|
||||
{
|
||||
if (_appContext == null) return;
|
||||
if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave)
|
||||
{
|
||||
LogHelper.Debug<ScheduledPublishing>("Does not run on slave servers.");
|
||||
return;
|
||||
}
|
||||
|
||||
using (DisposableTimer.DebugDuration<ScheduledPublishing>(() => "Scheduled publishing executing", () => "Scheduled publishing complete"))
|
||||
{
|
||||
@@ -75,5 +87,20 @@ namespace Umbraco.Web.Scheduling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Task PerformRunAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool IsAsync
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool RunsOnShutdown
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
@@ -16,19 +17,33 @@ namespace Umbraco.Web.Scheduling
|
||||
// would need to be a publicly available task (URL) which isn't really very good :(
|
||||
// We should really be using the AdminTokenAuthorizeAttribute for this stuff
|
||||
|
||||
internal class ScheduledTasks : DisposableObject, IBackgroundTask
|
||||
internal class ScheduledTasks : DelayedRecurringTaskBase<ScheduledTasks>
|
||||
{
|
||||
private readonly ApplicationContext _appContext;
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private static readonly Hashtable ScheduledTaskTimes = new Hashtable();
|
||||
private static bool _isPublishingRunning = false;
|
||||
|
||||
public ScheduledTasks(ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
public ScheduledTasks(IBackgroundTaskRunner<ScheduledTasks> runner, int delayMilliseconds, int periodMilliseconds,
|
||||
ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
: base(runner, delayMilliseconds, periodMilliseconds)
|
||||
{
|
||||
_appContext = appContext;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public ScheduledTasks(ScheduledTasks source)
|
||||
: base(source)
|
||||
{
|
||||
_appContext = source._appContext;
|
||||
_settings = source._settings;
|
||||
}
|
||||
|
||||
protected override ScheduledTasks GetRecurring()
|
||||
{
|
||||
return new ScheduledTasks(this);
|
||||
}
|
||||
|
||||
private void ProcessTasks()
|
||||
{
|
||||
var scheduledTasks = _settings.ScheduledTasks.Tasks;
|
||||
@@ -77,15 +92,14 @@ namespace Umbraco.Web.Scheduling
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
|
||||
/// </summary>
|
||||
protected override void DisposeResources()
|
||||
public override void PerformRun()
|
||||
{
|
||||
}
|
||||
if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave)
|
||||
{
|
||||
LogHelper.Debug<ScheduledTasks>("Does not run on slave servers.");
|
||||
return;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
using (DisposableTimer.DebugDuration<ScheduledTasks>(() => "Scheduled tasks executing", () => "Scheduled tasks complete"))
|
||||
{
|
||||
if (_isPublishingRunning) return;
|
||||
@@ -106,5 +120,20 @@ namespace Umbraco.Web.Scheduling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Task PerformRunAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool IsAsync
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool RunsOnShutdown
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,10 @@ namespace Umbraco.Web.Scheduling
|
||||
internal sealed class Scheduler : ApplicationEventHandler
|
||||
{
|
||||
private static Timer _pingTimer;
|
||||
private static Timer _schedulingTimer;
|
||||
private static BackgroundTaskRunner<ScheduledPublishing> _publishingRunner;
|
||||
private static BackgroundTaskRunner<ScheduledTasks> _tasksRunner;
|
||||
private static BackgroundTaskRunner<LogScrubber> _scrubberRunner;
|
||||
private static Timer _logScrubberTimer;
|
||||
private static volatile bool _started = false;
|
||||
private static BackgroundTaskRunner<IBackgroundTask> _publishingRunner;
|
||||
private static BackgroundTaskRunner<IBackgroundTask> _tasksRunner;
|
||||
private static BackgroundTaskRunner<IBackgroundTask> _scrubberRunner;
|
||||
private static volatile bool _started;
|
||||
private static readonly object Locker = new object();
|
||||
|
||||
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
|
||||
@@ -49,97 +47,37 @@ namespace Umbraco.Web.Scheduling
|
||||
_started = true;
|
||||
LogHelper.Debug<Scheduler>(() => "Initializing the scheduler");
|
||||
|
||||
// time to setup the tasks
|
||||
// backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly
|
||||
_publishingRunner = new BackgroundTaskRunner<IBackgroundTask>();
|
||||
_tasksRunner = new BackgroundTaskRunner<IBackgroundTask>();
|
||||
_scrubberRunner = new BackgroundTaskRunner<IBackgroundTask>();
|
||||
|
||||
//We have 3 background runners that are web aware, if the app domain dies, these tasks will wind down correctly
|
||||
_publishingRunner = new BackgroundTaskRunner<ScheduledPublishing>();
|
||||
_tasksRunner = new BackgroundTaskRunner<ScheduledTasks>();
|
||||
_scrubberRunner = new BackgroundTaskRunner<LogScrubber>();
|
||||
var settings = UmbracoConfig.For.UmbracoSettings();
|
||||
|
||||
//NOTE: It is important to note that we need to use the ctor for a timer without the 'state' object specified, this is in order
|
||||
// to ensure that the timer itself is not GC'd since internally .net will pass itself in as the state object and that will keep it alive.
|
||||
// There's references to this here: http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer
|
||||
// we also make these timers static to ensure further GC safety.
|
||||
// note
|
||||
// must use the single-parameter constructor on Timer to avoid it from being GC'd
|
||||
// also make the timer static to ensure further GC safety
|
||||
// read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer
|
||||
|
||||
// ping/keepalive - NOTE: we don't use a background runner for this because it does not need to be web aware, if the app domain dies, no problem
|
||||
// ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies
|
||||
_pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings()));
|
||||
_pingTimer.Change(60000, 300000);
|
||||
|
||||
// scheduled publishing/unpublishing
|
||||
_schedulingTimer = new Timer(state => PerformScheduling(applicationContext, UmbracoConfig.For.UmbracoSettings()));
|
||||
_schedulingTimer.Change(60000, 60000);
|
||||
// install on all, will only run on non-slaves servers
|
||||
// both are delayed recurring tasks
|
||||
_publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings));
|
||||
_tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings));
|
||||
|
||||
//log scrubbing
|
||||
_logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext, UmbracoConfig.For.UmbracoSettings()));
|
||||
_logScrubberTimer.Change(60000, GetLogScrubbingInterval(UmbracoConfig.For.UmbracoSettings()));
|
||||
// log scrubbing
|
||||
// install & run on all servers
|
||||
// LogScrubber is a delayed recurring task
|
||||
_scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private int GetLogScrubbingInterval(IUmbracoSettingsSection settings)
|
||||
{
|
||||
int interval = 24 * 60 * 60; //24 hours
|
||||
try
|
||||
{
|
||||
if (settings.Logging.CleaningMiliseconds > -1)
|
||||
interval = settings.Logging.CleaningMiliseconds;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogHelper.Error<Scheduler>("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e);
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
private static void PerformLogScrub(ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
{
|
||||
_scrubberRunner.Add(new LogScrubber(appContext, settings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This performs all of the scheduling on the one timer
|
||||
/// </summary>
|
||||
/// <param name="appContext"></param>
|
||||
/// <param name="settings"></param>
|
||||
/// <remarks>
|
||||
/// No processing will be done if this server is a slave
|
||||
/// </remarks>
|
||||
private static void PerformScheduling(ApplicationContext appContext, IUmbracoSettingsSection settings)
|
||||
{
|
||||
using (DisposableTimer.DebugDuration<Scheduler>(() => "Scheduling interval executing", () => "Scheduling interval complete"))
|
||||
{
|
||||
//get the current server status to see if this server should execute the scheduled publishing
|
||||
var serverStatus = ServerEnvironmentHelper.GetStatus(settings);
|
||||
|
||||
switch (serverStatus)
|
||||
{
|
||||
case CurrentServerEnvironmentStatus.Single:
|
||||
case CurrentServerEnvironmentStatus.Master:
|
||||
case CurrentServerEnvironmentStatus.Unknown:
|
||||
//if it's a single server install, a master or it cannot be determined
|
||||
// then we will process the scheduling
|
||||
|
||||
//do the scheduled publishing
|
||||
_publishingRunner.Add(new ScheduledPublishing(appContext, settings));
|
||||
|
||||
//do the scheduled tasks
|
||||
_tasksRunner.Add(new ScheduledTasks(appContext, settings));
|
||||
|
||||
break;
|
||||
case CurrentServerEnvironmentStatus.Slave:
|
||||
//do not process
|
||||
|
||||
LogHelper.Debug<Scheduler>(
|
||||
() => string.Format("Current server ({0}) detected as a slave, no scheduled processes will execute on this server", NetworkHelper.MachineName));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TResult>(TaskCompletionSource<TResult> completionSource, Task task)
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
completionSource.SetException(task.Exception.InnerException);
|
||||
else
|
||||
completionSource.SetResult(default(TResult));
|
||||
}
|
||||
|
||||
static void SetCompletionSource<TResult>(TaskCompletionSource<TResult> completionSource, Task<TResult> task)
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
completionSource.SetException(task.Exception.InnerException);
|
||||
else
|
||||
completionSource.SetResult(task.Result);
|
||||
}
|
||||
|
||||
public static Task ContinueWithTask(this Task task, Func<Task, Task> continuation)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<object>();
|
||||
task.ContinueWith(atask => continuation(atask).ContinueWith(atask2 => SetCompletionSource(completionSource, atask2)));
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
public static Task ContinueWithTask(this Task task, Func<Task, Task> continuation, CancellationToken token)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<object>();
|
||||
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<object>();
|
||||
taskSource.SetResult(null);
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
public static Task Sync(this TaskFactory factory, Action action)
|
||||
{
|
||||
var taskSource = new TaskCompletionSource<object>();
|
||||
action();
|
||||
taskSource.SetResult(null);
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -506,6 +506,12 @@
|
||||
<Compile Include="Mvc\UmbracoVirtualNodeRouteHandler.cs" />
|
||||
<Compile Include="Routing\CustomRouteUrlProvider.cs" />
|
||||
<Compile Include="Routing\UrlProviderExtensions.cs" />
|
||||
<Compile Include="Scheduling\BackgroundTaskRunnerOptions.cs" />
|
||||
<Compile Include="Scheduling\DelayedRecurringTaskBase.cs" />
|
||||
<Compile Include="Scheduling\IBackgroundTaskRunner.cs" />
|
||||
<Compile Include="Scheduling\ILatchedBackgroundTask.cs" />
|
||||
<Compile Include="Scheduling\RecurringTaskBase.cs" />
|
||||
<Compile Include="Scheduling\TaskAndFactoryExtensions.cs" />
|
||||
<Compile Include="Strategies\Migrations\ClearCsrfCookiesAfterUpgrade.cs" />
|
||||
<Compile Include="Strategies\Migrations\ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs" />
|
||||
<Compile Include="Strategies\Migrations\EnsureListViewDataTypeIsCreated.cs" />
|
||||
@@ -555,6 +561,7 @@
|
||||
<SubType>ASPXCodeBehind</SubType>
|
||||
</Compile>
|
||||
<Compile Include="umbraco.presentation\umbraco\Trees\loadPackager.cs" />
|
||||
<Compile Include="PublishedCache\XmlPublishedCache\XmlCacheFilePersister.cs" />
|
||||
<Compile Include="Web References\org.umbraco.our\Reference.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
@@ -529,24 +530,8 @@ namespace Umbraco.Web
|
||||
urlRouting.PostResolveRequestCache(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the xml cache file needs to be updated/persisted
|
||||
/// </summary>
|
||||
/// <param name="httpContext"></param>
|
||||
/// <remarks>
|
||||
/// TODO: This needs an overhaul, see the error report created here:
|
||||
/// https://docs.google.com/document/d/1neGE3q3grB4lVJfgID1keWY2v9JYqf-pw75sxUUJiyo/edit
|
||||
/// </remarks>
|
||||
static void PersistXmlCache(HttpContextBase httpContext)
|
||||
{
|
||||
if (content.Instance.IsXmlQueuedForPersistenceToFile)
|
||||
{
|
||||
content.Instance.RemoveXmlFilePersistenceQueue();
|
||||
content.Instance.PersistXmlToFile();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
/// <summary>
|
||||
/// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request
|
||||
/// </summary>
|
||||
/// <param name="http"></param>
|
||||
@@ -613,13 +598,6 @@ namespace Umbraco.Web
|
||||
ProcessRequest(new HttpContextWrapper(httpContext));
|
||||
};
|
||||
|
||||
// used to check if the xml cache file needs to be updated/persisted
|
||||
app.PostRequestHandlerExecute += (sender, e) =>
|
||||
{
|
||||
var httpContext = ((HttpApplication)sender).Context;
|
||||
PersistXmlCache(new HttpContextWrapper(httpContext));
|
||||
};
|
||||
|
||||
app.EndRequest += (sender, args) =>
|
||||
{
|
||||
var httpContext = ((HttpApplication)sender).Context;
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Xml;
|
||||
using System.Xml.XPath;
|
||||
using umbraco.cms.presentation;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using umbraco.BusinessLogic;
|
||||
using umbraco.BusinessLogic.Actions;
|
||||
using umbraco.BusinessLogic.Utils;
|
||||
using umbraco.cms.businesslogic;
|
||||
using umbraco.cms.businesslogic.cache;
|
||||
using umbraco.cms.businesslogic.web;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Profiling;
|
||||
using umbraco.DataLayer;
|
||||
using umbraco.presentation.nodeFactory;
|
||||
using Umbraco.Web;
|
||||
using Action = umbraco.BusinessLogic.Actions.Action;
|
||||
using Umbraco.Web.PublishedCache.XmlPublishedCache;
|
||||
using Umbraco.Web.Scheduling;
|
||||
using Node = umbraco.NodeFactory.Node;
|
||||
using Umbraco.Core;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace umbraco
|
||||
@@ -37,6 +32,18 @@ namespace umbraco
|
||||
/// </summary>
|
||||
public class content
|
||||
{
|
||||
private static readonly BackgroundTaskRunner<XmlCacheFilePersister> FilePersister
|
||||
= new BackgroundTaskRunner<XmlCacheFilePersister>(new BackgroundTaskRunnerOptions { LongRunning = true });
|
||||
|
||||
private XmlCacheFilePersister _persisterTask;
|
||||
|
||||
private content()
|
||||
{
|
||||
_persisterTask = new XmlCacheFilePersister(FilePersister, this, UmbracoXmlDiskCacheFileName,
|
||||
new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler));
|
||||
FilePersister.Add(_persisterTask);
|
||||
}
|
||||
|
||||
#region Declarations
|
||||
|
||||
// Sync access to disk file
|
||||
@@ -75,7 +82,6 @@ namespace umbraco
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Singleton
|
||||
|
||||
private static readonly Lazy<content> LazyInstance = new Lazy<content>(() => new content());
|
||||
@@ -134,7 +140,7 @@ namespace umbraco
|
||||
/// <remarks>
|
||||
/// Before returning we always check to ensure that the xml is loaded
|
||||
/// </remarks>
|
||||
protected virtual XmlDocument XmlContentInternal
|
||||
protected internal virtual XmlDocument XmlContentInternal
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -320,8 +326,7 @@ namespace umbraco
|
||||
// and clear the queue in case is this a web request, we don't want it reprocessing.
|
||||
if (UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache)
|
||||
{
|
||||
RemoveXmlFilePersistenceQueue();
|
||||
PersistXmlToFile(xmlDoc);
|
||||
QueueXmlForPersistence();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,54 +934,6 @@ namespace umbraco
|
||||
|
||||
#region Protected & Private methods
|
||||
|
||||
internal const string PersistenceFlagContextKey = "vnc38ykjnkjdnk2jt98ygkxjng";
|
||||
|
||||
/// <summary>
|
||||
/// Removes the flag that queues the file for persistence
|
||||
/// </summary>
|
||||
internal void RemoveXmlFilePersistenceQueue()
|
||||
{
|
||||
if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null)
|
||||
{
|
||||
UmbracoContext.Current.HttpContext.Application.Lock();
|
||||
UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] = null;
|
||||
UmbracoContext.Current.HttpContext.Application.UnLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsXmlQueuedForPersistenceToFile
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null)
|
||||
{
|
||||
bool val = UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] != null;
|
||||
if (val)
|
||||
{
|
||||
DateTime persistenceTime = DateTime.MinValue;
|
||||
try
|
||||
{
|
||||
persistenceTime = (DateTime)UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey];
|
||||
if (persistenceTime > GetCacheFileUpdateTime())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveXmlFilePersistenceQueue();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Nothing to catch here - we'll just persist
|
||||
LogHelper.Error<content>("An error occurred checking if xml file is queued for persistence", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the disk content cache file. Effectively just deletes it.
|
||||
/// </summary>
|
||||
@@ -1248,51 +1205,26 @@ order by umbracoNode.level, umbracoNode.sortOrder";
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("This method should not be used, xml file persistence is done in a queue using a BackgroundTaskRunner")]
|
||||
public void PersistXmlToFile()
|
||||
{
|
||||
PersistXmlToFile(_xmlContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist a XmlDocument to the Disk Cache
|
||||
/// </summary>
|
||||
/// <param name="xmlDoc"></param>
|
||||
internal void PersistXmlToFile(XmlDocument xmlDoc)
|
||||
{
|
||||
lock (ReaderWriterSyncLock)
|
||||
{
|
||||
if (xmlDoc != null)
|
||||
{
|
||||
LogHelper.Debug<content>("Saving content to disk on thread '{0}' (Threadpool? {1})",
|
||||
() => Thread.CurrentThread.Name,
|
||||
() => Thread.CurrentThread.IsThreadPoolThread);
|
||||
|
||||
if (_xmlContent != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Stopwatch stopWatch = Stopwatch.StartNew();
|
||||
// create directory for cache path if it doesn't yet exist
|
||||
var directoryName = Path.GetDirectoryName(UmbracoXmlDiskCacheFileName);
|
||||
Directory.CreateDirectory(directoryName);
|
||||
|
||||
DeleteXmlCache();
|
||||
|
||||
// Try to create directory for cache path if it doesn't yet exist
|
||||
string directoryName = Path.GetDirectoryName(UmbracoXmlDiskCacheFileName);
|
||||
if (!File.Exists(UmbracoXmlDiskCacheFileName) && !Directory.Exists(directoryName))
|
||||
{
|
||||
// We're already in a try-catch and saving will fail if this does, so don't need another
|
||||
Directory.CreateDirectory(directoryName);
|
||||
}
|
||||
|
||||
xmlDoc.Save(UmbracoXmlDiskCacheFileName);
|
||||
|
||||
LogHelper.Debug<content>("Saved content on thread '{0}' in {1} (Threadpool? {2})",
|
||||
() => Thread.CurrentThread.Name,
|
||||
() => stopWatch.Elapsed,
|
||||
() => Thread.CurrentThread.IsThreadPoolThread);
|
||||
_xmlContent.Save(UmbracoXmlDiskCacheFileName);
|
||||
}
|
||||
catch (Exception ee)
|
||||
{
|
||||
// If for whatever reason something goes wrong here, invalidate disk cache
|
||||
DeleteXmlCache();
|
||||
|
||||
|
||||
LogHelper.Error<content>(string.Format(
|
||||
"Error saving content on thread '{0}' due to '{1}' (Threadpool? {2})",
|
||||
Thread.CurrentThread.Name, ee.Message, Thread.CurrentThread.IsThreadPoolThread), ee);
|
||||
@@ -1302,48 +1234,17 @@ order by umbracoNode.level, umbracoNode.sortOrder";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a flag in the HttpContext so that, upon page execution completion, the Xml cache will
|
||||
/// get persisted to disk. Ensure this method is only called from a thread executing a page request
|
||||
/// since UmbracoModule is the only monitor of this flag and is responsible
|
||||
/// for enacting the persistence at the PostRequestHandlerExecute stage of the page lifecycle.
|
||||
/// Adds a task to the xml cache file persister
|
||||
/// </summary>
|
||||
private void QueueXmlForPersistence()
|
||||
{
|
||||
//if this is called outside a web request we cannot queue it it will run in the current thread.
|
||||
|
||||
if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null)
|
||||
{
|
||||
UmbracoContext.Current.HttpContext.Application.Lock();
|
||||
try
|
||||
{
|
||||
if (UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] != null)
|
||||
{
|
||||
UmbracoContext.Current.HttpContext.Application.Add(PersistenceFlagContextKey, null);
|
||||
}
|
||||
UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] = DateTime.UtcNow;
|
||||
}
|
||||
finally
|
||||
{
|
||||
UmbracoContext.Current.HttpContext.Application.UnLock();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Save copy of content
|
||||
if (UmbracoSettings.CloneXmlCacheOnPublish)
|
||||
{
|
||||
XmlDocument xmlContentCopy = CloneXmlDoc(_xmlContent);
|
||||
PersistXmlToFile(xmlContentCopy);
|
||||
}
|
||||
else
|
||||
{
|
||||
PersistXmlToFile();
|
||||
}
|
||||
}
|
||||
_persisterTask = _persisterTask.Touch();
|
||||
}
|
||||
|
||||
internal DateTime GetCacheFileUpdateTime()
|
||||
{
|
||||
//TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed?
|
||||
|
||||
if (File.Exists(UmbracoXmlDiskCacheFileName))
|
||||
{
|
||||
return new FileInfo(UmbracoXmlDiskCacheFileName).LastWriteTimeUtc;
|
||||
|
||||
@@ -317,10 +317,10 @@ namespace umbraco.presentation
|
||||
|
||||
void context_PostRequestHandlerExecute(object sender, EventArgs e)
|
||||
{
|
||||
if (content.Instance.IsXmlQueuedForPersistenceToFile)
|
||||
{
|
||||
content.Instance.PersistXmlToFile();
|
||||
}
|
||||
//if (content.Instance.IsXmlQueuedForPersistenceToFile)
|
||||
//{
|
||||
// content.Instance.PersistXmlToFile();
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user