diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 78b9943976..ef58671e91 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Web; using System.Web.Compilation; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -16,7 +17,7 @@ using File = System.IO.File; namespace Umbraco.Core.Composing { /// - /// Provides methods to find and instanciate types. + /// Provides methods to find and instantiate types. /// /// /// This class should be used to get all types, the class should never be used directly. @@ -32,9 +33,12 @@ namespace Umbraco.Core.Composing private readonly IGlobalSettings _globalSettings; private readonly ProfilingLogger _logger; + private readonly Dictionary _types = new Dictionary(); private readonly object _typesLock = new object(); - private readonly Dictionary _types = new Dictionary(); + private readonly object _timerLock = new object(); + private Timer _timer; + private bool _timing; private string _cachedAssembliesHash; private string _currentAssembliesHash; private IEnumerable _assemblies; @@ -78,8 +82,7 @@ namespace Umbraco.Core.Composing // rescanning of all types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); WriteCacheTypesHash(); } @@ -90,8 +93,7 @@ namespace Umbraco.Core.Composing // rescanning of all types including lazy ones. // http://issues.umbraco.org/issue/U4-4789 var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); // always set to true if we're not detecting (generally only for testing) RequiresRescanning = true; @@ -128,7 +130,8 @@ namespace Umbraco.Core.Composing // internal for tests internal void AddTypeList(TypeList typeList) { - _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; + var tobject = typeof(object); // CompositeTypeTypeKey does not support null values + _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; } #region Hashing @@ -287,11 +290,14 @@ namespace Umbraco.Core.Composing private const int ListFileOpenReadTimeout = 4000; // milliseconds private const int ListFileOpenWriteTimeout = 2000; // milliseconds + private const int ListFileWriteThrottle = 500; // milliseconds - throttle before writing + private const int ListFileCacheDuration = 2 * 60; // seconds - duration we cache the entire list + private const int FileDeleteTimeout = 4000; // milliseconds // internal for tests internal Attempt> TryGetCached(Type baseType, Type attributeType) { - var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromSeconds(ListFileCacheDuration)); cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out IEnumerable types); return types == null @@ -310,7 +316,7 @@ namespace Umbraco.Core.Composing try { var typesListFilePath = GetTypesListFilePath(); - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); } catch { @@ -415,6 +421,7 @@ namespace Umbraco.Core.Composing // internal for tests internal void WriteCache() { + _logger.Logger.Debug("Writing cache file."); var typesListFilePath = GetTypesListFilePath(); using (var stream = GetFileStream(typesListFilePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) using (var writer = new StreamWriter(stream)) @@ -433,11 +440,27 @@ namespace Umbraco.Core.Composing // internal for tests internal void UpdateCache() { - // note - // at the moment we write the cache to disk every time we update it. ideally we defer the writing - // since all the updates are going to happen in a row when Umbraco starts. that being said, the - // file is small enough, so it is not a priority. - WriteCache(); + void TimerRelease(object o) + { + lock (_timerLock) + { + try + { + WriteCache(); + } + catch { /* bah - just don't die */ } + if (!_timing) _timer = null; + } + } + + lock (_timerLock) + { + if (_timer == null) + _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite); + else + _timer.Change(ListFileWriteThrottle, Timeout.Infinite); + _timing = true; + } } /// @@ -447,12 +470,10 @@ namespace Umbraco.Core.Composing public void ClearTypesCache() { var typesListFilePath = GetTypesListFilePath(); - if (File.Exists(typesListFilePath)) - File.Delete(typesListFilePath); + DeleteFile(typesListFilePath, FileDeleteTimeout); var typesHashFilePath = GetTypesHashFilePath(); - if (File.Exists(typesHashFilePath)) - File.Delete(typesHashFilePath); + DeleteFile(typesHashFilePath, FileDeleteTimeout); _runtimeCache.ClearCacheItem(CacheKey); } @@ -478,6 +499,27 @@ namespace Umbraco.Core.Composing } } + private void DeleteFile(string path, int timeoutMilliseconds) + { + const int pauseMilliseconds = 250; + var attempts = timeoutMilliseconds / pauseMilliseconds; + while (File.Exists(path)) + { + try + { + File.Delete(path); + } + catch + { + if (--attempts == 0) + throw; + + _logger.Logger.Debug("Attempted to delete file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); + Thread.Sleep(pauseMilliseconds); + } + } + } + #endregion #region Get Types @@ -495,30 +537,37 @@ namespace Umbraco.Core.Composing // do not cache anything from specific assemblies cache &= specificAssemblies == null; - // if not caching, or not IDiscoverable, directly get types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} (slow).", typeof(T).FullName); + // warn + _logger.Logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} (slow).", typeof(T).FullName); return GetTypesInternal( - typeof (T), null, + typeof(T), null, () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", cache); } - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - + // get IDiscoverable and always cache var discovered = GetTypesInternal( typeof (IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", true); + // warn + if (!cache) + _logger.Logger.Debug("Running a non-cached, filter for discoverable type {TypeName} (slowish).", typeof(T).FullName); + + // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( typeof (T), null, () => discovered .Where(x => typeof (T).IsAssignableFrom(x)), - true); + "filtering IDiscoverable", + cache); } /// @@ -536,31 +585,37 @@ namespace Umbraco.Core.Composing // do not cache anything from specific assemblies cache &= specificAssemblies == null; - // if not caching, or not IDiscoverable, directly get types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + // if not IDiscoverable, directly get types + if (!typeof(IDiscoverable).IsAssignableFrom(typeof(T))) { - _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); + _logger.Logger.Debug("Running a full, " + (cache ? "" : "non-") + "cached, scan for non-discoverable type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); return GetTypesInternal( - typeof (T), typeof (TAttribute), + typeof(T), typeof(TAttribute), () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", cache); } - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - + // get IDiscoverable and always cache var discovered = GetTypesInternal( typeof (IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), + "scanning assemblies", true); + // warn + if (!cache) + _logger.Logger.Debug("Running a non-cached, filter for discoverable type {TypeName} / attribute {AttributeName} (slowish).", typeof(T).FullName, typeof(TAttribute).FullName); + + // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( typeof (T), typeof (TAttribute), () => discovered .Where(x => typeof(T).IsAssignableFrom(x)) .Where(x => x.GetCustomAttributes(false).Any()), - true); + "filtering IDiscoverable", + cache); } /// @@ -577,20 +632,20 @@ namespace Umbraco.Core.Composing // do not cache anything from specific assemblies cache &= specificAssemblies == null; - if (cache == false) - { + if (!cache) _logger.Logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); - } return GetTypesInternal( typeof (object), typeof (TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + "scanning assemblies", cache); } private IEnumerable GetTypesInternal( Type baseType, Type attributeType, Func> finder, + string action, bool cache) { // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable @@ -606,7 +661,7 @@ namespace Umbraco.Core.Composing "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find { // get within a lock & timer - return GetTypesInternalLocked(baseType, attributeType, finder, cache); + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); } } @@ -620,10 +675,12 @@ namespace Umbraco.Core.Composing private IEnumerable GetTypesInternalLocked( Type baseType, Type attributeType, Func> finder, + string action, bool cache) { // check if the TypeList already exists, if so return it, if not we'll create it - var listKey = new TypeListKey(baseType, attributeType); + var tobject = typeof(object); // CompositeTypeTypeKey does not support null values + var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); TypeList typeList = null; if (cache) _types.TryGetValue(listKey, out typeList); // else null @@ -698,7 +755,7 @@ namespace Umbraco.Core.Composing if (scan) { // either we had to scan, or we could not get the types from the cache file - scan now - _logger.Logger.Debug("Getting {TypeName}: scanning assemblies.", GetName(baseType, attributeType)); + _logger.Logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); foreach (var t in finder()) typeList.Add(t); @@ -730,41 +787,6 @@ namespace Umbraco.Core.Composing #region Nested classes and stuff - /// - /// Groups a type and a resolution kind into a key. - /// - private struct TypeListKey - { - // ReSharper disable MemberCanBePrivate.Local - public readonly Type BaseType; - public readonly Type AttributeType; - // ReSharper restore MemberCanBePrivate.Local - - public TypeListKey(Type baseType, Type attributeType) - { - BaseType = baseType ?? typeof (object); - AttributeType = attributeType; - } - - public override bool Equals(object obj) - { - if (obj == null || obj is TypeListKey == false) return false; - var o = (TypeListKey)obj; - return BaseType == o.BaseType && AttributeType == o.AttributeType; - } - - public override int GetHashCode() - { - // in case AttributeType is null we need something else, using typeof (TypeListKey) - // which does not really "mean" anything, it's just a value... - - var hash = 5381; - hash = ((hash << 5) + hash) ^ BaseType.GetHashCode(); - hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode(); - return hash; - } - } - /// /// Represents a list of types obtained by looking for types inheriting/implementing a /// specified type, and/or marked with a specified attribute type.