Makes the section scanning and loading from the xml lazy so this no longer happens on startup but instead when sections are required, this ensures during startup there is no IO or scanning for sections, this should assist with U4-5126 config files get cleared out with zero size and of course also help a bunch with startup time.

This commit is contained in:
Shannon
2014-11-12 18:32:51 +11:00
parent aa11d3504d
commit 10bc7d0c1e
5 changed files with 179 additions and 57 deletions

View File

@@ -18,16 +18,16 @@ namespace Umbraco.Core.Services
private readonly CacheHelper _cache;
private IEnumerable<ApplicationTree> _allAvailableTrees;
private volatile bool _isInitialized = false;
private readonly object _locker = new object();
internal const string TreeConfigFileName = "trees.config";
private static string _treeConfig;
private static readonly object Locker = new object();
public ApplicationTreeService(CacheHelper cache)
{
_cache = cache;
}
internal const string TreeConfigFileName = "trees.config";
private static string _treeConfig;
private static readonly object Locker = new object();
/// <summary>
/// gets/sets the trees.config file path

View File

@@ -6,10 +6,15 @@ namespace Umbraco.Core.Services
public interface ISectionService
{
/// <summary>
/// Ensures all available sections exist in the storage medium
/// Initializes the service with all available application plugins
/// </summary>
/// <param name="existingSections"></param>
void Initialize(IEnumerable<Section> existingSections);
/// <param name="allAvailableSections">
/// All application plugins found in assemblies
/// </param>
/// <remarks>
/// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file
/// </remarks>
void Initialize(IEnumerable<Section> allAvailableSections);
/// <summary>
/// The cache storage for all applications

View File

@@ -17,12 +17,19 @@ namespace Umbraco.Core.Services
internal class SectionService : ISectionService
{
private readonly IUserService _userService;
private IEnumerable<Section> _allAvailableSections;
private readonly IApplicationTreeService _applicationTreeService;
private readonly IDatabaseUnitOfWorkProvider _uowProvider;
private readonly CacheHelper _cache;
internal const string AppConfigFileName = "applications.config";
private static string _appConfig;
private volatile bool _isInitialized = false;
private static readonly object Locker = new object();
public SectionService(
IUserService userService,
IApplicationTreeService applicationTreeService,
IApplicationTreeService applicationTreeService,
IDatabaseUnitOfWorkProvider uowProvider,
CacheHelper cache)
{
if (applicationTreeService == null) throw new ArgumentNullException("applicationTreeService");
@@ -30,12 +37,11 @@ namespace Umbraco.Core.Services
_userService = userService;
_applicationTreeService = applicationTreeService;
_uowProvider = uowProvider;
_cache = cache;
}
internal const string AppConfigFileName = "applications.config";
private static string _appConfig;
private static readonly object Locker = new object();
/// <summary>
/// gets/sets the application.config file path
@@ -57,22 +63,17 @@ namespace Umbraco.Core.Services
}
/// <summary>
/// Ensures all available sections exist in the storage medium
/// Initializes the service with all available application plugins
/// </summary>
/// <param name="existingSections"></param>
public void Initialize(IEnumerable<Section> existingSections)
/// <param name="allAvailableSections">
/// All application plugins found in assemblies
/// </param>
/// <remarks>
/// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file
/// </remarks>
public void Initialize(IEnumerable<Section> allAvailableSections)
{
LoadXml(doc =>
{
foreach (var attr in existingSections)
{
doc.Root.Add(new XElement("add",
new XAttribute("alias", attr.Alias),
new XAttribute("name", attr.Name),
new XAttribute("icon", attr.Icon),
new XAttribute("sortOrder", attr.SortOrder)));
}
}, true);
_allAvailableSections = allAvailableSections;
}
/// <summary>
@@ -80,7 +81,7 @@ namespace Umbraco.Core.Services
/// </summary>
public IEnumerable<Section> GetSections()
{
return _cache.GetCacheItem(
return _cache.RuntimeCache.GetCacheItem<IEnumerable<Section>>(
CacheKeys.ApplicationsCacheKey,
() =>
{
@@ -88,28 +89,66 @@ namespace Umbraco.Core.Services
//if (_testApps != null)
// return _testApps;
var tmp = new List<Section>();
var list = ReadFromXmlAndSort();
LoadXml(doc =>
//On first access we need to do some initialization
if (_isInitialized == false)
{
lock (Locker)
{
foreach (var addElement in doc.Root.Elements("add").OrderBy(x =>
{
var sortOrderAttr = x.Attribute("sortOrder");
return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0;
}))
if (_isInitialized == false)
{
var sortOrderAttr = addElement.Attribute("sortOrder");
tmp.Add(new Section(addElement.Attribute("name").Value,
addElement.Attribute("alias").Value,
addElement.Attribute("icon").Value,
sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0));
//now we can check the non-volatile flag
if (_allAvailableSections != null)
{
var hasChanges = false;
LoadXml(doc =>
{
//Now, load in the xml structure and update it with anything that is not declared there and save the file.
//NOTE: On the first iteration here, it will lazily scan all apps, etc... this is because this ienumerable is lazy
// based on the ApplicationRegistrar - 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.
//Get all the trees not registered in the config
var unregistered = _allAvailableSections
.Where(x => list.Any(l => l.Alias == x.Alias) == false)
.ToArray();
hasChanges = unregistered.Any();
var count = 0;
foreach (var attr in unregistered)
{
doc.Root.Add(new XElement("add",
new XAttribute("alias", attr.Alias),
new XAttribute("name", attr.Name),
new XAttribute("icon", attr.Icon),
new XAttribute("sortOrder", attr.SortOrder)));
count++;
}
//don't save if there's no changes
return count > 0;
}, true);
if (hasChanges)
{
//If there were changes, we need to re-read the structures from the XML
list = ReadFromXmlAndSort();
}
}
}
}, false);
return tmp;
}
}
return list;
});
}
internal void LoadXml(Action<XDocument> callback, bool saveAfterCallback)
internal void LoadXml(Func<XDocument, bool> callback, bool saveAfterCallbackIfChanged)
{
lock (Locker)
{
@@ -119,9 +158,9 @@ namespace Umbraco.Core.Services
if (doc.Root != null)
{
callback.Invoke(doc);
var changed = callback.Invoke(doc);
if (saveAfterCallback)
if (saveAfterCallbackIfChanged && changed)
{
//ensure the folder is created!
Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath));
@@ -194,6 +233,7 @@ namespace Umbraco.Core.Services
new XAttribute("name", name),
new XAttribute("icon", icon),
new XAttribute("sortOrder", sortOrder)));
return true;
}, true);
//raise event
@@ -209,7 +249,7 @@ namespace Umbraco.Core.Services
lock (Locker)
{
//delete the assigned applications
ApplicationContext.Current.DatabaseContext.Database.Execute(
_uowProvider.GetUnitOfWork().Database.Execute(
"delete from umbracoUser2App where app = @appAlias",
new { appAlias = section.Alias });
@@ -222,7 +262,10 @@ namespace Umbraco.Core.Services
LoadXml(doc =>
{
doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias).Remove();
doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias)
.Remove();
return true;
}, true);
//raise event
@@ -230,6 +273,30 @@ namespace Umbraco.Core.Services
}
}
private List<Section> ReadFromXmlAndSort()
{
var tmp = new List<Section>();
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 sortOrderAttr = addElement.Attribute("sortOrder");
tmp.Add(new Section(addElement.Attribute("name").Value,
addElement.Attribute("alias").Value,
addElement.Attribute("icon").Value,
sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0));
}
return false;
}, false);
return tmp;
}
internal static event TypedEventHandler<Section, EventArgs> Deleted;
private static void OnDeleted(Section app, EventArgs args)
{

View File

@@ -162,7 +162,7 @@ namespace Umbraco.Core.Services
_treeService = new Lazy<IApplicationTreeService>(() => new ApplicationTreeService(cache));
if (_sectionService == null)
_sectionService = new Lazy<ISectionService>(() => new SectionService(_userService.Value, _treeService.Value, cache));
_sectionService = new Lazy<ISectionService>(() => new SectionService(_userService.Value, _treeService.Value, provider, cache));
if (_macroService == null)
_macroService = new Lazy<IMacroService>(() => new MacroService(provider, repositoryFactory.Value));

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;
@@ -16,23 +18,26 @@ using umbraco.interfaces;
namespace umbraco.BusinessLogic
{
/// <summary>
/// A startup handler for putting the app config in the config file based on attributes found
/// </summary>
/// /// <remarks>
/// 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.
/// </remarks>
public class ApplicationRegistrar : ApplicationEventHandler, IMapperConfiguration
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
// Load all Applications by attribute and add them to the XML config
var types = PluginManager.Current.ResolveApplications();
//since applications don't populate their metadata from the attribute and because it is an interface,
//we need to interrogate the attributes for the data. Would be better to have a base class that contains
//metadata populated by the attribute. Oh well i guess.
var attrs = types.Select(x => x.GetCustomAttributes<ApplicationAttribute>(false).Single())
.Where(x => applicationContext.Services.SectionService.GetByAlias(x.Alias) == null)
.ToArray();
applicationContext.Services.SectionService.Initialize(attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder)));
//initialize the section service with a lazy collection of found app plugins
applicationContext.Services.SectionService.Initialize(new LazyEnumerableSections());
}
/// <summary>
/// Configure a few auto-mappings
/// </summary>
/// <param name="config"></param>
/// <param name="applicationContext"></param>
public void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext)
{
config.CreateMap<Section, Application>()
@@ -46,5 +51,50 @@ namespace umbraco.BusinessLogic
.ForMember(x => x.Name, expression => expression.MapFrom(x => x.name))
.ForMember(x => x.SortOrder, expression => expression.MapFrom(x => x.sortOrder));
}
/// <summary>
/// This class is here so that we can provide lazy access to tree scanning for when it is needed
/// </summary>
private class LazyEnumerableSections : IEnumerable<Section>
{
public LazyEnumerableSections()
{
_lazySections = new Lazy<IEnumerable<Section>>(() =>
{
// Load all Applications by attribute and add them to the XML config
var types = PluginManager.Current.ResolveApplications();
//since applications don't populate their metadata from the attribute and because it is an interface,
//we need to interrogate the attributes for the data. Would be better to have a base class that contains
//metadata populated by the attribute. Oh well i guess.
var attrs = types.Select(x => x.GetCustomAttributes<ApplicationAttribute>(false).Single());
return attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder)).ToArray();
});
}
private readonly Lazy<IEnumerable<Section>> _lazySections;
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
/// </returns>
public IEnumerator<Section> GetEnumerator()
{
return _lazySections.Value.GetEnumerator();
}
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
}