From e8f7f77bb6182168ca95fbc8b981a01ee04b4717 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Jun 2014 14:34:21 +1000 Subject: [PATCH] Fixes: U4-581 Automatic publishing not working in load balanced setup - added some more convention and configuration to distributed calls so that servers are aware of the master and how to call into themselves for scheduled tasks, ping and scheduled publishing. Will need to update the docs on LB regarding this too. Cleaned up the code that does the scheduling and separates it into proper segments. Obsoletes the old presentation classes that were doing it. --- .../Publishing/ScheduledPublisher.cs | 61 +++++++++ .../Sync/CurrentServerEnvironmentStatus.cs | 28 +++++ .../Sync/ServerEnvironmentHelper.cs | 116 ++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 3 + src/Umbraco.Tests/TypeHelperTests.cs | 3 +- .../config/umbracoSettings.Release.config | 41 ++++++- .../config/umbracoSettings.config | 47 ++++++- src/Umbraco.Web/LegacyScheduledTasks.cs | 107 ---------------- src/Umbraco.Web/Scheduling/KeepAlive.cs | 35 ++++++ src/Umbraco.Web/Scheduling/LogScrubber.cs | 77 ++++++++++++ .../Scheduling/ScheduledPublishing.cs | 47 +++++++ src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 98 +++++++++++++++ src/Umbraco.Web/Scheduling/Scheduler.cs | 82 +++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 7 +- src/Umbraco.Web/WebBootManager.cs | 3 +- .../WebServices/ScheduledPublishController.cs | 59 +++++++++ .../umbraco.presentation/keepAliveService.cs | 6 +- .../umbraco.presentation/publishingService.cs | 63 +++------- 18 files changed, 711 insertions(+), 172 deletions(-) create mode 100644 src/Umbraco.Core/Publishing/ScheduledPublisher.cs create mode 100644 src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs create mode 100644 src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs delete mode 100644 src/Umbraco.Web/LegacyScheduledTasks.cs create mode 100644 src/Umbraco.Web/Scheduling/KeepAlive.cs create mode 100644 src/Umbraco.Web/Scheduling/LogScrubber.cs create mode 100644 src/Umbraco.Web/Scheduling/ScheduledPublishing.cs create mode 100644 src/Umbraco.Web/Scheduling/ScheduledTasks.cs create mode 100644 src/Umbraco.Web/Scheduling/Scheduler.cs create mode 100644 src/Umbraco.Web/WebServices/ScheduledPublishController.cs diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs new file mode 100644 index 0000000000..45492423ab --- /dev/null +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -0,0 +1,61 @@ +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Publishing +{ + /// + /// Used to perform scheduled publishing/unpublishing + /// + internal class ScheduledPublisher + { + private readonly IContentService _contentService; + + public ScheduledPublisher(IContentService contentService) + { + _contentService = contentService; + } + + public void CheckPendingAndProcess() + { + foreach (var d in _contentService.GetContentForRelease()) + { + try + { + d.ReleaseDate = null; + var result = _contentService.SaveAndPublishWithStatus(d, (int)d.GetWriterProfile().Id); + if (result.Success == false) + { + if (result.Exception != null) + { + LogHelper.Error("Could not published the document (" + d.Id + ") based on it's scheduled release, status result: " + result.Result.StatusType, result.Exception); + } + else + { + LogHelper.Warn("Could not published the document (" + d.Id + ") based on it's scheduled release. Status result: " + result.Result.StatusType); + } + } + } + catch (Exception ee) + { + LogHelper.Error(string.Format("Error publishing node {0}", d.Id), ee); + throw; + } + } + foreach (var d in _contentService.GetContentForExpiration()) + { + try + { + d.ExpireDate = null; + _contentService.UnPublish(d, (int)d.GetWriterProfile().Id); + } + catch (Exception ee) + { + LogHelper.Error(string.Format("Error unpublishing node {0}", d.Id), ee); + throw; + } + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs b/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs new file mode 100644 index 0000000000..95305b7cdc --- /dev/null +++ b/src/Umbraco.Core/Sync/CurrentServerEnvironmentStatus.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Sync +{ + /// + /// The current status of the server in the Umbraco environment + /// + internal enum CurrentServerEnvironmentStatus + { + /// + /// If the current server is detected as the 'master' server when configured in a load balanced scenario + /// + Master, + + /// + /// If the current server is detected as a 'slave' server when configured in a load balanced scenario + /// + Slave, + + /// + /// If the current server cannot be detected as a 'slave' or 'master' when configured in a load balanced scenario + /// + Unknown, + + /// + /// If load balancing is not enabled and this is the only server in the umbraco environment + /// + Single + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs new file mode 100644 index 0000000000..c5363b494f --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Web; +using System.Xml; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Sync +{ + /// + /// A helper used to determine the current server environment status + /// + internal static class ServerEnvironmentHelper + { + /// + /// Returns the current umbraco base url for the current server depending on it's environment + /// 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() + { + 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 ApplicationContext.Current.OriginalRequestUrl; + } + + var servers = UmbracoSettings.DistributionServers; + + var nodes = servers.SelectNodes("./server"); + if (nodes == null) + { + //cannot be determined, then the base url has to be the first url registered + return ApplicationContext.Current.OriginalRequestUrl; + } + + var xmlNodes = nodes.Cast().ToList(); + + foreach (var xmlNode in xmlNodes) + { + var appId = xmlNode.AttributeValue("appId"); + var serverName = xmlNode.AttributeValue("serverName"); + + if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) + { + continue; + } + + if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) + { + //match by appId or computer name! return the url configured + return string.Format("{0}://{1}:{2}/{3}", + xmlNode.AttributeValue("forceProtocol").IsNullOrWhiteSpace() ? "http" : xmlNode.AttributeValue("forceProtocol"), + 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 ApplicationContext.Current.OriginalRequestUrl; + } + + /// + /// Returns the current environment status for the current server + /// + /// + public static CurrentServerEnvironmentStatus GetStatus() + { + if (UmbracoSettings.UseDistributedCalls == false) + { + return CurrentServerEnvironmentStatus.Single; + } + + var servers = UmbracoSettings.DistributionServers; + + var nodes = servers.SelectNodes("./server"); + if (nodes == null) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + var master = nodes.Cast().FirstOrDefault(); + + if (master == null) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + //we determine master/slave based on the first server registered + //TODO: In v7 we have publicized ServerRegisterResolver - we won't be able to determine this based on that + // but we'd need to change the IServerAddress interfaces which is breaking. + + var appId = master.AttributeValue("appId"); + var serverName = master.AttributeValue("serverName"); + + if (appId.IsNullOrWhiteSpace() && serverName.IsNullOrWhiteSpace()) + { + return CurrentServerEnvironmentStatus.Unknown; + } + + if ((appId.IsNullOrWhiteSpace() == false && appId.Trim().InvariantEquals(HttpRuntime.AppDomainAppId)) + || (serverName.IsNullOrWhiteSpace() == false && serverName.Trim().InvariantEquals(NetworkHelper.MachineName))) + { + //match by appdid or server name! + return CurrentServerEnvironmentStatus.Master; + } + + return CurrentServerEnvironmentStatus.Slave; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 89752d47ed..d0d0cd573b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -770,6 +770,7 @@ + @@ -830,7 +831,9 @@ + + diff --git a/src/Umbraco.Tests/TypeHelperTests.cs b/src/Umbraco.Tests/TypeHelperTests.cs index f637b1ff20..6e9ee10152 100644 --- a/src/Umbraco.Tests/TypeHelperTests.cs +++ b/src/Umbraco.Tests/TypeHelperTests.cs @@ -9,6 +9,7 @@ using Umbraco.Core; using Umbraco.Tests.PartialTrust; using Umbraco.Web; using Umbraco.Web.Cache; +using Umbraco.Web.Scheduling; using UmbracoExamine; using umbraco; using umbraco.presentation; @@ -67,7 +68,7 @@ namespace Umbraco.Tests Assert.AreEqual(typeof(UmbracoEventManager), t5.Result); var t6 = TypeHelper.GetLowestBaseType(typeof (IApplicationEventHandler), - typeof (LegacyScheduledTasks), + typeof (Scheduler), typeof(CacheRefresherEventHandler)); Assert.IsTrue(t6.Success); Assert.AreEqual(typeof(IApplicationEventHandler), t6.Result); diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 0dc21ae2fc..0851581225 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -223,16 +223,47 @@ - + 0 + + + - - - - + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index a089b97608..0a60537af5 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -223,16 +223,51 @@ - - + + 0 + + + - - - - + + + + localhost + umb1.dev + umb2.dev + diff --git a/src/Umbraco.Web/LegacyScheduledTasks.cs b/src/Umbraco.Web/LegacyScheduledTasks.cs deleted file mode 100644 index 8b4ccce8e9..0000000000 --- a/src/Umbraco.Web/LegacyScheduledTasks.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Web; -using System.Web.Caching; -using Umbraco.Core; -using Umbraco.Core.Logging; -using global::umbraco.BusinessLogic; - -namespace Umbraco.Web -{ - // note: has to be public to be detected by the resolver - // if it's made internal, which would make more sense, then it's not detected - // and it needs to be manually registered - which we want to avoid, in order - // to be as unobtrusive as possible - - internal sealed class LegacyScheduledTasks : ApplicationEventHandler - { - Timer _pingTimer; - Timer _publishingTimer; - CacheItemRemovedCallback _onCacheRemove; - - protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, Core.ApplicationContext applicationContext) - { - if (umbracoApplication.Context == null) - return; - - // time to setup the tasks - - // these are the legacy tasks - // just copied over here for backward compatibility - // of course we should have a proper scheduler, see #U4-809 - - // ping/keepalive - _pingTimer = new Timer(new TimerCallback(global::umbraco.presentation.keepAliveService.PingUmbraco), applicationContext, 60000, 300000); - - // (un)publishing _and_ also run scheduled tasks (!) - _publishingTimer = new Timer(new TimerCallback(global::umbraco.presentation.publishingService.CheckPublishing), applicationContext, 30000, 60000); - - // log scrubbing - AddTask(LOG_SCRUBBER_TASK_NAME, GetLogScrubbingInterval()); - } - - #region Log Scrubbing - - // this is a raw copy of the legacy code in all its uglyness - - const string LOG_SCRUBBER_TASK_NAME = "ScrubLogs"; - - 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() - { - int maximumAge = 24 * 60 * 60; - try - { - if (global::umbraco.UmbracoSettings.MaxLogAge > -1) - maximumAge = global::umbraco.UmbracoSettings.MaxLogAge; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); - } - return maximumAge; - - } - - 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); - } - - public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r) - { - if (k.Equals(LOG_SCRUBBER_TASK_NAME)) - { - ScrubLogs(); - } - AddTask(k, Convert.ToInt32(v)); - } - - private static void ScrubLogs() - { - Log.CleanLogs(GetLogScrubbingMaximumAge()); - } - - #endregion - } -} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs new file mode 100644 index 0000000000..443c3727e1 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + internal class KeepAlive + { + public static void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + var appContext = sender as ApplicationContext; + if (appContext == null) return; + + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs new file mode 100644 index 0000000000..cea4d1b01c --- /dev/null +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -0,0 +1,77 @@ +using System; +using System.Web; +using System.Web.Caching; +using umbraco.BusinessLogic; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Scheduling +{ + //TODO: Refactor this to use a normal scheduling processor! + + internal class LogScrubber + { + // this is a raw copy of the legacy code in all its uglyness + + CacheItemRemovedCallback _onCacheRemove; + const string LogScrubberTaskName = "ScrubLogs"; + + public void Start() + { + // 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; + } + + private static int GetLogScrubbingMaximumAge() + { + int maximumAge = 24 * 60 * 60; + try + { + if (global::umbraco.UmbracoSettings.MaxLogAge > -1) + maximumAge = global::umbraco.UmbracoSettings.MaxLogAge; + } + catch (Exception e) + { + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); + } + return maximumAge; + + } + + 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); + } + + public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r) + { + if (k.Equals(LogScrubberTaskName)) + { + ScrubLogs(); + } + 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 new file mode 100644 index 0000000000..7a249ce4ac --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -0,0 +1,47 @@ +using System; +using System.Diagnostics; +using System.Net; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + internal class ScheduledPublishing + { + private static bool _isPublishingRunning = false; + + public void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + var appContext = sender as ApplicationContext; + if (appContext == null) return; + + if (_isPublishingRunning) return; + + _isPublishingRunning = true; + + try + { + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + var url = string.Format("{0}/RestServices/ScheduledPublish/", umbracoBaseUrl); + using (var wc = new WebClient()) + { + var result = wc.UploadString(url, ""); + } + } + catch (Exception ee) + { + LogHelper.Error("An error occurred with the scheduled publishing", ee); + } + finally + { + _isPublishingRunning = false; + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs new file mode 100644 index 0000000000..9abf7d9f69 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Net; +using System.Xml; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Core.Sync; + +namespace Umbraco.Web.Scheduling +{ + //TODO: No scheduled task (i.e. URL) would be secured, so if people are actually using these each task + // would need to be a publicly available task (URL) which isn't really very good :( + + internal class ScheduledTasks + { + private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); + private static bool _isPublishingRunning = false; + + public void Start(object sender) + { + //NOTE: sender will be the umbraco ApplicationContext + + if (_isPublishingRunning) return; + + _isPublishingRunning = true; + + try + { + ProcessTasks(); + } + catch (Exception ee) + { + LogHelper.Error("Error executing scheduled task", ee); + } + finally + { + _isPublishingRunning = false; + } + } + + private static void ProcessTasks() + { + + + var scheduledTasks = UmbracoSettings.ScheduledTasks; + if (scheduledTasks != null) + { + var tasks = scheduledTasks.SelectNodes("./task"); + if (tasks == null) return; + + foreach (XmlNode task in tasks) + { + var runTask = false; + if (ScheduledTaskTimes.ContainsKey(task.Attributes.GetNamedItem("alias").Value) == false) + { + runTask = true; + ScheduledTaskTimes.Add(task.Attributes.GetNamedItem("alias").Value, DateTime.Now); + } + // Add 1 second to timespan to compensate for differencies in timer + else if (new TimeSpan( + DateTime.Now.Ticks - ((DateTime)ScheduledTaskTimes[task.Attributes.GetNamedItem("alias").Value]).Ticks).TotalSeconds + 1 + >= int.Parse(task.Attributes.GetNamedItem("interval").Value)) + { + runTask = true; + ScheduledTaskTimes[task.Attributes.GetNamedItem("alias").Value] = DateTime.Now; + } + + if (runTask) + { + bool taskResult = GetTaskByHttp(task.Attributes.GetNamedItem("url").Value); + if (bool.Parse(task.Attributes.GetNamedItem("log").Value)) + LogHelper.Info(string.Format("{0} has been called with response: {1}", task.Attributes.GetNamedItem("alias").Value, taskResult)); + } + } + } + } + + private static bool GetTaskByHttp(string url) + { + var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); + + try + { + using (var response = (HttpWebResponse)myHttpWebRequest.GetResponse()) + { + return response.StatusCode == HttpStatusCode.OK; + } + } + catch (Exception ex) + { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs new file mode 100644 index 0000000000..75930fed4c --- /dev/null +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -0,0 +1,82 @@ +using System.Threading; +using Umbraco.Core; +using Umbraco.Core.Sync; + +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. + /// + /// + internal sealed class Scheduler : ApplicationEventHandler + { + private Timer _pingTimer; + private Timer _schedulingTimer; + private LogScrubber _scrubber; + + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + if (umbracoApplication.Context == null) + return; + + // time to setup the tasks + + // these are the legacy tasks + // just copied over here for backward compatibility + // of course we should have a proper scheduler, see #U4-809 + + // ping/keepalive + _pingTimer = new Timer(KeepAlive.Start, applicationContext, 60000, 300000); + + // scheduled publishing/unpublishing + + _schedulingTimer = new Timer(PerformScheduling, applicationContext, 30000, 60000); + + //log scrubbing + _scrubber = new LogScrubber(); + _scrubber.Start(); + } + + /// + /// 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) + { + + //get the current server status to see if this server should execute the scheduled publishing + var serverStatus = ServerEnvironmentHelper.GetStatus(); + + 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 + var scheduledPublishing = new ScheduledPublishing(); + scheduledPublishing.Start(sender); + + //do the scheduled tasks + var scheduledTasks = new ScheduledTasks(); + scheduledTasks.Start(sender); + + break; + case CurrentServerEnvironmentStatus.Slave: + //do not process + break; + } + } + + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 49da691cd2..a8bcafd5f7 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -325,6 +325,10 @@ + + + + @@ -499,7 +503,7 @@ ASPXCodeBehind - + @@ -1782,6 +1786,7 @@ + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 05398a708c..5a313b2350 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -24,6 +24,7 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Web.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; +using Umbraco.Web.Scheduling; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; using umbraco.presentation.cache; @@ -116,7 +117,7 @@ namespace Umbraco.Web protected override void InitializeApplicationEventsResolver() { base.InitializeApplicationEventsResolver(); - ApplicationEventsResolver.Current.AddType(); + ApplicationEventsResolver.Current.AddType(); //We need to remove these types because we've obsoleted them and we don't want them executing: ApplicationEventsResolver.Current.RemoveType(); } diff --git a/src/Umbraco.Web/WebServices/ScheduledPublishController.cs b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs new file mode 100644 index 0000000000..bc2b5df18c --- /dev/null +++ b/src/Umbraco.Web/WebServices/ScheduledPublishController.cs @@ -0,0 +1,59 @@ +using System; +using System.Web.Mvc; +using umbraco; +using Umbraco.Core.Logging; +using Umbraco.Core.Publishing; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.WebServices +{ + //TODO: How to authenticate? + + /// + /// A REST controller used for running the scheduled publishing, this is called from the background worker timer + /// + public class ScheduledPublishController : UmbracoController + { + private static bool _isPublishingRunning = false; + + [HttpPost] + public JsonResult Index() + { + if (_isPublishingRunning) + return null; + _isPublishingRunning = true; + + try + { + // DO not run publishing if content is re-loading + if (content.Instance.isInitializing == false) + { + var publisher = new ScheduledPublisher(Services.ContentService); + publisher.CheckPendingAndProcess(); + } + + return Json(new + { + success = true + }); + + } + catch (Exception ee) + { + LogHelper.Error("Error executing scheduled task", ee); + + Response.StatusCode = 400; + + return Json(new + { + success = false, + message = ee.Message + }); + } + finally + { + _isPublishingRunning = false; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs index 4ab1646a9e..6d80d8bedd 100644 --- a/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs +++ b/src/Umbraco.Web/umbraco.presentation/keepAliveService.cs @@ -7,9 +7,7 @@ using Umbraco.Core.Logging; namespace umbraco.presentation { - /// - /// Makes a call to /umbraco/ping.aspx which is used to keep the web app alive - /// + [Obsolete("This is no longer used and will be removed in future versions")] public class keepAliveService { //NOTE: sender will be the umbraco ApplicationContext @@ -20,6 +18,8 @@ namespace umbraco.presentation var appContext = (ApplicationContext) sender; + //TODO: This won't always work, in load balanced scenarios ping will not work because + // this original request url will be public and not internal to the server. var url = string.Format("http://{0}/ping.aspx", appContext.OriginalRequestUrl); try { diff --git a/src/Umbraco.Web/umbraco.presentation/publishingService.cs b/src/Umbraco.Web/umbraco.presentation/publishingService.cs index d22165d648..7bd85236e6 100644 --- a/src/Umbraco.Web/umbraco.presentation/publishingService.cs +++ b/src/Umbraco.Web/umbraco.presentation/publishingService.cs @@ -4,15 +4,15 @@ using System.Diagnostics; using System.Net; using System.Web; using System.Xml; +using Umbraco.Core; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Publishing; namespace umbraco.presentation { - /// - /// Summary description for publishingService. - /// + [Obsolete("This is no longer used and will be removed in future versions")] public class publishingService { private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); @@ -26,37 +26,10 @@ namespace umbraco.presentation _isPublishingRunning = true; try { - // DO not run publishing if content is re-loading - if(!content.Instance.isInitializing) - { - - foreach (var d in Document.GetDocumentsForRelease()) - { - try - { - d.ReleaseDate = DateTime.MinValue; //new DateTime(1, 1, 1); // Causes release date to be null - d.SaveAndPublish(d.User); - } - catch(Exception ee) - { - LogHelper.Error(string.Format("Error publishing node {0}", d.Id), ee); - } - } - foreach(Document d in Document.GetDocumentsForExpiration()) - { - try - { - d.ExpireDate = DateTime.MinValue; + //run the scheduled publishing - we need to determine if this server - d.UnPublish(); - } - catch (Exception ee) - { - LogHelper.Error(string.Format("Error unpublishing node {0}", d.Id), ee); - } - - } - } + var publisher = new ScheduledPublisher(ApplicationContext.Current.Services.ContentService); + publisher.CheckPendingAndProcess(); // run scheduled url tasks try @@ -70,7 +43,7 @@ namespace umbraco.presentation foreach (XmlNode task in tasks) { bool runTask = false; - if (!ScheduledTaskTimes.ContainsKey(task.Attributes.GetNamedItem("alias").Value)) + if (ScheduledTaskTimes.ContainsKey(task.Attributes.GetNamedItem("alias").Value) == false) { runTask = true; ScheduledTaskTimes.Add(task.Attributes.GetNamedItem("alias").Value, DateTime.Now); @@ -88,7 +61,7 @@ namespace umbraco.presentation if (runTask) { - bool taskResult = getTaskByHttp(task.Attributes.GetNamedItem("url").Value); + bool taskResult = GetTaskByHttp(task.Attributes.GetNamedItem("url").Value); if (bool.Parse(task.Attributes.GetNamedItem("log").Value)) LogHelper.Info(string.Format("{0} has been called with response: {1}", task.Attributes.GetNamedItem("alias").Value, taskResult)); } @@ -111,26 +84,20 @@ namespace umbraco.presentation } } - private static bool getTaskByHttp(string url) + private static bool GetTaskByHttp(string url) { var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); HttpWebResponse myHttpWebResponse = null; try { - myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse(); - if(myHttpWebResponse.StatusCode == HttpStatusCode.OK) - { - myHttpWebResponse.Close(); - return true; - } - else - { - myHttpWebResponse.Close(); - return false; - } + using (myHttpWebResponse = (HttpWebResponse) myHttpWebRequest.GetResponse()) + { + return myHttpWebResponse.StatusCode == HttpStatusCode.OK; + } } - catch + catch (Exception ex) { + LogHelper.Error("An error occurred calling web task for url: " + url, ex); } finally {