diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index 573adeda3d..cac68241f4 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -34,12 +34,18 @@ namespace Umbraco.Web.Scheduling 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(); - }, null, _delayMilliseconds, 0); + }); + _timer.Change(_delayMilliseconds, 0); return _gate; } } diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 2edbe80726..a0c2c6979e 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -9,17 +9,31 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - internal class LogScrubber : DisposableObject, IBackgroundTask + internal class LogScrubber : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(ApplicationContext appContext, IUmbracoSettingsSection settings) + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } + public LogScrubber(LogScrubber source) + : base(source) + { + _appContext = source._appContext; + _settings = source._settings; + } + + protected override LogScrubber GetRecurring() + { + return new LogScrubber(this); + } + private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { int maximumAge = 24 * 60 * 60; @@ -36,14 +50,22 @@ namespace Umbraco.Web.Scheduling } - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { + public static 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; } - public void Run() + public override void PerformRun() { using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) { @@ -51,12 +73,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 91d86d97b4..553e62d3a0 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -67,12 +67,17 @@ namespace Umbraco.Web.Scheduling var recur = GetRecurring(); if (recur == 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); - }, null, _periodMilliseconds, 0); + }); + _timer.Change(_periodMilliseconds, 0); } /// diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index cacb4e133d..de92374379 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -12,30 +12,41 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing : DisposableObject, IBackgroundTask + internal class ScheduledPublishing : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private static bool _isPublishingRunning = false; + private static bool _isPublishingRunning; - public ScheduledPublishing(ApplicationContext appContext, IUmbracoSettingsSection settings) + public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() + private ScheduledPublishing(ScheduledPublishing source) + : base(source) { + _appContext = source._appContext; + _settings = source._settings; } - public void Run() + protected override ScheduledPublishing GetRecurring() + { + return new ScheduledPublishing(this); + } + + public override void PerformRun() { if (_appContext == null) return; + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return; + } using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) { @@ -77,12 +88,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index ddcb9ea533..bd3a3524f6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -17,19 +17,33 @@ 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 : DisposableObject, IBackgroundTask + internal class ScheduledTasks : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); private static bool _isPublishingRunning = false; - public ScheduledTasks(ApplicationContext appContext, IUmbracoSettingsSection settings) + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } + public ScheduledTasks(ScheduledTasks source) + : base(source) + { + _appContext = source._appContext; + _settings = source._settings; + } + + protected override ScheduledTasks GetRecurring() + { + return new ScheduledTasks(this); + } + private void ProcessTasks() { var scheduledTasks = _settings.ScheduledTasks.Tasks; @@ -78,15 +92,14 @@ 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 override void PerformRun() { - } + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return; + } - public void Run() - { using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { if (_isPublishingRunning) return; @@ -108,12 +121,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index ee02947e20..6e586efad8 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -19,12 +19,10 @@ namespace Umbraco.Web.Scheduling internal sealed class Scheduler : ApplicationEventHandler { private static Timer _pingTimer; - private static Timer _schedulingTimer; - private static BackgroundTaskRunner _publishingRunner; - private static BackgroundTaskRunner _tasksRunner; - private static BackgroundTaskRunner _scrubberRunner; - private static Timer _logScrubberTimer; - private static volatile bool _started = false; + private static BackgroundTaskRunner _publishingRunner; + private static BackgroundTaskRunner _tasksRunner; + private static BackgroundTaskRunner _scrubberRunner; + private static volatile bool _started; private static readonly object Locker = new object(); protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) @@ -49,97 +47,37 @@ namespace Umbraco.Web.Scheduling _started = true; LogHelper.Debug(() => "Initializing the scheduler"); - // time to setup the tasks + // 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(); - //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(); + var settings = UmbracoConfig.For.UmbracoSettings(); - //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. + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // also make the timer static to ensure further GC safety + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - // 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 + // ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); _pingTimer.Change(60000, 300000); // scheduled publishing/unpublishing - _schedulingTimer = new Timer(state => PerformScheduling(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _schedulingTimer.Change(60000, 60000); + // install on all, will only run on non-slaves servers + // both are delayed recurring tasks + _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); + _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); - //log scrubbing - _logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _logScrubberTimer.Change(60000, GetLogScrubbingInterval(UmbracoConfig.For.UmbracoSettings())); + // log scrubbing + // install & run on all servers + // LogScrubber is a delayed recurring task + _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); } } } }; }; } - - - private int GetLogScrubbingInterval(IUmbracoSettingsSection settings) - { - var interval = 4 * 60 * 60 * 1000; // 4 hours, in milliseconds - try - { - if (settings.Logging.CleaningMiliseconds > -1) - interval = settings.Logging.CleaningMiliseconds; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 4 hours.", 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(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(settings); - - switch (serverStatus) - { - case CurrentServerEnvironmentStatus.Single: - case CurrentServerEnvironmentStatus.Master: - case CurrentServerEnvironmentStatus.Unknown: - //if it's a single server install, a master or it cannot be determined - // then we will process the scheduling - - //do the scheduled publishing - _publishingRunner.Add(new ScheduledPublishing(appContext, settings)); - - //do the scheduled tasks - _tasksRunner.Add(new ScheduledTasks(appContext, settings)); - - break; - case CurrentServerEnvironmentStatus.Slave: - //do not process - - LogHelper.Debug( - () => string.Format("Current server ({0}) detected as a slave, no scheduled processes will execute on this server", NetworkHelper.MachineName)); - - break; - } - } - } - } }