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 @@ + +