diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs
index aadad2abe0..48f3cfecd6 100644
--- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs
+++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Web;
using System.Xml;
using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
namespace Umbraco.Core.Sync
@@ -17,23 +18,30 @@ namespace Umbraco.Core.Sync
/// status. This will attempt to determine the internal umbraco base url that can be used by the current
/// server to send a request to itself if it is in a load balanced environment.
///
- /// The full base url including schema (i.e. http://myserver:80/umbraco )
- public static string GetCurrentServerUmbracoBaseUrl()
+ /// The full base url including schema (i.e. http://myserver:80/umbraco ) - or null if the url
+ /// cannot be determined at the moment (usually because the first request has not properly completed yet).
+ public static string GetCurrentServerUmbracoBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
- var status = GetStatus();
+ var status = GetStatus(settings);
if (status == CurrentServerEnvironmentStatus.Single)
{
- //if it's a single install, then the base url has to be the first url registered
- return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl);
+ // single install, return null if no original url, else use original url as base
+ // use http or https as appropriate
+ return string.IsNullOrWhiteSpace(appContext.OriginalRequestUrl)
+ ? null // not initialized yet
+ : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl);
}
- var servers = UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers.ToArray();
+ var servers = settings.DistributedCall.Servers.ToArray();
if (servers.Any() == false)
{
- //cannot be determined, then the base url has to be the first url registered
- return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl);
+ // cannot be determined, return null if no original url, else use original url as base
+ // use http or https as appropriate
+ return string.IsNullOrWhiteSpace(appContext.OriginalRequestUrl)
+ ? null // not initialized yet
+ : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl);
}
foreach (var server in servers)
@@ -58,22 +66,25 @@ namespace Umbraco.Core.Sync
}
}
- //cannot be determined, then the base url has to be the first url registered
- return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl);
+ // cannot be determined, return null if no original url, else use original url as base
+ // use http or https as appropriate
+ return string.IsNullOrWhiteSpace(appContext.OriginalRequestUrl)
+ ? null // not initialized yet
+ : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl);
}
///
/// Returns the current environment status for the current server
///
///
- public static CurrentServerEnvironmentStatus GetStatus()
+ public static CurrentServerEnvironmentStatus GetStatus(IUmbracoSettingsSection settings)
{
- if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false)
+ if (settings.DistributedCall.Enabled == false)
{
return CurrentServerEnvironmentStatus.Single;
}
- var servers = UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers.ToArray();
+ var servers = settings.DistributedCall.Servers.ToArray();
if (servers.Any() == false)
{
diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs
new file mode 100644
index 0000000000..8162b27c62
--- /dev/null
+++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Umbraco.Core;
+using Umbraco.Web.Scheduling;
+
+namespace Umbraco.Tests.Scheduling
+{
+ [TestFixture]
+ public class BackgroundTaskRunnerTests
+ {
+
+
+ [Test]
+ public void Startup_And_Shutdown()
+ {
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.StartUp();
+ }
+
+ NUnit.Framework.Assert.IsFalse(tManager.IsRunning);
+ }
+
+ [Test]
+ public void Startup_Starts_Automatically()
+ {
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.Add(new MyTask());
+ NUnit.Framework.Assert.IsTrue(tManager.IsRunning);
+ }
+ }
+
+ [Test]
+ public void Task_Runs()
+ {
+ var myTask = new MyTask();
+ var waitHandle = new ManualResetEvent(false);
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.TaskCompleted += (sender, task) => waitHandle.Set();
+
+ tManager.Add(myTask);
+
+ //wait for ITasks to complete
+ waitHandle.WaitOne();
+
+ NUnit.Framework.Assert.IsTrue(myTask.Ended != default(DateTime));
+ }
+ }
+
+ [Test]
+ public void Many_Tasks_Run()
+ {
+ var tasks = new Dictionary();
+ for (var i = 0; i < 10; i++)
+ {
+ tasks.Add(new MyTask(), new ManualResetEvent(false));
+ }
+
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set();
+
+ tasks.ForEach(t => tManager.Add(t.Key));
+
+ //wait for all ITasks to complete
+ WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray());
+
+ foreach (var task in tasks)
+ {
+ NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime));
+ }
+ }
+ }
+
+ [Test]
+ public void Tasks_Can_Keep_Being_Added_And_Will_Execute()
+ {
+ Func> getTasks = () =>
+ {
+ var newTasks = new Dictionary();
+ for (var i = 0; i < 10; i++)
+ {
+ newTasks.Add(new MyTask(), new ManualResetEvent(false));
+ }
+ return newTasks;
+ };
+
+ IDictionary tasks = getTasks();
+
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set();
+
+ //execute first batch
+ tasks.ForEach(t => tManager.Add(t.Key));
+
+ //wait for all ITasks to complete
+ WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray());
+
+ foreach (var task in tasks)
+ {
+ NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime));
+ }
+
+ //execute another batch after a bit
+ Thread.Sleep(2000);
+
+ tasks = getTasks();
+ tasks.ForEach(t => tManager.Add(t.Key));
+
+ //wait for all ITasks to complete
+ WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray());
+
+ foreach (var task in tasks)
+ {
+ NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime));
+ }
+ }
+ }
+
+ [Test]
+ public void Task_Queue_Will_Be_Completed_Before_Shutdown()
+ {
+ var tasks = new Dictionary();
+ for (var i = 0; i < 10; i++)
+ {
+ tasks.Add(new MyTask(), new ManualResetEvent(false));
+ }
+
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(true, true))
+ {
+ tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set();
+
+ tasks.ForEach(t => tManager.Add(t.Key));
+
+ ////wait for all ITasks to complete
+ //WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray());
+
+ tManager.Stop(false);
+ //immediate stop will block until complete - but since we are running on
+ // a single thread this doesn't really matter as the above will just process
+ // until complete.
+ tManager.Stop(true);
+
+ NUnit.Framework.Assert.AreEqual(0, tManager.TaskCount);
+ }
+ }
+
+ //NOTE: These tests work in .Net 4.5 but in this current version we don't have the correct
+ // async/await signatures with GetAwaiter, so am just commenting these out in this version
+
+ [Test]
+ public async void Non_Persistent_Runner_Will_End_After_Queue_Empty()
+ {
+ var tasks = new List();
+ for (var i = 0; i < 10; i++)
+ {
+ tasks.Add(new MyTask());
+ }
+
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread:true))
+ {
+ tasks.ForEach(t => tManager.Add(t));
+
+ //wait till the thread is done
+ await tManager;
+
+ foreach (var task in tasks)
+ {
+ Assert.IsTrue(task.Ended != default(DateTime));
+ }
+
+ Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus);
+ Assert.IsFalse(tManager.IsRunning);
+ Assert.IsFalse(tManager.IsDisposed);
+ }
+ }
+
+ [Test]
+ public async void Non_Persistent_Runner_Will_Start_New_Threads_When_Required()
+ {
+ Func> getTasks = () =>
+ {
+ var newTasks = new List();
+ for (var i = 0; i < 10; i++)
+ {
+ newTasks.Add(new MyTask());
+ }
+ return newTasks;
+ };
+
+ List tasks = getTasks();
+
+ BackgroundTaskRunner tManager;
+ using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread: true))
+ {
+ tasks.ForEach(t => tManager.Add(t));
+
+ //wait till the thread is done
+ await tManager;
+
+ Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus);
+ Assert.IsFalse(tManager.IsRunning);
+ Assert.IsFalse(tManager.IsDisposed);
+
+ foreach (var task in tasks)
+ {
+ Assert.IsTrue(task.Ended != default(DateTime));
+ }
+
+ //create more tasks
+ tasks = getTasks();
+
+ //add more tasks
+ 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);
+ }
+ }
+
+ private class MyTask : BaseTask
+ {
+ public MyTask()
+ {
+ }
+
+ public override void Run()
+ {
+ Thread.Sleep(500);
+ }
+
+ public override void Cancel()
+ {
+
+ }
+ }
+
+ public abstract class BaseTask : IBackgroundTask
+ {
+ public Guid UniqueId { get; protected set; }
+
+ public abstract void Run();
+ public abstract void Cancel();
+
+ public DateTime Queued { get; set; }
+ public DateTime Started { get; set; }
+ public DateTime Ended { get; set; }
+
+ public virtual void Dispose()
+ {
+ Ended = DateTime.Now;
+ }
+ }
+
+ }
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 4caf247a2e..92b02b1b3e 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -311,6 +311,7 @@
+
diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs
new file mode 100644
index 0000000000..a0c6720a88
--- /dev/null
+++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs
@@ -0,0 +1,381 @@
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Hosting;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Web.Scheduling
+{
+ ///
+ /// This is used to create a background task runner which will stay alive in the background of and complete
+ /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain
+ /// is shutdown.
+ ///
+ ///
+ internal class BackgroundTaskRunner : IDisposable, IRegisteredObject
+ where T : IBackgroundTask
+ {
+ private readonly bool _dedicatedThread;
+ private readonly bool _persistentThread;
+ private readonly BlockingCollection _tasks = new BlockingCollection();
+ private Task _consumer;
+
+ private volatile bool _isRunning = false;
+ private static readonly object Locker = new object();
+ private CancellationTokenSource _tokenSource;
+ internal event EventHandler> TaskError;
+ internal event EventHandler> TaskStarting;
+ internal event EventHandler> TaskCompleted;
+ internal event EventHandler> TaskCancelled;
+
+ public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false)
+ {
+ _dedicatedThread = dedicatedThread;
+ _persistentThread = persistentThread;
+ HostingEnvironment.RegisterObject(this);
+ }
+
+ public int TaskCount
+ {
+ get { return _tasks.Count; }
+ }
+
+ public bool IsRunning
+ {
+ get { return _isRunning; }
+ }
+
+ public TaskStatus TaskStatus
+ {
+ get { return _consumer.Status; }
+ }
+
+
+ ///
+ /// Returns the task awaiter so that consumers of the BackgroundTaskManager can await
+ /// the threading operation.
+ ///
+ ///
+ ///
+ /// This is just the coolest thing ever, check this article out:
+ /// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx
+ ///
+ /// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion
+ /// we can await anything! :)
+ ///
+ public TaskAwaiter GetAwaiter()
+ {
+ return _consumer.GetAwaiter();
+ }
+
+ public void Add(T task)
+ {
+ //add any tasks first
+ LogHelper.Debug>(" Task added {0}", () => task.GetType());
+ _tasks.Add(task);
+
+ //ensure's everything is started
+ StartUp();
+ }
+
+ public void StartUp()
+ {
+ if (!_isRunning)
+ {
+ lock (Locker)
+ {
+ //double check
+ if (!_isRunning)
+ {
+ _isRunning = true;
+ //Create a new token source since this is a new proces
+ _tokenSource = new CancellationTokenSource();
+ StartConsumer();
+ LogHelper.Debug>("Starting");
+ }
+ }
+ }
+ }
+
+ public void ShutDown()
+ {
+ lock (Locker)
+ {
+ _isRunning = false;
+
+ try
+ {
+ if (_consumer != null)
+ {
+ //cancel all operations
+ _tokenSource.Cancel();
+
+ try
+ {
+ _consumer.Wait();
+ }
+ catch (AggregateException e)
+ {
+ //NOTE: We are logging Debug because we are expecting these errors
+
+ LogHelper.Debug>("AggregateException thrown with the following inner exceptions:");
+ // Display information about each exception.
+ foreach (var v in e.InnerExceptions)
+ {
+ var exception = v as TaskCanceledException;
+ if (exception != null)
+ {
+ LogHelper.Debug>(" .Net TaskCanceledException: .Net Task ID {0}", () => exception.Task.Id);
+ }
+ else
+ {
+ LogHelper.Debug>(" Exception: {0}", () => v.GetType().Name);
+ }
+ }
+ }
+ }
+
+ if (_tasks.Count > 0)
+ {
+ LogHelper.Debug>("Processing remaining tasks before shutdown: {0}", () => _tasks.Count);
+
+ //now we need to ensure the remaining queue is processed if there's any remaining,
+ // this will all be processed on the current/main thread.
+ T remainingTask;
+ while (_tasks.TryTake(out remainingTask))
+ {
+ ConsumeTaskInternal(remainingTask);
+ }
+ }
+
+ LogHelper.Debug>("Shutdown");
+
+ //disposing these is really optional since they'll be disposed immediately since they are no longer running
+ //but we'll put this here anyways.
+ if (_consumer != null && (_consumer.IsCompleted || _consumer.IsCanceled))
+ {
+ _consumer.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Error>("Error occurred shutting down task runner", ex);
+ }
+ finally
+ {
+ HostingEnvironment.UnregisterObject(this);
+ }
+ }
+ }
+
+ ///
+ /// Starts the consumer task
+ ///
+ private void StartConsumer()
+ {
+ var token = _tokenSource.Token;
+
+ _consumer = Task.Factory.StartNew(() =>
+ StartThread(token),
+ token,
+ _dedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None,
+ TaskScheduler.Default);
+
+ //if this is not a persistent thread, wait till it's done and shut ourselves down
+ // thus ending the thread or giving back to the thread pool. If another task is added
+ // another thread will spawn or be taken from the pool to process.
+ if (!_persistentThread)
+ {
+ _consumer.ContinueWith(task => ShutDown());
+ }
+
+ }
+
+ ///
+ /// Invokes a new worker thread to consume tasks
+ ///
+ ///
+ private void StartThread(CancellationToken token)
+ {
+ // Was cancellation already requested?
+ if (token.IsCancellationRequested)
+ {
+ LogHelper.Info>("Thread {0} was cancelled before it got started.", () => Thread.CurrentThread.ManagedThreadId);
+ token.ThrowIfCancellationRequested();
+ }
+
+ TakeAndConsumeTask(token);
+ }
+
+ ///
+ /// Trys to get a task from the queue, if there isn't one it will wait a second and try again
+ ///
+ ///
+ private void TakeAndConsumeTask(CancellationToken token)
+ {
+ if (token.IsCancellationRequested)
+ {
+ LogHelper.Info>("Thread {0} was cancelled.", () => Thread.CurrentThread.ManagedThreadId);
+ token.ThrowIfCancellationRequested();
+ }
+
+ //If this is true, the thread will stay alive and just wait until there is anything in the queue
+ // and process it. When there is nothing in the queue, the thread will just block until there is
+ // something to process.
+ //When this is false, the thread will process what is currently in the queue and once that is
+ // done, the thread will end and we will shutdown the process
+
+ if (_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);
+ }
+ else
+ {
+ T repositoryTask;
+ while (_tasks.TryTake(out repositoryTask))
+ {
+ ConsumeTaskCancellable(repositoryTask, token);
+ }
+
+ //the task will end here
+ }
+ }
+
+ internal void ConsumeTaskCancellable(T task, CancellationToken token)
+ {
+ if (token.IsCancellationRequested)
+ {
+ OnTaskCancelled(new TaskEventArgs(task));
+
+ //NOTE: Since the task hasn't started this is pretty pointless so leaving it out.
+ LogHelper.Info>("Task {0}) was cancelled.",
+ () => task.GetType());
+
+ token.ThrowIfCancellationRequested();
+ }
+
+ ConsumeTaskInternal(task);
+ }
+
+ private void ConsumeTaskInternal(T task)
+ {
+ try
+ {
+ OnTaskStarting(new TaskEventArgs(task));
+
+ try
+ {
+ using (task)
+ {
+ task.Run();
+ }
+ }
+ catch (Exception e)
+ {
+ OnTaskError(new TaskEventArgs(task, e));
+ throw;
+ }
+
+ OnTaskCompleted(new TaskEventArgs(task));
+ }
+ catch (Exception ex)
+ {
+ LogHelper.Error>("An error occurred consuming task", ex);
+ }
+ }
+
+ protected virtual void OnTaskError(TaskEventArgs e)
+ {
+ var handler = TaskError;
+ if (handler != null) handler(this, e);
+ }
+
+ protected virtual void OnTaskStarting(TaskEventArgs e)
+ {
+ var handler = TaskStarting;
+ if (handler != null) handler(this, e);
+ }
+
+ protected virtual void OnTaskCompleted(TaskEventArgs e)
+ {
+ var handler = TaskCompleted;
+ if (handler != null) handler(this, e);
+ }
+
+ protected virtual void OnTaskCancelled(TaskEventArgs e)
+ {
+ var handler = TaskCancelled;
+ if (handler != null) handler(this, e);
+ }
+
+
+ #region Disposal
+ private readonly object _disposalLocker = new object();
+ public bool IsDisposed { get; private set; }
+
+ ~BackgroundTaskRunner()
+ {
+ this.Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (this.IsDisposed || !disposing)
+ return;
+ lock (_disposalLocker)
+ {
+ if (IsDisposed)
+ return;
+ DisposeResources();
+ IsDisposed = true;
+ }
+ }
+
+ protected virtual void DisposeResources()
+ {
+ ShutDown();
+ }
+ #endregion
+
+ public void Stop(bool immediate)
+ {
+ if (immediate == false)
+ {
+ LogHelper.Debug>("Application is shutting down, waiting for tasks to complete");
+ Dispose();
+ }
+ else
+ {
+ //NOTE: this will thread block the current operation if the manager
+ // is still shutting down because the Shutdown operation is also locked
+ // by this same lock instance. This would only matter if Stop is called by ASP.Net
+ // on two different threads though, otherwise the current thread will just block normally
+ // until the app is shutdown
+ lock (Locker)
+ {
+ LogHelper.Info>("Application is shutting down immediately");
+ }
+ }
+
+ }
+
+ }
+}
diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs
new file mode 100644
index 0000000000..3e0bddfddb
--- /dev/null
+++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Umbraco.Web.Scheduling
+{
+ internal interface IBackgroundTask : IDisposable
+ {
+ void Run();
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs
index 0ebce6878b..40e6ae0ecb 100644
--- a/src/Umbraco.Web/Scheduling/KeepAlive.cs
+++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs
@@ -1,6 +1,8 @@
using System;
using System.Net;
using Umbraco.Core;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Sync;
@@ -8,24 +10,33 @@ namespace Umbraco.Web.Scheduling
{
internal class KeepAlive
{
- public static void Start(object sender)
+ public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete"))
{
- var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl();
+ var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(
+ appContext,
+ settings);
- var url = string.Format("{0}/ping.aspx", umbracoBaseUrl);
-
- try
+ if (string.IsNullOrWhiteSpace(umbracoBaseUrl))
{
- using (var wc = new WebClient())
- {
- wc.DownloadString(url);
- }
+ LogHelper.Warn("No url for service (yet), skip.");
}
- catch (Exception ee)
+ else
{
- LogHelper.Error("Error in ping", ee);
+ var url = string.Format("{0}/ping.aspx", umbracoBaseUrl);
+
+ try
+ {
+ using (var wc = new WebClient())
+ {
+ wc.DownloadString(url);
+ }
+ }
+ catch (Exception ee)
+ {
+ LogHelper.Error("Error in ping", ee);
+ }
}
}
diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs
index cea4d1b01c..933e6d7c6b 100644
--- a/src/Umbraco.Web/Scheduling/LogScrubber.cs
+++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs
@@ -2,47 +2,31 @@ using System;
using System.Web;
using System.Web.Caching;
using umbraco.BusinessLogic;
+using Umbraco.Core;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Scheduling
{
- //TODO: Refactor this to use a normal scheduling processor!
- internal class LogScrubber
+ internal class LogScrubber : DisposableObject, IBackgroundTask
{
- // this is a raw copy of the legacy code in all its uglyness
+ private readonly ApplicationContext _appContext;
+ private readonly IUmbracoSettingsSection _settings;
- CacheItemRemovedCallback _onCacheRemove;
- const string LogScrubberTaskName = "ScrubLogs";
-
- public void Start()
+ public LogScrubber(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
- // log scrubbing
- AddTask(LogScrubberTaskName, GetLogScrubbingInterval());
+ _appContext = appContext;
+ _settings = settings;
}
- private static int GetLogScrubbingInterval()
- {
- int interval = 24 * 60 * 60; //24 hours
- try
- {
- if (global::umbraco.UmbracoSettings.CleaningMiliseconds > -1)
- interval = global::umbraco.UmbracoSettings.CleaningMiliseconds;
- }
- catch (Exception e)
- {
- LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e);
- }
- return interval;
- }
-
- private static int GetLogScrubbingMaximumAge()
+ private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings)
{
int maximumAge = 24 * 60 * 60;
try
{
- if (global::umbraco.UmbracoSettings.MaxLogAge > -1)
- maximumAge = global::umbraco.UmbracoSettings.MaxLogAge;
+ if (settings.Logging.MaxLogAge > -1)
+ maximumAge = settings.Logging.MaxLogAge;
}
catch (Exception e)
{
@@ -52,26 +36,19 @@ namespace Umbraco.Web.Scheduling
}
- private void AddTask(string name, int seconds)
- {
- _onCacheRemove = new CacheItemRemovedCallback(CacheItemRemoved);
- HttpRuntime.Cache.Insert(name, seconds, null,
- DateTime.Now.AddSeconds(seconds), System.Web.Caching.Cache.NoSlidingExpiration,
- CacheItemPriority.NotRemovable, _onCacheRemove);
+ ///
+ /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
+ ///
+ protected override void DisposeResources()
+ {
}
- public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r)
+ public void Run()
{
- if (k.Equals(LogScrubberTaskName))
+ using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete"))
{
- ScrubLogs();
+ Log.CleanLogs(GetLogScrubbingMaximumAge(_settings));
}
- AddTask(k, Convert.ToInt32(v));
- }
-
- private static void ScrubLogs()
- {
- Log.CleanLogs(GetLogScrubbingMaximumAge());
}
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs
index 9d7fde0789..bc303851f7 100644
--- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs
+++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Net;
using System.Text;
using Umbraco.Core;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Publishing;
using Umbraco.Core.Sync;
@@ -10,30 +11,55 @@ using Umbraco.Web.Mvc;
namespace Umbraco.Web.Scheduling
{
- internal class ScheduledPublishing
+ internal class ScheduledPublishing : DisposableObject, IBackgroundTask
{
+ private readonly ApplicationContext _appContext;
+ private readonly IUmbracoSettingsSection _settings;
+
private static bool _isPublishingRunning = false;
- public void Start(ApplicationContext appContext)
+ public ScheduledPublishing(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
- if (appContext == null) return;
+ _appContext = appContext;
+ _settings = settings;
+ }
+
+
+ ///
+ /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
+ ///
+ protected override void DisposeResources()
+ {
+ }
+
+ public void Run()
+ {
+ if (_appContext == null) return;
using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete"))
- {
+ {
if (_isPublishingRunning) return;
_isPublishingRunning = true;
-
+
try
{
- var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl();
- var url = string.Format("{0}/RestServices/ScheduledPublish/Index", umbracoBaseUrl);
- using (var wc = new WebClient())
- {
- //pass custom the authorization header
- wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(appContext));
+ var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(_appContext, _settings);
- var result = wc.UploadString(url, "");
+ if (string.IsNullOrWhiteSpace(umbracoBaseUrl))
+ {
+ LogHelper.Warn("No url for service (yet), skip.");
+ }
+ else
+ {
+ var url = string.Format("{0}/RestServices/ScheduledPublish/Index", umbracoBaseUrl);
+ using (var wc = new WebClient())
+ {
+ //pass custom the authorization header
+ wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(_appContext));
+
+ var result = wc.UploadString(url, "");
+ }
}
}
catch (Exception ee)
@@ -46,7 +72,5 @@ namespace Umbraco.Web.Scheduling
}
}
}
-
-
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs
index e4ce265444..cfebfbb246 100644
--- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs
+++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Net;
using System.Xml;
using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Publishing;
using Umbraco.Core.Sync;
@@ -15,40 +16,25 @@ 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
+ internal class ScheduledTasks : DisposableObject, IBackgroundTask
{
+ private readonly ApplicationContext _appContext;
+ private readonly IUmbracoSettingsSection _settings;
private static readonly Hashtable ScheduledTaskTimes = new Hashtable();
+
private static bool _isPublishingRunning = false;
- public void Start(ApplicationContext appContext)
+ public ScheduledTasks(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
- using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete"))
- {
- if (_isPublishingRunning) return;
-
- _isPublishingRunning = true;
-
- try
- {
- ProcessTasks();
- }
- catch (Exception ee)
- {
- LogHelper.Error("Error executing scheduled task", ee);
- }
- finally
- {
- _isPublishingRunning = false;
- }
- }
-
+ _appContext = appContext;
+ _settings = settings;
}
- private static void ProcessTasks()
+ private void ProcessTasks()
{
- var scheduledTasks = UmbracoConfig.For.UmbracoSettings().ScheduledTasks.Tasks;
+ var scheduledTasks = _settings.ScheduledTasks.Tasks;
foreach (var t in scheduledTasks)
{
var runTask = false;
@@ -75,7 +61,7 @@ namespace Umbraco.Web.Scheduling
}
}
- private static bool GetTaskByHttp(string url)
+ private bool GetTaskByHttp(string url)
{
var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
@@ -93,5 +79,35 @@ namespace Umbraco.Web.Scheduling
return false;
}
+
+ ///
+ /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic.
+ ///
+ protected override void DisposeResources()
+ {
+ }
+
+ public void Run()
+ {
+ using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete"))
+ {
+ if (_isPublishingRunning) return;
+
+ _isPublishingRunning = true;
+
+ try
+ {
+ ProcessTasks();
+ }
+ catch (Exception ee)
+ {
+ LogHelper.Error("Error executing scheduled task", ee);
+ }
+ finally
+ {
+ _isPublishingRunning = false;
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs
index e18e41aeda..5944c301d4 100644
--- a/src/Umbraco.Web/Scheduling/Scheduler.cs
+++ b/src/Umbraco.Web/Scheduling/Scheduler.cs
@@ -1,5 +1,9 @@
-using System.Threading;
+using System;
+using System.Threading;
+using System.Web;
using Umbraco.Core;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Sync;
@@ -9,65 +13,107 @@ namespace Umbraco.Web.Scheduling
/// Used to do the scheduling for tasks, publishing, etc...
///
///
- ///
- /// TODO: Much of this code is legacy and needs to be updated, there are a few new/better ways to do scheduling
- /// in a web project nowadays.
- ///
- /// //TODO: We need a much more robust way of handing scheduled tasks and also need to take into account app shutdowns during
- /// a scheduled tasks operation
- /// http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/
- ///
+ /// All tasks are run in a background task runner which is web aware and will wind down the task correctly instead of killing it completely when
+ /// the app domain shuts down.
///
internal sealed class Scheduler : ApplicationEventHandler
{
private static Timer _pingTimer;
private static Timer _schedulingTimer;
- private static LogScrubber _scrubber;
+ private static BackgroundTaskRunner _publishingRunner;
+ private static BackgroundTaskRunner _tasksRunner;
+ private static BackgroundTaskRunner _scrubberRunner;
+ private static Timer _logScrubberTimer;
+ private static volatile bool _started = false;
+ private static readonly object Locker = new object();
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
if (umbracoApplication.Context == null)
return;
- LogHelper.Debug(() => "Initializing the scheduler");
+ //subscribe to app init so we can subsribe to the application events
+ UmbracoApplicationBase.ApplicationInit += (sender, args) =>
+ {
+ var app = (HttpApplication)sender;
- // time to setup the tasks
+ //subscribe to the end of a successful request (a handler actually executed)
+ app.PostRequestHandlerExecute += (o, eventArgs) =>
+ {
+ if (_started == false)
+ {
+ lock (Locker)
+ {
+ if (_started == false)
+ {
+ _started = true;
+ LogHelper.Debug(() => "Initializing the scheduler");
- // these are the legacy tasks
- // just copied over here for backward compatibility
- // of course we should have a proper scheduler, see #U4-809
+ // time to setup the tasks
- //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.
+ //We have 3 background runners that are web aware, if the app domain dies, these tasks will wind down correctly
+ _publishingRunner = new BackgroundTaskRunner();
+ _tasksRunner = new BackgroundTaskRunner();
+ _scrubberRunner = new BackgroundTaskRunner();
- // ping/keepalive
- _pingTimer = new Timer(KeepAlive.Start);
- _pingTimer.Change(60000, 300000);
+ //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.
- // scheduled publishing/unpublishing
- _schedulingTimer = new Timer(PerformScheduling);
- _schedulingTimer.Change(30000, 60000);
+ // 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
+ _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings()));
+ _pingTimer.Change(60000, 300000);
- //log scrubbing
- _scrubber = new LogScrubber();
- _scrubber.Start();
+ // scheduled publishing/unpublishing
+ _schedulingTimer = new Timer(state => PerformScheduling(applicationContext, UmbracoConfig.For.UmbracoSettings()));
+ _schedulingTimer.Change(60000, 60000);
+
+ //log scrubbing
+ _logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext, UmbracoConfig.For.UmbracoSettings()));
+ _logScrubberTimer.Change(60000, GetLogScrubbingInterval(UmbracoConfig.For.UmbracoSettings()));
+ }
+ }
+ }
+ };
+ };
+ }
+
+
+ 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("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));
}
///
/// This performs all of the scheduling on the one timer
///
- ///
+ ///
+ ///
///
/// No processing will be done if this server is a slave
///
- private static void PerformScheduling(object sender)
+ private static void PerformScheduling(ApplicationContext appContext, IUmbracoSettingsSection settings)
{
using (DisposableTimer.DebugDuration(() => "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();
+ var serverStatus = ServerEnvironmentHelper.GetStatus(settings);
switch (serverStatus)
{
@@ -78,13 +124,11 @@ namespace Umbraco.Web.Scheduling
// then we will process the scheduling
//do the scheduled publishing
- var scheduledPublishing = new ScheduledPublishing();
- scheduledPublishing.Start(ApplicationContext.Current);
-
+ _publishingRunner.Add(new ScheduledPublishing(appContext, settings));
+
//do the scheduled tasks
- var scheduledTasks = new ScheduledTasks();
- scheduledTasks.Start(ApplicationContext.Current);
-
+ _tasksRunner.Add(new ScheduledTasks(appContext, settings));
+
break;
case CurrentServerEnvironmentStatus.Slave:
//do not process
@@ -97,5 +141,6 @@ namespace Umbraco.Web.Scheduling
}
}
+
}
}
diff --git a/src/Umbraco.Web/Scheduling/TaskEventArgs.cs b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs
new file mode 100644
index 0000000000..2ce64a239f
--- /dev/null
+++ b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Umbraco.Web.Scheduling
+{
+ internal class TaskEventArgs : EventArgs
+ where T : IBackgroundTask
+ {
+ public T Task { get; private set; }
+ public Exception Exception { get; private set; }
+
+ public TaskEventArgs(T task)
+ {
+ Task = task;
+ }
+
+ public TaskEventArgs(T task, Exception exception)
+ {
+ Task = task;
+ Exception = exception;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 83f500a112..6be2973fff 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -262,6 +262,7 @@
+
@@ -464,6 +465,8 @@
+
+