diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index e83ce400d9..299c11881d 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -564,7 +564,7 @@ namespace Umbraco.Tests.Scheduling } } - private class MyDelayedTask : IDelayedBackgroundTask + private class MyDelayedTask : ILatchedBackgroundTask { private readonly int _runMilliseconds; private readonly ManualResetEvent _gate; @@ -577,12 +577,17 @@ namespace Umbraco.Tests.Scheduling _gate = new ManualResetEvent(false); } - public WaitHandle DelayWaitHandle + public WaitHandle Latch { get { return _gate; } } - public bool IsDelayed + public bool IsLatched + { + get { return true; } + } + + public bool RunsOnShutdown { get { return true; } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 482a666538..1bed36160f 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using System.Xml; +using umbraco; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Scheduling; @@ -19,22 +20,120 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : DisposableObject, IBackgroundTask + internal class XmlCacheFilePersister : ILatchedBackgroundTask { - private readonly XmlDocument _xDoc; + private readonly IBackgroundTaskRunner _runner; private readonly string _xmlFileName; private readonly ProfilingLogger _logger; + private readonly content _content; + private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); + private readonly object _locko = new object(); + private bool _released; + private Timer _timer; + private DateTime _initialTouch; - public XmlCacheFilePersister(XmlDocument xDoc, string xmlFileName, ProfilingLogger logger) + private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min) + private const int MaxWaitMilliseconds = 10000; // save the cache after some time (ie no more than 10s of changes) + + // save the cache when the app goes down + public bool RunsOnShutdown { get { return true; } } + + public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, string xmlFileName, ProfilingLogger logger, bool touched = false) { - _xDoc = xDoc; + _runner = runner; + _content = content; _xmlFileName = xmlFileName; _logger = logger; + + if (touched == false) return; + + LogHelper.Debug("Create new touched, start."); + + _initialTouch = DateTime.Now; + _timer = new Timer(_ => Release()); + + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); } + public XmlCacheFilePersister Touch() + { + lock (_locko) + { + if (_released) + { + LogHelper.Debug("Touched, was released, create new."); + + // released, has run or is running, too late, add & return a new task + var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, _logger, true); + _runner.Add(persister); + return persister; + } + + if (_timer == null) + { + LogHelper.Debug("Touched, was idle, start."); + + // not started yet, start + _initialTouch = DateTime.Now; + _timer = new Timer(_ => Release()); + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); + return this; + } + + // set the timer to trigger in WaitMilliseconds unless we've been touched first more + // than MaxWaitMilliseconds ago and then release now + + if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds)) + { + LogHelper.Debug("Touched, was waiting, wait.", () => WaitMilliseconds); + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); + } + else + { + LogHelper.Debug("Save now, release."); + ReleaseLocked(); + } + + return this; // still available + } + } + + private void Release() + { + lock (_locko) + { + ReleaseLocked(); + } + } + + private void ReleaseLocked() + { + LogHelper.Debug("Timer: save now, release."); + if (_timer != null) + _timer.Dispose(); + _timer = null; + _released = true; + _latch.Set(); + } + + public WaitHandle Latch + { + get { return _latch.WaitHandle; } + } + + public bool IsLatched + { + get { return true; } + } + public async Task RunAsync() { - await PersistXmlToFileAsync(_xDoc); + LogHelper.Debug("Run now."); + var doc = _content.XmlContentInternal; + await PersistXmlToFileAsync(doc); } public bool IsAsync @@ -91,14 +190,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - protected override void DisposeResources() - { - } + public void Dispose() + { } public void Run() { throw new NotImplementedException(); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index c511a0af53..82fb601815 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Scheduling private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); private readonly object _locker = new object(); - private readonly ManualResetEvent _completedEvent = new ManualResetEvent(false); + private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(false); private volatile bool _isRunning; // is running private volatile bool _isCompleted; // does not accept tasks anymore, may still be running @@ -304,13 +304,19 @@ namespace Umbraco.Web.Scheduling return; } - // wait for delayed task, supporting cancellation - var dbgTask = bgTask as IDelayedBackgroundTask; - if (dbgTask != null && dbgTask.IsDelayed) + // wait for latched task, supporting cancellation + var dbgTask = bgTask as ILatchedBackgroundTask; + if (dbgTask != null && dbgTask.IsLatched) { - WaitHandle.WaitAny(new[] { dbgTask.DelayWaitHandle, token.WaitHandle, _completedEvent }); + WaitHandle.WaitAny(new[] { dbgTask.Latch, token.WaitHandle, _completedEvent.WaitHandle }); if (TaskSourceCanceled(taskSource, token)) return; - // else run now, either because delay is ok or runner is completed + // else run now, either because latch ok or runner is completed + // still latched & not running on shutdown = stop here + if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) + { + TaskSourceCompleted(taskSource, token); + return; + } } // run the task as first task, or a continuation @@ -378,7 +384,7 @@ namespace Umbraco.Web.Scheduling OnTaskError(new TaskEventArgs(bgTask, e)); throw; } - Console.WriteLine("!1"); + OnTaskCompleted(new TaskEventArgs(bgTask)); } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index cac68241f4..3ecae089cc 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { @@ -7,52 +8,67 @@ namespace Umbraco.Web.Scheduling /// Provides a base class for recurring background tasks. /// /// The type of the managed tasks. - internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, IDelayedBackgroundTask + internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, ILatchedBackgroundTask where T : class, IBackgroundTask { - private readonly int _delayMilliseconds; - private ManualResetEvent _gate; + private readonly ManualResetEventSlim _latch; private Timer _timer; protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) : base(runner, periodMilliseconds) { - _delayMilliseconds = delayMilliseconds; + if (delayMilliseconds > 0) + { + _latch = new ManualResetEventSlim(false); + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _latch.Set(); + }); + _timer.Change(delayMilliseconds, 0); + } } protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) : base(source) { - _delayMilliseconds = 0; + // no latch on recurring instances + _latch = null; } - public WaitHandle DelayWaitHandle + public override void Run() + { + if (_latch != null) + _latch.Dispose(); + base.Run(); + } + + public override async Task RunAsync() + { + if (_latch != null) + _latch.Dispose(); + await base.RunAsync(); + } + + public WaitHandle Latch { get { - if (_delayMilliseconds == 0) return new ManualResetEvent(true); - - if (_gate != null) return _gate; - _gate = new ManualResetEvent(false); - - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _gate.Set(); - }); - _timer.Change(_delayMilliseconds, 0); - return _gate; + if (_latch == null) + throw new InvalidOperationException("The task is not latched."); + return _latch.WaitHandle; } } - public bool IsDelayed + public bool IsLatched { - get { return _delayMilliseconds > 0; } + get { return _latch != null; } + } + + public virtual bool RunsOnShutdown + { + get { return true; } } } } diff --git a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs deleted file mode 100644 index 01f8a5e01a..0000000000 --- a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Represents a delayed background task. - /// - /// Delayed background tasks can suspend their execution until - /// a condition is met. However if the tasks runner has to terminate, - /// delayed background tasks are executed immediately. - internal interface IDelayedBackgroundTask : IBackgroundTask - { - /// - /// Gets a wait handle on the task condition. - /// - WaitHandle DelayWaitHandle { get; } - - /// - /// Gets a value indicating whether the task is delayed. - /// - bool IsDelayed { get; } - } -} diff --git a/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs new file mode 100644 index 0000000000..0ad4d42bdf --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Represents a latched background task. + /// + /// Latched background tasks can suspend their execution until + /// a condition is met. However if the tasks runner has to terminate, + /// latched background tasks can be executed immediately, depending on + /// the value returned by RunsOnShutdown. + internal interface ILatchedBackgroundTask : IBackgroundTask + { + /// + /// Gets a wait handle on the task condition. + /// + /// The task is not latched. + WaitHandle Latch { get; } + + /// + /// Gets a value indicating whether the task is latched. + /// + bool IsLatched { get; } + + /// + /// Gets a value indicating whether the task can be executed immediately if the task runner has to terminate. + /// + bool RunsOnShutdown { get; } + } +} diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index a0c2c6979e..c4a5ce2c00 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -82,5 +82,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 553e62d3a0..6bae7406f9 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.Scheduling private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; private Timer _timer; + private T _recurrent; /// /// Initializes a new instance of the class with a tasks runner and a period. @@ -34,6 +35,7 @@ namespace Umbraco.Web.Scheduling protected RecurringTaskBase(RecurringTaskBase source) { _runner = source._runner; + _timer = source._timer; _periodMilliseconds = source._periodMilliseconds; } @@ -41,7 +43,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public void Run() + public virtual void Run() { PerformRun(); Repeat(); @@ -51,7 +53,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public async Task RunAsync() + public virtual async Task RunAsync() { await PerformRunAsync(); Repeat(); @@ -64,19 +66,19 @@ namespace Umbraco.Web.Scheduling if (_periodMilliseconds == 0) return; - var recur = GetRecurring(); - if (recur == null) return; // done + _recurrent = GetRecurring(); + if (_recurrent == null) + { + _timer.Dispose(); + _timer = null; + return; // done + } // note // must use the single-parameter constructor on Timer to avoid it from being GC'd // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _runner.TryAdd(recur); - }); + _timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent)); _timer.Change(_periodMilliseconds, 0); } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index de92374379..9db21fba8a 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -97,5 +97,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index bd3a3524f6..cba3cb4fc8 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -130,5 +130,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 58821bec6b..24d5058e31 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -501,7 +501,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index cbf296115a..fa2e93b0b1 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -35,6 +35,15 @@ namespace umbraco private static readonly BackgroundTaskRunner FilePersister = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true }); + private XmlCacheFilePersister _persisterTask; + + private content() + { + _persisterTask = new XmlCacheFilePersister(FilePersister, this, UmbracoXmlDiskCacheFileName, + new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler)); + FilePersister.Add(_persisterTask); + } + #region Declarations // Sync access to disk file @@ -131,7 +140,7 @@ namespace umbraco /// /// Before returning we always check to ensure that the xml is loaded /// - protected virtual XmlDocument XmlContentInternal + protected internal virtual XmlDocument XmlContentInternal { get { @@ -317,8 +326,7 @@ namespace umbraco // and clear the queue in case is this a web request, we don't want it reprocessing. if (UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache) { - FilePersister.Add(new XmlCacheFilePersister(xmlDoc, UmbracoXmlDiskCacheFileName , - new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); + QueueXmlForPersistence(); } } } @@ -1230,8 +1238,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; /// private void QueueXmlForPersistence() { - FilePersister.Add(new XmlCacheFilePersister(_xmlContent, UmbracoXmlDiskCacheFileName, - new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); + _persisterTask = _persisterTask.Touch(); } internal DateTime GetCacheFileUpdateTime()