From 4eb9a54fa5d4e0330b6adc178191221d41fde165 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 7 Dec 2014 16:16:29 +0100 Subject: [PATCH] Ports v7 changes for U4-5728 - OriginalRequestUrl not initialized, KeepAlive or ScheduledPublishing run too soon #U4-5728 --- .../Sync/ServerEnvironmentHelper.cs | 36 +- .../Scheduling/BackgroundTaskRunner.cs | 363 ++++++++++++++++++ src/Umbraco.Web/Scheduling/IBackgroundTask.cs | 9 + src/Umbraco.Web/Scheduling/KeepAlive.cs | 33 +- src/Umbraco.Web/Scheduling/LogScrubber.cs | 49 +-- .../Scheduling/ScheduledPublishing.cs | 50 ++- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 64 +-- src/Umbraco.Web/Scheduling/Scheduler.cs | 108 ++++-- src/Umbraco.Web/Scheduling/TaskEventArgs.cs | 22 ++ src/Umbraco.Web/Umbraco.Web.csproj | 7 +- 10 files changed, 596 insertions(+), 145 deletions(-) create mode 100644 src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs create mode 100644 src/Umbraco.Web/Scheduling/IBackgroundTask.cs create mode 100644 src/Umbraco.Web/Scheduling/TaskEventArgs.cs diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs index 73dca4cc98..c39b2805fc 100644 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -17,15 +17,19 @@ 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) { var status = GetStatus(); 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 = UmbracoSettings.DistributionServers; @@ -33,8 +37,9 @@ namespace Umbraco.Core.Sync var nodes = servers.SelectNodes("./server"); if (nodes == null) { - //cannot be determined, then the base url has to be the first url registered - return string.Format("http://{0}", ApplicationContext.Current.OriginalRequestUrl); + return string.IsNullOrWhiteSpace(appContext.OriginalRequestUrl) + ? null // not initialized yet + : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl); } var xmlNodes = nodes.Cast().ToList(); @@ -58,11 +63,14 @@ namespace Umbraco.Core.Sync xmlNode.InnerText, xmlNode.AttributeValue("forcePortnumber").IsNullOrWhiteSpace() ? "80" : xmlNode.AttributeValue("forcePortnumber"), IOHelper.ResolveUrl(SystemDirectories.Umbraco).TrimStart('/')); - } + } } - - //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); } /// @@ -85,7 +93,7 @@ namespace Umbraco.Core.Sync } var master = nodes.Cast().FirstOrDefault(); - + if (master == null) { return CurrentServerEnvironmentStatus.Unknown; @@ -104,12 +112,12 @@ namespace Umbraco.Core.Sync } if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) - || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) { //match by appdid or server name! - return CurrentServerEnvironmentStatus.Master; + return CurrentServerEnvironmentStatus.Master; } - + return CurrentServerEnvironmentStatus.Slave; } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs new file mode 100644 index 0000000000..d1608aee85 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -0,0 +1,363 @@ +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; } + } + + 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..343f076b2a --- /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..a6f329d13c 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -8,27 +8,32 @@ namespace Umbraco.Web.Scheduling { internal class KeepAlive { - public static void Start(object sender) + public static void Start(ApplicationContext appContext) { using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) - { - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); - - var url = string.Format("{0}/ping.aspx", umbracoBaseUrl); - - try + { + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(appContext); + if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) { - using (var wc = new WebClient()) + LogHelper.Warn("No url for service (yet), skip."); + } + else + { + var url = string.Format("{0}/ping.aspx", umbracoBaseUrl); + + try { - wc.DownloadString(url); + using (var wc = new WebClient()) + { + wc.DownloadString(url); + } + } + catch (Exception ee) + { + LogHelper.Error("Error in ping", ee); } } - catch (Exception ee) - { - LogHelper.Error("Error in ping", ee); - } } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index cea4d1b01c..46fd36d49e 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -2,38 +2,20 @@ using System; using System.Web; using System.Web.Caching; using umbraco.BusinessLogic; +using Umbraco.Core; 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; - CacheItemRemovedCallback _onCacheRemove; - const string LogScrubberTaskName = "ScrubLogs"; - - public void Start() + public LogScrubber(ApplicationContext appContext) { - // log scrubbing - AddTask(LogScrubberTaskName, GetLogScrubbingInterval()); - } - - 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; + _appContext = appContext; } private static int GetLogScrubbingMaximumAge() @@ -52,26 +34,19 @@ namespace Umbraco.Web.Scheduling } - private void AddTask(string name, int seconds) + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() { - _onCacheRemove = new CacheItemRemovedCallback(CacheItemRemoved); - HttpRuntime.Cache.Insert(name, seconds, null, - DateTime.Now.AddSeconds(seconds), System.Web.Caching.Cache.NoSlidingExpiration, - CacheItemPriority.NotRemovable, _onCacheRemove); } - 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()); } - 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..debb8146bc 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -10,30 +10,52 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing + internal class ScheduledPublishing : DisposableObject, IBackgroundTask { + private readonly ApplicationContext _appContext; + private static bool _isPublishingRunning = false; - public void Start(ApplicationContext appContext) + public ScheduledPublishing(ApplicationContext appContext) { - if (appContext == null) return; + _appContext = appContext; + } + + /// + /// 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); - 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) @@ -44,9 +66,7 @@ namespace Umbraco.Web.Scheduling { _isPublishingRunning = false; } - } + } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 6bfb6168c2..3a65e68dc1 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -14,39 +14,19 @@ 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 static readonly Hashtable ScheduledTaskTimes = new Hashtable(); private static bool _isPublishingRunning = false; - - public void Start(ApplicationContext appContext) + + public ScheduledTasks(ApplicationContext appContext) { - 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; } - private static void ProcessTasks() + private void ProcessTasks() { - - var scheduledTasks = UmbracoSettings.ScheduledTasks; if (scheduledTasks != null) { @@ -80,7 +60,7 @@ namespace Umbraco.Web.Scheduling } } - private static bool GetTaskByHttp(string url) + private bool GetTaskByHttp(string url) { var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); @@ -98,5 +78,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..a36c8fde51 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System; +using System.Threading; +using System.Web; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Sync; @@ -9,60 +11,99 @@ 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)); + _pingTimer.Change(60000, 300000); - //log scrubbing - _scrubber = new LogScrubber(); - _scrubber.Start(); + // scheduled publishing/unpublishing + _schedulingTimer = new Timer(state => PerformScheduling(applicationContext)); + _schedulingTimer.Change(60000, 60000); + + //log scrubbing + _logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext)); + _logScrubberTimer.Change(60000, GetLogScrubbingInterval()); + } + } + } + }; + }; } + private 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 void PerformLogScrub(ApplicationContext appContext) + { + _scrubberRunner.Add(new LogScrubber(appContext)); + } /// /// 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) { using (DisposableTimer.DebugDuration(() => "Scheduling interval executing", () => "Scheduling interval complete")) { @@ -78,12 +119,10 @@ 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)); //do the scheduled tasks - var scheduledTasks = new ScheduledTasks(); - scheduledTasks.Start(ApplicationContext.Current); + _tasksRunner.Add(new ScheduledTasks(appContext)); break; case CurrentServerEnvironmentStatus.Slave: @@ -96,6 +135,5 @@ 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..27e5174616 --- /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 b0e93f5afa..2403084c96 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -321,10 +321,13 @@ + + + @@ -750,9 +753,7 @@ - - ASPXCodeBehind - + ASPXCodeBehind