diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index a11606937e..d84a5871af 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,24 +1,27 @@ using System; using System.Collections.Concurrent; -using System.Globalization; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Web.Hosting; -using Umbraco.Core.Logging; using Umbraco.Core.Events; +using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { + // exists for logging purposes + internal class BackgroundTaskRunner + { } + /// /// Manages a queue of tasks of type and runs them in the background. /// /// The type of the managed tasks. /// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain /// shuts down (ie is unloaded). - internal class BackgroundTaskRunner : IBackgroundTaskRunner + internal class BackgroundTaskRunner : BackgroundTaskRunner, IBackgroundTaskRunner where T : class, IBackgroundTask { + private readonly string _logPrefix; private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); private readonly object _locker = new object(); @@ -35,13 +38,26 @@ namespace Umbraco.Web.Scheduling internal event TypedEventHandler, TaskEventArgs> TaskStarting; internal event TypedEventHandler, TaskEventArgs> TaskCompleted; internal event TypedEventHandler, TaskEventArgs> TaskCancelled; + + // triggers when the runner stops (but could start again if a task is added to it) + internal event TypedEventHandler, EventArgs> Stopped; + + // triggers when the runner completes (no task can be added to it anymore) internal event TypedEventHandler, EventArgs> Completed; /// /// Initializes a new instance of the class. /// public BackgroundTaskRunner() - : this(new BackgroundTaskRunnerOptions()) + : this(typeof (T).FullName, new BackgroundTaskRunnerOptions()) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the runner. + public BackgroundTaskRunner(string name) + : this(name, new BackgroundTaskRunnerOptions()) { } /// @@ -49,9 +65,19 @@ namespace Umbraco.Web.Scheduling /// /// The set of options. public BackgroundTaskRunner(BackgroundTaskRunnerOptions options) + : this(typeof (T).FullName, options) + { } + + /// + /// Initializes a new instance of the class with a set of options. + /// + /// The name of the runner. + /// The set of options. + public BackgroundTaskRunner(string name, BackgroundTaskRunnerOptions options) { if (options == null) throw new ArgumentNullException("options"); _options = options; + _logPrefix = "[" + name + "] "; HostingEnvironment.RegisterObject(this); @@ -133,7 +159,7 @@ namespace Umbraco.Web.Scheduling throw new InvalidOperationException("The task runner has completed."); // add task - LogHelper.Debug>("Task added {0}", task.GetType); + LogHelper.Debug(_logPrefix + "Task added {0}", task.GetType); _tasks.Add(task); // start @@ -154,7 +180,7 @@ namespace Umbraco.Web.Scheduling if (_isCompleted) return false; // add task - LogHelper.Debug>("Task added {0}", task.GetType); + LogHelper.Debug(_logPrefix + "Task added {0}", task.GetType); _tasks.Add(task); // start @@ -195,7 +221,7 @@ namespace Umbraco.Web.Scheduling // create a new token source since this is a new process _tokenSource = new CancellationTokenSource(); _runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token); - LogHelper.Debug>("Starting"); + LogHelper.Debug(_logPrefix + "Starting"); } /// @@ -236,7 +262,6 @@ namespace Umbraco.Web.Scheduling // tasks in the queue will be executed... if (wait == false) return; _runningTask.Wait(); // wait for whatever is running to end... - } /// @@ -260,23 +285,24 @@ namespace Umbraco.Web.Scheduling { if (token.IsCancellationRequested || _tasks.Count == 0) { - LogHelper.Debug>("_isRunning = false"); + LogHelper.Debug(_logPrefix + "Stopping"); _isRunning = false; // done if (_options.PreserveRunningTask == false) _runningTask = null; - //raise event - OnCompleted(); return; } } + OnStopped(); + // if _runningTask is taskSource.Task then we must keep continuing it, // not starting a new taskSource, else _runningTask would complete and // something may be waiting on it //PumpIBackgroundTasks(factory, token); // restart - // ReSharper disable once MethodSupportsCancellation // always run + // ReSharper disable MethodSupportsCancellation // always run t.ContinueWithTask(_ => PumpIBackgroundTasks(factory, token)); // restart + // ReSharper restore MethodSupportsCancellation }); Action pump = null; @@ -288,7 +314,7 @@ namespace Umbraco.Web.Scheduling if (task != null && task.IsFaulted) { var exception = task.Exception; - LogHelper.Error>("Task runner exception.", exception); + LogHelper.Error(_logPrefix + "Task runner exception.", exception); } // is it ok to run? @@ -298,6 +324,7 @@ namespace Umbraco.Web.Scheduling // the blocking MoveNext will end if token is cancelled or collection is completed T bgTask; var hasBgTask = _options.KeepAlive + // ReSharper disable once PossibleNullReferenceException ? (bgTask = enumerator.MoveNext() ? enumerator.Current : null) != null // blocking : _tasks.TryTake(out bgTask); // non-blocking @@ -343,7 +370,7 @@ namespace Umbraco.Web.Scheduling return taskSourceContinuing; } - private bool TaskSourceCanceled(TaskCompletionSource taskSource, CancellationToken token) + private static bool TaskSourceCanceled(TaskCompletionSource taskSource, CancellationToken token) { if (token.IsCancellationRequested) { @@ -353,7 +380,7 @@ namespace Umbraco.Web.Scheduling return false; } - private void TaskSourceCompleted(TaskCompletionSource taskSource, CancellationToken token) + private static void TaskSourceCompleted(TaskCompletionSource taskSource, CancellationToken token) { if (token.IsCancellationRequested) taskSource.SetCanceled(); @@ -394,83 +421,57 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - LogHelper.Error>("Task has failed.", ex); + LogHelper.Error(_logPrefix + "Task has failed.", ex); } } #region Events + private void OnEvent(TypedEventHandler, TArgs> handler, string name, TArgs e) + { + if (handler == null) return; + + try + { + handler(this, e); + } + catch (Exception ex) + { + LogHelper.Error(_logPrefix + name + " exception occurred", ex); + } + } + protected virtual void OnTaskError(TaskEventArgs e) { - var handler = TaskError; - if (handler != null) handler(this, e); + OnEvent(TaskError, "TaskError", e); } protected virtual void OnTaskStarting(TaskEventArgs e) { - var handler = TaskStarting; - if (handler != null) - { - try - { - handler(this, e); - } - catch (Exception ex) - { - LogHelper.Error>("TaskStarting exception occurred", ex); - } - } + OnEvent(TaskStarting, "TaskStarting", e); } protected virtual void OnTaskCompleted(TaskEventArgs e) { - var handler = TaskCompleted; - if (handler != null) - { - try - { - handler(this, e); - } - catch (Exception ex) - { - LogHelper.Error>("TaskCompleted exception occurred", ex); - } - } + OnEvent(TaskCompleted, "TaskCompleted", e); } protected virtual void OnTaskCancelled(TaskEventArgs e) { - var handler = TaskCancelled; - if (handler != null) - { - try - { - handler(this, e); - } - catch (Exception ex) - { - LogHelper.Error>("TaskCancelled exception occurred", ex); - } - } + OnEvent(TaskCancelled, "TaskCancelled", e); //dispose it e.Task.Dispose(); } + protected virtual void OnStopped() + { + OnEvent(Stopped, "Stopped", EventArgs.Empty); + } + protected virtual void OnCompleted() { - var handler = Completed; - if (handler != null) - { - try - { - handler(this, EventArgs.Empty); - } - catch (Exception ex) - { - LogHelper.Error>("OnCompleted exception occurred", ex); - } - } + OnEvent(Completed, "Completed", EventArgs.Empty); } #endregion @@ -482,7 +483,7 @@ namespace Umbraco.Web.Scheduling ~BackgroundTaskRunner() { - this.Dispose(false); + Dispose(false); } public void Dispose() @@ -493,7 +494,7 @@ namespace Umbraco.Web.Scheduling protected virtual void Dispose(bool disposing) { - if (this.IsDisposed || disposing == false) + if (IsDisposed || disposing == false) return; lock (_disposalLocker) @@ -531,23 +532,26 @@ namespace Umbraco.Web.Scheduling // processing, call the UnregisterObject method, and then return or it can return immediately and complete // processing asynchronously before calling the UnregisterObject method. - LogHelper.Info>("Shutting down, waiting for tasks to complete."); + LogHelper.Info(_logPrefix + "Shutting down, waiting for tasks to complete."); Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait + // raise the completed event only after the running task has completed + // and there's no more task running + lock (_locker) { if (_runningTask != null) _runningTask.ContinueWith(_ => { HostingEnvironment.UnregisterObject(this); - LogHelper.Info>("Down, tasks completed."); - Stopped.RaiseEvent(new StoppedEventArgs(false), this); + LogHelper.Info(_logPrefix + "Down, tasks completed."); + Completed.RaiseEvent(EventArgs.Empty, this); }); else { HostingEnvironment.UnregisterObject(this); - LogHelper.Info>("Down, tasks completed."); - Stopped.RaiseEvent(new StoppedEventArgs(false), this); + LogHelper.Info(_logPrefix + "Down, tasks completed."); + Completed.RaiseEvent(EventArgs.Empty, this); } } } @@ -558,24 +562,13 @@ namespace Umbraco.Web.Scheduling // immediate parameter is true, the registered object must call the UnregisterObject method before returning; // otherwise, its registration will be removed by the application manager. - LogHelper.Info>("Shutting down immediately."); + LogHelper.Info(_logPrefix + "Shutting down immediately."); Shutdown(true, true); // cancel all tasks, wait for the current one to end HostingEnvironment.UnregisterObject(this); - LogHelper.Info>("Down."); - Stopped.RaiseEvent(new StoppedEventArgs(true), this); + LogHelper.Info(_logPrefix + "Down."); + // raise the completed event: there's no more task running + Completed.RaiseEvent(EventArgs.Empty, this); } } - - public class StoppedEventArgs : EventArgs - { - public StoppedEventArgs(bool immediate) - { - Immediate = immediate; - } - - public bool Immediate { get; private set; } - } - - public event TypedEventHandler, StoppedEventArgs> Stopped; } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs index 0c38ee43a3..6f8e8a0748 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerAwaiter.cs @@ -15,7 +15,8 @@ namespace Umbraco.Web.Scheduling /// A custom awaiter requires implementing INotifyCompletion as well as IsCompleted, OnCompleted and GetResult /// see: http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx /// - internal class BackgroundTaskRunnerAwaiter : INotifyCompletion where T : class, IBackgroundTask + internal class BackgroundTaskRunnerAwaiter : INotifyCompletion + where T : class, IBackgroundTask { private readonly BackgroundTaskRunner _runner; private readonly TaskCompletionSource _tcs; @@ -27,21 +28,20 @@ namespace Umbraco.Web.Scheduling _runner = runner; _tcs = new TaskCompletionSource(); - _awaiter = _tcs.Task.GetAwaiter(); if (_runner.IsRunning) { - _runner.Completed += (s, e) => + _runner.Stopped += (s, e) => { - LogHelper.Debug>("Setting result"); - + LogHelper.Debug>("Runner has stopped."); _tcs.SetResult(0); }; } else { //not running, just set the result + LogHelper.Debug>("Runner is stopped."); _tcs.SetResult(0); } @@ -59,6 +59,7 @@ namespace Umbraco.Web.Scheduling { get { + // FIXME I DONT UNDERSTAND LogHelper.Debug>("IsCompleted :: " + _tcs.Task.IsCompleted + ", " + (_runner.IsRunning == false)); //Need to check if the task is completed because it might already be done on the ctor and the runner never runs return _tcs.Task.IsCompleted || _runner.IsRunning == false; diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index f7cec0079b..af3dedbe70 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -63,7 +63,7 @@ namespace Umbraco.Web.Scheduling public bool IsLatched { - get { return _latch != null; } + get { return _latch != null && _latch.IsSet == false; } } public virtual bool RunsOnShutdown diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index 6e586efad8..409a67028f 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -48,9 +48,9 @@ namespace Umbraco.Web.Scheduling LogHelper.Debug(() => "Initializing the scheduler"); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly - _publishingRunner = new BackgroundTaskRunner(); - _tasksRunner = new BackgroundTaskRunner(); - _scrubberRunner = new BackgroundTaskRunner(); + _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing"); + _tasksRunner = new BackgroundTaskRunner("ScheduledTasks"); + _scrubberRunner = new BackgroundTaskRunner("LogScrubber"); var settings = UmbracoConfig.For.UmbracoSettings(); diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 34ecccd4ed..e7dd090289 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -49,7 +49,7 @@ namespace umbraco // and prepare the persister task // there's always be one task keeping a ref to the runner // so it's safe to just create it as a local var here - var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions + var runner = new BackgroundTaskRunner("XmlCacheFilePersister", new BackgroundTaskRunnerOptions { LongRunning = true, KeepAlive = true @@ -58,7 +58,7 @@ namespace umbraco // when the runner has stopped we know we will not be writing // to the file anymore, so we can release the lock now - and // not wait for the AppDomain unload - runner.Stopped += (sender, args) => + runner.Completed += (sender, args) => { if (_fileLock == null) return; // not locking (testing?) if (_fileLocked == null) return; // not locked