using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Threading; using System.Web.Services.Protocols; using System.Xml; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using umbraco.BusinessLogic; namespace Umbraco.Web.Cache { public static class DistributedCacheExtensions { /// /// Refreshes the cache amongst servers for a template /// /// /// public static void RefreshTemplateCache(this DistributedCache dc, int templateId) { dc.Refresh(new Guid(DistributedCache.TemplateRefresherId), templateId); } /// /// Refreshes the cache amongst servers for all pages /// /// public static void RefreshAllPageCache(this DistributedCache dc) { dc.RefreshAll(new Guid(DistributedCache.PageCacheRefresherId)); } /// /// Refreshes the cache amongst servers for a page /// /// /// public static void RefreshPageCache(this DistributedCache dc, int pageId) { dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), pageId); } /// /// Removes the cache amongst servers for a page /// /// /// public static void RemovePageCache(this DistributedCache dc, int pageId) { dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), pageId); } } /// /// DistrubutedCacheDispatcher is used to handle Umbraco's load balancing. /// /// /// Distributing calls to all registered load balanced servers, ensuring that content are synced and cached on all servers. /// Dispatcher is exendable, so 3rd party services can easily be integrated into the workflow, using the interfaces.ICacheRefresher interface. /// /// Dispatcher can refresh/remove content, templates and macros. /// Load balanced servers are registered in umbracoSettings.config. /// /// UPDATE 2010 02 - Alex Norcliffe - Refactored Dispatcher to support parallel dispatch threads, and preventing failure of whole dispatch /// if one node fails. Still needs more work to get it to Enterprise level though but this is for 4.1 /// public class DistributedCache { #region Public constants/Ids public const string TemplateRefresherId = "DD12B6A0-14B9-46e8-8800-C154F74047C8"; public const string PageCacheRefresherId = "27AB3022-3DFA-47b6-9119-5945BC88FD66"; #endregion private readonly string _login; private readonly string _password; private readonly string _webServicesUrl; private static readonly DistributedCache InstanceObject = new DistributedCache(); /// /// Constructor /// private DistributedCache() { _login = User.GetUser(UmbracoSettings.DistributedCallUser).LoginName; _password = User.GetUser(UmbracoSettings.DistributedCallUser).GetPassword(); _webServicesUrl = IOHelper.ResolveUrl(SystemDirectories.WebServices); } /// /// Singleton /// /// public static DistributedCache Instance { get { return InstanceObject; } } /// /// Sends a request to all registered load-balanced servers to refresh node with the specified Id /// using the specified ICacheRefresher with the guid factoryGuid. /// /// The unique identifier of the ICacheRefresher used to refresh the node. /// The id of the node. public void Refresh(Guid factoryGuid, int id) { InvokeDispatchMethod(DispatchType.RefreshByNumericId, factoryGuid, id, Guid.Empty); } /// /// Sends a request to all registered load-balanced servers to refresh the node with the specified guid /// using the specified ICacheRefresher with the guid factoryGuid. /// /// The unique identifier of the ICacheRefresher used to refresh the node. /// The guid of the node. public void Refresh(Guid factoryGuid, Guid id) { InvokeDispatchMethod(DispatchType.RefreshByGuid, factoryGuid, 0, id); } /// /// Sends a request to all registered load-balanced servers to refresh all nodes /// using the specified ICacheRefresher with the guid factoryGuid. /// /// The unique identifier. public void RefreshAll(Guid factoryGuid) { InvokeDispatchMethod(DispatchType.RefreshAll, factoryGuid, 0, Guid.Empty); } /// /// Sends a request to all registered load-balanced servers to remove the node with the specified id /// using the specified ICacheRefresher with the guid factoryGuid. /// /// The unique identifier. /// The id. public void Remove(Guid factoryGuid, int id) { InvokeDispatchMethod(DispatchType.RemoveById, factoryGuid, id, Guid.Empty); } /// /// Invokes the relevant dispatch method. /// /// Type of the dispatch. /// The factory GUID. /// The numeric id. /// The GUID id. private void InvokeDispatchMethod(DispatchType dispatchType, Guid factoryGuid, int numericId, Guid guidId) { //TODO: THIS IS NOT USED, WHY IS IT HERE?? var name = GetFactoryObjectName(factoryGuid); try { using (var cacheRefresher = new CacheRefresherClient()) { var asyncResultsList = new List(); LogStartDispatch(); // Go through each configured node submitting a request asynchronously foreach (XmlNode n in GetDistributedNodes()) { SetWebServiceUrlFromNode(cacheRefresher, n); // Add the returned WaitHandle to the list for later checking switch (dispatchType) { case DispatchType.RefreshAll: asyncResultsList.Add(cacheRefresher.BeginRefreshAll(factoryGuid, _login, _password, null, null)); break; case DispatchType.RefreshByGuid: asyncResultsList.Add(cacheRefresher.BeginRefreshByGuid(factoryGuid, guidId, _login, _password, null, null)); break; case DispatchType.RefreshByNumericId: asyncResultsList.Add(cacheRefresher.BeginRefreshById(factoryGuid, numericId, _login, _password, null, null)); break; case DispatchType.RemoveById: asyncResultsList.Add(cacheRefresher.BeginRemoveById(factoryGuid, numericId, _login, _password, null, null)); break; } } List waitHandlesList; IAsyncResult[] asyncResults = GetAsyncResults(asyncResultsList, out waitHandlesList); int errorCount = 0; // Once for each WaitHandle that we have, wait for a response and log it // We're previously submitted all these requests effectively in parallel and will now retrieve responses on a FIFO basis for (int waitCalls = 0; waitCalls < asyncResults.Length; waitCalls++) { int handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); try { // Find out if the call succeeded switch (dispatchType) { case DispatchType.RefreshAll: cacheRefresher.EndRefreshAll(asyncResults[waitCalls]); break; case DispatchType.RefreshByGuid: cacheRefresher.EndRefreshByGuid(asyncResults[waitCalls]); break; case DispatchType.RefreshByNumericId: cacheRefresher.EndRefreshById(asyncResults[waitCalls]); break; case DispatchType.RemoveById: cacheRefresher.EndRemoveById(asyncResults[waitCalls]); break; } } catch (WebException ex) { LogDispatchNodeError(ex); errorCount++; } catch (Exception ex) { LogDispatchNodeError(ex); errorCount++; } } LogDispatchBatchResult(errorCount); } } catch (Exception ee) { LogDispatchBatchError(ee); } } private void LogDispatchBatchError(Exception ee) { LogHelper.Error("Error refreshing distributed list", ee); } private void LogDispatchBatchResult(int errorCount) { LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); } private void LogDispatchNodeError(Exception ex) { LogHelper.Error("Error refreshing a node in the distributed list", ex); } private void LogDispatchNodeError(WebException ex) { string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); } /// /// Sets the web service URL for a CacheRefresher from an XmlNode. /// /// The CacheRefresher. /// The XmlNode. private void SetWebServiceUrlFromNode(WebClientProtocol cr, XmlNode n) { string protocol = GlobalSettings.UseSSL ? "https" : "http"; if (n.Attributes.GetNamedItem("forceProtocol") != null && !String.IsNullOrEmpty(n.Attributes.GetNamedItem("forceProtocol").Value)) protocol = n.Attributes.GetNamedItem("forceProtocol").Value; string domain = XmlHelper.GetNodeValue(n); if (n.Attributes.GetNamedItem("forcePortnumber") != null && !String.IsNullOrEmpty(n.Attributes.GetNamedItem("forcePortnumber").Value)) domain += string.Format(":{0}", n.Attributes.GetNamedItem("forcePortnumber").Value); cr.Url = string.Format("{0}://{1}{2}/cacheRefresher.asmx", protocol, domain, _webServicesUrl); } private string GetFactoryObjectName(Guid uniqueIdentifier) { var cacheRefresher = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); return cacheRefresher != null ? cacheRefresher.Name : ""; } private void LogStartDispatch() { LogHelper.Info("Submitting calls to distributed servers"); } /// /// Gets the node list of DistributionServers from config. /// /// private XmlNodeList GetDistributedNodes() { return UmbracoSettings.DistributionServers.SelectNodes("./server"); } private IAsyncResult[] GetAsyncResults(List asyncResultsList, out List waitHandlesList) { IAsyncResult[] asyncResults = asyncResultsList.ToArray(); waitHandlesList = new List(); foreach (IAsyncResult asyncResult in asyncResults) { waitHandlesList.Add(asyncResult.AsyncWaitHandle); } return asyncResults; } #region Nested type: DispatchType private enum DispatchType { RefreshAll, RefreshByNumericId, RefreshByGuid, RemoveById } #endregion } }