From aa11d3504de5a37ca908afc686b1e5799e8d88c3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Nov 2014 18:06:10 +1100 Subject: [PATCH] Makes the tree scanning and loading from the xml lazy so this no longer happens on startup but instead when trees are required, this ensures during startup there is no IO or scanning for trees, this should assist with U4-5126 config files get cleared out with zero size and of course also help a bunch with startup time. --- .../Services/ApplicationTreeService.cs | 242 +++++++++++++----- .../Services/IApplicationTreeService.cs | 12 +- .../Trees/ApplicationTreeRegistrar.cs | 95 ++++--- 3 files changed, 245 insertions(+), 104 deletions(-) diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index dbf5c9b85a..ca6bb122d1 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Cache; using File = System.IO.File; namespace Umbraco.Core.Services @@ -15,6 +16,9 @@ namespace Umbraco.Core.Services internal class ApplicationTreeService : IApplicationTreeService { private readonly CacheHelper _cache; + private IEnumerable _allAvailableTrees; + private volatile bool _isInitialized = false; + private readonly object _locker = new object(); public ApplicationTreeService(CacheHelper cache) { @@ -45,74 +49,101 @@ namespace Umbraco.Core.Services } /// - /// The cache storage for all application trees + /// The main entry point to get application trees /// + /// + /// This lazily on first access will scan for plugin trees and ensure the trees.config is up-to-date with the plugins. If plugins + /// haven't changed on disk then the file will not be saved. The trees are all then loaded from this config file into cache and returned. + /// private List GetAppTrees() { - return _cache.GetCacheItem( + return _cache.RuntimeCache.GetCacheItem>( CacheKeys.ApplicationTreeCacheKey, () => + { + var list = ReadFromXmlAndSort(); + + //On first access we need to do some initialization + if (_isInitialized == false) { - var list = new List(); - - LoadXml(doc => + lock (Locker) + { + if (_isInitialized == false) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + //now we can check the non-volatile flag + if (_allAvailableTrees != null) { - var applicationAlias = (string) addElement.Attribute("application"); - var type = (string) addElement.Attribute("type"); - var assembly = (string) addElement.Attribute("assembly"); + var hasChanges = false; - var clrType = Type.GetType(type); - if (clrType == null) + LoadXml(doc => { - LogHelper.Warn("The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); - continue; - } + //Now, load in the xml structure and update it with anything that is not declared there and save the file. - //check if the tree definition (applicationAlias + type + assembly) is already in the list + //NOTE: On the first iteration here, it will lazily scan all trees, etc... this is because this ienumerable is lazy + // based on the ApplicationTreeRegistrar - and as noted there this is not an ideal way to do things but were stuck like this + // currently because of the legacy assemblies and types not in the Core. - if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) + //Get all the trees not registered in the config + var unregistered = _allAvailableTrees + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + if (hasChanges == false) return false; + + //add the unregistered ones to the list and re-save the file if any changes were found + var count = 0; + foreach (var tree in unregistered) + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("title", tree.Title), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + count++; + } + + //don't save if there's no changes + return count > 0; + }, true); + + if (hasChanges) { - list.Add(new ApplicationTree( - addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte) 0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - addElement.Attribute("type").Value)); + //If there were changes, we need to re-read the structures from the XML + list = ReadFromXmlAndSort(); } } - }, false); - return list; - }); + _isInitialized = true; + } + } + } + + + return list; + + + }); } - public void Intitialize(IEnumerable existingTrees) + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + public void Intitialize(IEnumerable allAvailableTrees) { - LoadXml(doc => - { - foreach (var tree in existingTrees) - { - doc.Root.Add(new XElement("add", - new XAttribute("initialize", tree.Initialize), - new XAttribute("sortOrder", tree.SortOrder), - new XAttribute("alias", tree.Alias), - new XAttribute("application", tree.ApplicationAlias), - new XAttribute("title", tree.Title), - new XAttribute("iconClosed", tree.IconClosed), - new XAttribute("iconOpen", tree.IconOpened), - new XAttribute("type", tree.Type))); - } - - }, true); + _allAvailableTrees = allAvailableTrees; } /// @@ -134,16 +165,19 @@ namespace Umbraco.Core.Services if (el == null) { - doc.Root.Add(new XElement("add", - new XAttribute("initialize", initialize), - new XAttribute("sortOrder", sortOrder), - new XAttribute("alias", alias), - new XAttribute("application", applicationAlias), - new XAttribute("title", title), - new XAttribute("iconClosed", iconClosed), - new XAttribute("iconOpen", iconOpened), - new XAttribute("type", type))); + doc.Root.Add(new XElement("add", + new XAttribute("initialize", initialize), + new XAttribute("sortOrder", sortOrder), + new XAttribute("alias", alias), + new XAttribute("application", applicationAlias), + new XAttribute("title", title), + new XAttribute("iconClosed", iconClosed), + new XAttribute("iconOpen", iconOpened), + new XAttribute("type", type))); } + + return true; + }, true); OnNew(new ApplicationTree(initialize, sortOrder, applicationAlias, alias, title, iconClosed, iconOpened, type), new EventArgs()); @@ -161,7 +195,7 @@ namespace Umbraco.Core.Services if (el != null) { el.RemoveAttributes(); - + el.Add(new XAttribute("initialize", tree.Initialize)); el.Add(new XAttribute("sortOrder", tree.SortOrder)); el.Add(new XAttribute("alias", tree.Alias)); @@ -172,6 +206,8 @@ namespace Umbraco.Core.Services el.Add(new XAttribute("type", tree.Type)); } + return true; + }, true); OnUpdated(tree, new EventArgs()); @@ -181,11 +217,16 @@ namespace Umbraco.Core.Services /// Deletes this instance. /// public void DeleteTree(ApplicationTree tree) - { + { LoadXml(doc => { - doc.Root.Elements("add").Where(x => x.Attribute("application") != null && x.Attribute("application").Value == tree.ApplicationAlias && - x.Attribute("alias") != null && x.Attribute("alias").Value == tree.Alias).Remove(); + doc.Root.Elements("add") + .Where(x => x.Attribute("application") != null + && x.Attribute("application").Value == tree.ApplicationAlias + && x.Attribute("alias") != null && x.Attribute("alias").Value == tree.Alias).Remove(); + + return true; + }, true); OnDeleted(tree, new EventArgs()); @@ -231,31 +272,44 @@ namespace Umbraco.Core.Services { var list = GetAppTrees().FindAll( t => - { - if (onlyInitialized) - return (t.ApplicationAlias == applicationAlias && t.Initialize); - return (t.ApplicationAlias == applicationAlias); - } + { + if (onlyInitialized) + return (t.ApplicationAlias == applicationAlias && t.Initialize); + return (t.ApplicationAlias == applicationAlias); + } ); return list.OrderBy(x => x.SortOrder).ToArray(); } - internal void LoadXml(Action callback, bool saveAfterCallback) + /// + /// Loads in the xml structure from disk if one is found, otherwise loads in an empty xml structure, calls the + /// callback with the xml document and saves the structure back to disk if saveAfterCallback is true. + /// + /// + /// + internal void LoadXml(Func callback, bool saveAfterCallbackIfChanges) { lock (Locker) { var doc = File.Exists(TreeConfigFilePath) ? XDocument.Load(TreeConfigFilePath) : XDocument.Parse(""); + if (doc.Root != null) { - callback.Invoke(doc); + var hasChanges = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanges && hasChanges + //Don't save it if it is empty, in some very rare cases if the app domain get's killed in the middle of this process + // in some insane way the file saved will be empty. I'm pretty sure it's not actually anything to do with the xml doc and + // more about the IO trying to save the XML doc, but it doesn't hurt to check. + && doc.Root != null && doc.Root.Elements().Any()) { + //ensures the folder exists Directory.CreateDirectory(Path.GetDirectoryName(TreeConfigFilePath)); + //saves it doc.Save(TreeConfigFilePath); //remove the cache now that it has changed SD: I'm leaving this here even though it @@ -266,6 +320,54 @@ namespace Umbraco.Core.Services } } + private List ReadFromXmlAndSort() + { + var list = new List(); + + //read in the xml file containing trees and convert them all to ApplicationTree instances + LoadXml(doc => + { + foreach (var addElement in doc.Root.Elements("add").OrderBy(x => + { + var sortOrderAttr = x.Attribute("sortOrder"); + return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; + })) + { + var applicationAlias = (string)addElement.Attribute("application"); + var type = (string)addElement.Attribute("type"); + var assembly = (string)addElement.Attribute("assembly"); + + var clrType = Type.GetType(type); + if (clrType == null) + { + LogHelper.Warn("The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); + continue; + } + + //check if the tree definition (applicationAlias + type + assembly) is already in the list + + if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) + { + list.Add(new ApplicationTree( + addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), + addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, + addElement.Attribute("application").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("title").Value, + addElement.Attribute("iconClosed").Value, + addElement.Attribute("iconOpen").Value, + addElement.Attribute("type").Value)); + } + } + + return false; + + }, false); + + return list; + } + + internal static event TypedEventHandler Deleted; private static void OnDeleted(ApplicationTree app, EventArgs args) { diff --git a/src/Umbraco.Core/Services/IApplicationTreeService.cs b/src/Umbraco.Core/Services/IApplicationTreeService.cs index c7aecec6fb..6a4d2bda33 100644 --- a/src/Umbraco.Core/Services/IApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/IApplicationTreeService.cs @@ -5,7 +5,17 @@ namespace Umbraco.Core.Services { public interface IApplicationTreeService { - void Intitialize(IEnumerable existingTrees); + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + void Intitialize(IEnumerable allAvailableTrees); /// /// Creates a new application tree. diff --git a/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs b/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs index 8dff05ab1c..d9043e8e15 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs @@ -1,64 +1,93 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Umbraco.Core; using Umbraco.Core.Models; using umbraco.businesslogic; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { - //TODO: Is there any way to get this to execute lazily when needed? - // i.e. When the back office loads so that this doesn't execute on startup for a content request. - /// /// A startup handler for putting the tree config in the config file based on attributes found /// + /// + /// TODO: This is really not a very ideal process but the code is found here because tree plugins are in the Web project or the legacy business logic project. + /// Moving forward we can put the base tree plugin classes in the core and then this can all just be taken care of normally within the service. + /// public sealed class ApplicationTreeRegistrar : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - ScanTrees(applicationContext); + //Call initialize on the tree service with the lazy enumerable class below, when the tree service needs to resolve, + // it will lazily do the scanning and comparing so it's not actually done on app start. + applicationContext.Services.ApplicationTreeService.Intitialize(new LazyEnumerableTrees()); } /// - /// Scans for all attributed trees and ensures they exist in the tree xml + /// This class is here so that we can provide lazy access to tree scanning for when it is needed /// - private static void ScanTrees(ApplicationContext applicationContext) + private class LazyEnumerableTrees : IEnumerable { - var added = new List(); + public LazyEnumerableTrees() + { + _lazyTrees = new Lazy>(() => + { + var added = new List(); - // Load all Controller Trees by attribute and add them to the XML config - // we also need to make sure that any alias added with the new trees is not also added - // with the legacy trees. - var types = PluginManager.Current.ResolveAttributedTreeControllers(); + // Load all Controller Trees by attribute + var types = PluginManager.Current.ResolveAttributedTreeControllers(); + //convert them to ApplicationTree instances + var items = types + .Select(x => + new Tuple(x, x.GetCustomAttributes(false).Single())) + .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) + .ToArray(); - //get all non-legacy application tree's - var items = types - .Select(x => - new Tuple(x, x.GetCustomAttributes(false).Single())) - .Where(x => applicationContext.Services.ApplicationTreeService.GetByAlias(x.Item2.Alias) == null) - .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) - .ToArray(); - - added.AddRange(items.Select(x => x.Alias)); + added.AddRange(items.Select(x => x.Alias)); - //find the legacy trees - var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); + //find the legacy trees + var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var legacyItems = legacyTreeTypes + .Select(x => + new Tuple( + x, + x.GetCustomAttributes(false).SingleOrDefault())) + .Where(x => x.Item2 != null) + //make sure the legacy tree isn't added on top of the controller tree! + .Where(x => added.InvariantContains(x.Item2.Alias) == false) + .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())); - var legacyItems = legacyTreeTypes - .Select(x => - new Tuple( - x, - x.GetCustomAttributes(false).SingleOrDefault())) - .Where(x => x.Item2 != null) - .Where(x => applicationContext.Services.ApplicationTreeService.GetByAlias(x.Item2.Alias) == null - //make sure the legacy tree isn't added on top of the controller tree! - && added.InvariantContains(x.Item2.Alias) == false) - .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())); + return items.Concat(legacyItems).ToArray(); + }); + } - applicationContext.Services.ApplicationTreeService.Intitialize(items.Concat(legacyItems)); + private readonly Lazy> _lazyTrees; + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _lazyTrees.Value.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } } }