using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Web; namespace Umbraco.Core.Cache { /// /// Implements a fast on top of HttpContext.Items. /// /// /// If no current HttpContext items can be found (no current HttpContext, /// or no Items...) then this cache acts as a pass-through and does not cache /// anything. /// public class HttpRequestAppCache : FastDictionaryAppCacheBase { /// /// Initializes a new instance of the class with a context, for unit tests! /// public HttpRequestAppCache(Func requestItems) { ContextItems = requestItems; } private Func ContextItems { get; } private bool TryGetContextItems(out IDictionary items) { items = ContextItems?.Invoke(); return items != null; } /// public override object Get(string key, Func factory) { //no place to cache so just return the callback result if (!TryGetContextItems(out var items)) return factory(); key = GetCacheKey(key); Lazy result; try { EnterWriteLock(); result = items[key] as Lazy; // null if key not found // cannot create value within the lock, so if result.IsValueCreated is false, just // do nothing here - means that if creation throws, a race condition could cause // more than one thread to reach the return statement below and throw - accepted. if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null { result = SafeLazy.GetSafeLazy(factory); items[key] = result; } } finally { ExitWriteLock(); } // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache // exceptions (but try again and again) and silently eat them - however at // some point we have to report them - so need to re-throw here // this does not throw anymore //return result.Value; var value = result.Value; // will not throw (safe lazy) if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once! return value; } #region Entries protected override IEnumerable GetDictionaryEntries() { const string prefix = CacheItemPrefix + "-"; if (!TryGetContextItems(out var items)) return Enumerable.Empty(); return items.Cast() .Where(x => x.Key is string s && s.StartsWith(prefix)); } protected override void RemoveEntry(string key) { if (!TryGetContextItems(out var items)) return; items.Remove(key); } protected override object GetEntry(string key) { return !TryGetContextItems(out var items) ? null : items[key]; } #endregion #region Lock private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered"; protected override void EnterReadLock() => EnterWriteLock(); protected override void EnterWriteLock() { if (!TryGetContextItems(out var items)) return; // note: cannot keep 'entered' as a class variable here, // since there is one per request - so storing it within // ContextItems - which is locked, so this should be safe var entered = false; Monitor.Enter(items.SyncRoot, ref entered); items[ContextItemsLockKey] = entered; } protected override void ExitReadLock() => ExitWriteLock(); protected override void ExitWriteLock() { if (!TryGetContextItems(out var items)) return; var entered = (bool?)items[ContextItemsLockKey] ?? false; if (entered) Monitor.Exit(items.SyncRoot); items.Remove(ContextItemsLockKey); } #endregion } }