security accessors should not be hybrid, ensures call context isn't flowed to child set/forget threads, renames TaskHelper and adds a test, removes GenericDictionaryRequestAppCache in favor of HttpContextRequestAppCache that relies on HttpContext and fixes http locks since there was a deadlock problem. Removes SafeCallContext, we just use ExecutionContext.SuppressFlow instead

This commit is contained in:
Shannon
2021-03-03 10:40:16 +11:00
parent dfc202a6c8
commit a70a739d62
24 changed files with 685 additions and 485 deletions

View File

@@ -1,30 +1,36 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Core
{
/// <summary>
/// Helper class to not repeat common patterns with Task.
/// Helper class to deal with Fire and Forget tasks correctly.
/// </summary>
public class TaskHelper
public class FireAndForgetTasks
{
private readonly ILogger<TaskHelper> _logger;
private readonly ILogger<FireAndForgetTasks> _logger;
public TaskHelper(ILogger<TaskHelper> logger)
{
_logger = logger;
}
public FireAndForgetTasks(ILogger<FireAndForgetTasks> logger) => _logger = logger;
/// <summary>
/// Runs a TPL Task fire-and-forget style, the right way - in the
/// background, separate from the current thread, with no risk
/// of it trying to rejoin the current thread.
/// </summary>
public void RunBackgroundTask(Func<Task> fn) => Task.Run(LoggingWrapper(fn)).ConfigureAwait(false);
public Task RunBackgroundTask(Func<Task> fn)
{
using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread
{
Task t = Task.Run(LoggingWrapper(fn));
t.ConfigureAwait(false);
return t;
}
}
/// <summary>
/// Runs a task fire-and-forget style and notifies the TPL that this
@@ -32,9 +38,15 @@ namespace Umbraco.Cms.Core
/// are multiple gaps in thread use that may be long.
/// Use for example when talking to a slow webservice.
/// </summary>
public void RunLongRunningBackgroundTask(Func<Task> fn) =>
Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning)
.ConfigureAwait(false);
public Task RunLongRunningBackgroundTask(Func<Task> fn)
{
using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread
{
Task t = Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning);
t.ConfigureAwait(false);
return t;
}
}
private Func<Task> LoggingWrapper(Func<Task> fn) =>
async () =>

View File

@@ -18,11 +18,8 @@ namespace Umbraco.Cms.Core
{
private readonly IRequestCache _requestCache;
// TODO: Do they need to be static?? These are singleton instances IMO they shouldn't be static
// ReSharper disable StaticMemberInGenericType
private static readonly object s_locker = new object();
private static bool s_registered;
// ReSharper restore StaticMemberInGenericType
private readonly object _locker = new object();
private bool _registered;
private string _itemKey;
@@ -53,37 +50,15 @@ namespace Umbraco.Cms.Core
{
_requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache));
lock (s_locker)
lock (_locker)
{
// register the itemKey once with SafeCallContext
if (s_registered)
if (_registered)
{
return;
}
s_registered = true;
_registered = true;
}
// ReSharper disable once VirtualMemberCallInConstructor
var itemKey = ItemKey; // virtual
SafeCallContext.Register(() =>
{
T value = CallContext<T>.GetData(itemKey);
return value;
}, o =>
{
if (o == null)
{
return;
}
if (!(o is T value))
{
throw new ArgumentException($"Expected type {typeof(T).FullName}, got {o.GetType().FullName}", nameof(o));
}
CallContext<T>.SetData(itemKey, value);
});
}
protected T Value

View File

@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Cms.Core
{
/// <summary>
/// Provides a way to stop the data flow of a logical call context (i.e. CallContext or AsyncLocal) from within
/// a SafeCallContext and then have the original data restored to the current logical call context.
/// </summary>
/// <remarks>
/// Some usages of this might be when spawning async thread or background threads in which the current logical call context will be flowed but
/// you don't want it to flow there yet you don't want to clear it either since you want the data to remain on the current thread.
/// </remarks>
public class SafeCallContext : IDisposable
{
private static readonly List<Func<object>> EnterFuncs = new List<Func<object>>();
private static readonly List<Action<object>> ExitActions = new List<Action<object>>();
private static int _count;
private readonly List<object> _objects;
private bool _disposed;
public static void Register(Func<object> enterFunc, Action<object> exitAction)
{
if (enterFunc == null) throw new ArgumentNullException(nameof(enterFunc));
if (exitAction == null) throw new ArgumentNullException(nameof(exitAction));
lock (EnterFuncs)
{
if (_count > 0) throw new InvalidOperationException("Cannot register while some SafeCallContext instances exist.");
EnterFuncs.Add(enterFunc);
ExitActions.Add(exitAction);
}
}
// tried to make the UmbracoDatabase serializable but then it leaks to weird places
// in ReSharper and so on, where Umbraco.Core is not available. Tried to serialize
// as an object instead but then it comes *back* deserialized into the original context
// as an object and of course it breaks everything. Cannot prevent this from flowing,
// and ExecutionContext.SuppressFlow() works for threads but not domains. and we'll
// have the same issue with anything that toys with logical call context...
//
// so this class lets anything that uses the logical call context register itself,
// providing two methods:
// - an enter func that removes and returns whatever is in the logical call context
// - an exit action that restores the value into the logical call context
// whenever a SafeCallContext instance is created, it uses these methods to capture
// and clear the logical call context, and restore it when disposed.
//
// in addition, a static Clear method is provided - which uses the enter funcs to
// remove everything from logical call context - not to be used when the app runs,
// but can be useful during tests
//
// note
// see System.Transactions
// pre 4.5.1, the TransactionScope would not flow in async, and then introduced
// an option to store in the LLC so that it flows
// they are using a conditional weak table to store the data, and what they store in
// LLC is the key - which is just an empty MarshalByRefObject that is created with
// the transaction scope - that way, they can "clear current data" provided that
// they have the key - but they need to hold onto a ref to the scope... not ok for us
public static void Clear()
{
lock (EnterFuncs)
{
foreach (var enter in EnterFuncs)
enter();
}
}
public SafeCallContext()
{
lock (EnterFuncs)
{
_count++;
_objects = EnterFuncs.Select(x => x()).ToList();
}
}
public void Dispose()
{
if (_disposed) throw new ObjectDisposedException("this");
_disposed = true;
lock (EnterFuncs)
{
for (var i = 0; i < ExitActions.Count; i++)
ExitActions[i](_objects[i]);
_count--;
}
}
// for unit tests ONLY
internal static void Reset()
{
lock (EnterFuncs)
{
if (_count > 0) throw new InvalidOperationException("Cannot reset while some SafeCallContext instances exist.");
EnterFuncs.Clear();
ExitActions.Clear();
}
}
}
}

View File

@@ -1,23 +0,0 @@
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Core.Security
{
public class HybridBackofficeSecurityAccessor : HybridAccessorBase<IBackOfficeSecurity>, IBackOfficeSecurityAccessor
{
/// <summary>
/// Initializes a new instance of the <see cref="HybridBackofficeSecurityAccessor"/> class.
/// </summary>
public HybridBackofficeSecurityAccessor(IRequestCache requestCache)
: base(requestCache)
{ }
/// <summary>
/// Gets or sets the <see cref="IBackOfficeSecurity"/> object.
/// </summary>
public IBackOfficeSecurity BackOfficeSecurity
{
get => Value;
set => Value = value;
}
}
}

View File

@@ -1,24 +0,0 @@
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Core.Security
{
public class HybridUmbracoWebsiteSecurityAccessor : HybridAccessorBase<IUmbracoWebsiteSecurity>, IUmbracoWebsiteSecurityAccessor
{
/// <summary>
/// Initializes a new instance of the <see cref="HybridUmbracoWebsiteSecurityAccessor"/> class.
/// </summary>
public HybridUmbracoWebsiteSecurityAccessor(IRequestCache requestCache)
: base(requestCache)
{ }
/// <summary>
/// Gets or sets the <see cref="IUmbracoWebsiteSecurity"/> object.
/// </summary>
public IUmbracoWebsiteSecurity WebsiteSecurity
{
get => Value;
set => Value = value;
}
}
}

View File

@@ -1,9 +1,10 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Examine;
using Examine.LuceneEngine;
using Examine.LuceneEngine.Providers;
@@ -35,7 +36,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
// context because they will fork a thread/task/whatever which should *not* capture our
// call context (and the database it can contain)! ideally we should be able to override
// SafelyProcessQueueItems but that's not possible in the current version of Examine.
// TODO: Make SafelyProcessQueueItems overrideable or make this easier
/// <summary>
/// Create a new <see cref="UmbracoExamineIndex"/>
@@ -92,7 +93,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
public IEnumerable<string> GetFields()
{
//we know this is a LuceneSearcher
var searcher = (LuceneSearcher) GetSearcher();
var searcher = (LuceneSearcher)GetSearcher();
return searcher.GetAllIndexedFields();
}
@@ -106,13 +107,30 @@ namespace Umbraco.Cms.Infrastructure.Examine
{
if (CanInitialize())
{
using (new SafeCallContext())
// Use SafeCallContext to prevent the current Execution Context (AsyncLocal) flow to child
// tasks executed in the base class so we don't leak Scopes.
// TODO: See notes at the top of this class
using (ExecutionContext.SuppressFlow())
{
base.PerformDeleteFromIndex(itemIds, onComplete);
}
}
}
protected override void PerformIndexItems(IEnumerable<ValueSet> values, Action<IndexOperationEventArgs> onComplete)
{
if (CanInitialize())
{
// Use SafeCallContext to prevent the current Execution Context (AsyncLocal) flow to child
// tasks executed in the base class so we don't leak Scopes.
// TODO: See notes at the top of this class
using (ExecutionContext.SuppressFlow())
{
base.PerformIndexItems(values, onComplete);
}
}
}
/// <summary>
/// Returns true if the Umbraco application is in a state that we can initialize the examine indexes
/// </summary>

View File

@@ -170,7 +170,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
// Services required to run background jobs (with out the handler)
builder.Services.AddUnique<IBackgroundTaskQueue, BackgroundTaskQueue>();
builder.Services.AddUnique<TaskHelper>();
builder.Services.AddUnique<FireAndForgetTasks>();
return builder;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
@@ -253,6 +253,8 @@ _hostingEnvironment = hostingEnvironment;
{
var updatedTempId = tempId + UpdatedSuffix;
using (ExecutionContext.SuppressFlow())
{
return Task.Run(() =>
{
try
@@ -285,6 +287,7 @@ _hostingEnvironment = hostingEnvironment;
}, _cancellationTokenSource.Token);
}
}
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
{

View File

@@ -71,7 +71,7 @@ namespace Umbraco.Cms.Core.Scoping
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8));
logger.LogDebug("create " + InstanceId.ToString("N").Substring(0, 8));
#endif
if (detachable)
@@ -333,7 +333,9 @@ namespace Umbraco.Cms.Core.Scoping
if (completed.HasValue == false || completed.Value == false)
{
if (LogUncompletedScopes)
_logger.LogDebug("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
{
_logger.LogWarning("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
}
_completed = false;
}
@@ -342,7 +344,9 @@ namespace Umbraco.Cms.Core.Scoping
private void EnsureNotDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
// TODO: safer?
//if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
@@ -356,15 +360,18 @@ namespace Umbraco.Cms.Core.Scoping
if (this != _scopeProvider.AmbientScope)
{
#if DEBUG_SCOPES
var ambient = _scopeProvider.AmbientScope;
_logger.Debug<Scope>("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
Scope ambient = _scopeProvider.AmbientScope;
_logger.LogDebug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
if (ambient == null)
{
throw new InvalidOperationException("Not the ambient scope (no ambient scope).");
var ambientInfos = _scopeProvider.GetScopeInfo(ambient);
var disposeInfos = _scopeProvider.GetScopeInfo(this);
}
ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient);
ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this);
throw new InvalidOperationException("Not the ambient scope (see ctor stack traces).\r\n"
+ "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n"
+ "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n");
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
#else
throw new InvalidOperationException("Not the ambient scope.");
#endif
@@ -500,13 +507,8 @@ namespace Umbraco.Cms.Core.Scoping
}
}
// backing field for LogUncompletedScopes
private static bool? _logUncompletedScopes;
// caching config
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
private bool LogUncompletedScopes => (_logUncompletedScopes
?? (_logUncompletedScopes = _coreDebugSettings.LogIncompletedScopes)).Value;
private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes;
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);

View File

@@ -7,8 +7,10 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Infrastructure.Persistence;
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
using Umbraco.Extensions;
#if DEBUG_SCOPES
using System.Collections.Generic;
using System.Linq;
using System.Text;
#endif
@@ -42,31 +44,6 @@ namespace Umbraco.Cms.Core.Scoping
_scopeReference = new ScopeReference(this);
}
static ScopeProvider()
{
SafeCallContext.Register(
() =>
{
var scope = GetCallContextObject<IScope>(ScopeItemKey);
var context = GetCallContextObject<IScopeContext>(ContextItemKey);
SetCallContextObject<IScope>(ScopeItemKey, null);
SetCallContextObject<IScopeContext>(ContextItemKey, null);
return Tuple.Create(scope, context);
},
o =>
{
// cannot re-attached over leaked scope/context
if (GetCallContextObject<IScope>(ScopeItemKey) != null)
throw new Exception("Found leaked scope when restoring call context.");
if (GetCallContextObject<IScopeContext>(ContextItemKey) != null)
throw new Exception("Found leaked context when restoring call context.");
var t = (Tuple<IScope, IScopeContext>) o;
SetCallContextObject<IScope>(ScopeItemKey, t.Item1);
SetCallContextObject<IScopeContext>(ContextItemKey, t.Item2);
});
}
public IUmbracoDatabaseFactory DatabaseFactory { get; }
public ISqlContext SqlContext => DatabaseFactory.SqlContext;
@@ -76,13 +53,17 @@ namespace Umbraco.Cms.Core.Scoping
private static T GetCallContextObject<T>(string key)
where T : class, IInstanceIdentifiable
{
var obj = CallContext<T>.GetData(key);
if (obj == default(T)) return null;
T obj = CallContext<T>.GetData(key);
if (obj == default(T))
{
return null;
}
return obj;
}
private static void SetCallContextObject<T>(string key, T value)
where T: class, IInstanceIdentifiable
private static void SetCallContextObject<T>(string key, T value, ILogger<ScopeProvider> logger)
where T : class, IInstanceIdentifiable
{
#if DEBUG_SCOPES
// manage the 'context' that contains the scope (null, "http" or "call")
@@ -90,25 +71,34 @@ namespace Umbraco.Cms.Core.Scoping
if (key == ScopeItemKey)
{
// first, null-register the existing value
var ambientScope = CallContext<IScope>.GetData(ScopeItemKey);
IScope ambientScope = CallContext<IScope>.GetData(ScopeItemKey);
if (ambientScope != null)
{
RegisterContext(ambientScope, logger, null);
}
if (ambientScope != null) RegisterContext(ambientScope, null);
// then register the new value
var scope = value as IScope;
if (scope != null) RegisterContext(scope, "call");
if (value is IScope scope)
{
RegisterContext(scope, logger, "call");
}
}
#endif
if (value == null)
{
var obj = CallContext<T>.GetData(key);
T obj = CallContext<T>.GetData(key);
CallContext<T>.SetData(key, default); // aka remove
if (obj == null) return;
if (obj == null)
{
return;
}
}
else
{
#if DEBUG_SCOPES
Current.Logger.Debug<ScopeProvider>("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
logger.LogDebug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
#endif
CallContext<T>.SetData(key, value);
@@ -121,7 +111,9 @@ namespace Umbraco.Cms.Core.Scoping
{
if (!_requestCache.IsAvailable && required)
{
throw new Exception("Request cache is unavailable.");
}
return (T)_requestCache.Get(key);
}
@@ -131,7 +123,10 @@ namespace Umbraco.Cms.Core.Scoping
if (!_requestCache.IsAvailable)
{
if (required)
{
throw new Exception("Request cache is unavailable.");
}
return false;
}
@@ -142,20 +137,31 @@ namespace Umbraco.Cms.Core.Scoping
{
// first, null-register the existing value
var ambientScope = (IScope)_requestCache.Get(ScopeItemKey);
if (ambientScope != null) RegisterContext(ambientScope, null);
if (ambientScope != null)
{
RegisterContext(ambientScope, _logger, null);
}
// then register the new value
var scope = value as IScope;
if (scope != null) RegisterContext(scope, "http");
if (value is IScope scope)
{
RegisterContext(scope, _logger, "http");
}
}
#endif
if (value == null)
{
_requestCache.Remove(key);
}
else
{
_requestCache.Set(key, value);
}
return true;
}
#endregion
#endregion
#region Ambient Context
@@ -166,19 +172,24 @@ namespace Umbraco.Cms.Core.Scoping
get
{
// try http context, fallback onto call context
var value = GetHttpContextObject<IScopeContext>(ContextItemKey, false);
IScopeContext value = GetHttpContextObject<IScopeContext>(ContextItemKey, false);
return value ?? GetCallContextObject<IScopeContext>(ContextItemKey);
}
set
{
// clear both
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject<IScopeContext>(ContextItemKey, null);
if (value == null) return;
SetCallContextObject<IScopeContext>(ContextItemKey, null, _logger);
if (value == null)
{
return;
}
// set http/call context
if (SetHttpContextObject(ContextItemKey, value, false) == false)
SetCallContextObject<IScopeContext>(ContextItemKey, value);
{
SetCallContextObject<IScopeContext>(ContextItemKey, value, _logger);
}
}
}
@@ -186,8 +197,8 @@ namespace Umbraco.Cms.Core.Scoping
#region Ambient Scope
internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope";
internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference";
internal static readonly string ScopeItemKey = typeof(Scope).FullName;
internal static readonly string ScopeRefItemKey = typeof(ScopeReference).FullName;
// only 1 instance which can be disposed and disposed again
private readonly ScopeReference _scopeReference;
@@ -206,14 +217,21 @@ namespace Umbraco.Cms.Core.Scoping
// clear both
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject<IScope>(ScopeItemKey, null);
if (value == null) return;
SetCallContextObject<IScope>(ScopeItemKey, null, _logger);
if (value == null)
{
return;
}
// set http/call context
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
{
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
}
else
SetCallContextObject<IScope>(ScopeItemKey, value);
{
SetCallContextObject<IScope>(ScopeItemKey, value, _logger);
}
}
}
@@ -224,13 +242,16 @@ namespace Umbraco.Cms.Core.Scoping
// clear all
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject<IScope>(ScopeItemKey, null);
SetCallContextObject<IScope>(ScopeItemKey, null, _logger);
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject<IScopeContext>(ContextItemKey, null);
SetCallContextObject<IScopeContext>(ContextItemKey, null, _logger);
if (scope == null)
{
if (context != null)
{
throw new ArgumentException("Must be null if scope is null.", nameof(context));
}
return;
}
@@ -241,8 +262,8 @@ namespace Umbraco.Cms.Core.Scoping
}
else
{
SetCallContextObject<IScope>(ScopeItemKey, scope);
SetCallContextObject<IScopeContext>(ContextItemKey, context);
SetCallContextObject<IScope>(ScopeItemKey, scope, _logger);
SetCallContextObject<IScopeContext>(ContextItemKey, context, _logger);
}
}
@@ -304,11 +325,11 @@ namespace Umbraco.Cms.Core.Scoping
bool callContext = false,
bool autoComplete = false)
{
var ambientScope = AmbientScope;
Scope ambientScope = AmbientScope;
if (ambientScope == null)
{
var ambientContext = AmbientContext;
var newContext = ambientContext == null ? new ScopeContext() : null;
IScopeContext ambientContext = AmbientContext;
ScopeContext newContext = ambientContext == null ? new ScopeContext() : null;
var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
// assign only if scope creation did not throw!
SetAmbient(scope, newContext ?? ambientContext);
@@ -356,70 +377,86 @@ namespace Umbraco.Cms.Core.Scoping
//}
// all scope instances that are currently being tracked
private static readonly object StaticScopeInfosLock = new object();
private static readonly Dictionary<IScope, ScopeInfo> StaticScopeInfos = new Dictionary<IScope, ScopeInfo>();
private static readonly object s_staticScopeInfosLock = new object();
private static readonly Dictionary<IScope, ScopeInfo> s_staticScopeInfos = new Dictionary<IScope, ScopeInfo>();
public IEnumerable<ScopeInfo> ScopeInfos
{
get
{
lock (StaticScopeInfosLock)
lock (s_staticScopeInfosLock)
{
return StaticScopeInfos.Values.ToArray(); // capture in an array
return s_staticScopeInfos.Values.ToArray(); // capture in an array
}
}
}
public ScopeInfo GetScopeInfo(IScope scope)
{
lock (StaticScopeInfosLock)
lock (s_staticScopeInfosLock)
{
ScopeInfo scopeInfo;
return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null;
return s_staticScopeInfos.TryGetValue(scope, out ScopeInfo scopeInfo) ? scopeInfo : null;
}
}
//private static void Log(string message, UmbracoDatabase database)
//{
// LogHelper.Debug<ScopeProvider>(message + " (" + (database == null ? "" : database.InstanceSid) + ").");
//}
// register a scope and capture its ctor stacktrace
public void RegisterScope(IScope scope)
{
lock (StaticScopeInfosLock)
lock (s_staticScopeInfosLock)
{
if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered.");
_logger.Debug<ScopeProvider>("Register " + scope.InstanceId.ToString("N").Substring(0, 8));
StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace);
if (s_staticScopeInfos.ContainsKey(scope))
{
throw new Exception("oops: already registered.");
}
_logger.LogDebug("Register " + scope.InstanceId.ToString("N").Substring(0, 8));
s_staticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace);
}
}
// register that a scope is in a 'context'
// 'context' that contains the scope (null, "http" or "call")
public static void RegisterContext(IScope scope, string context)
public static void RegisterContext(IScope scope, ILogger<ScopeProvider> logger, string context)
{
lock (StaticScopeInfosLock)
lock (s_staticScopeInfosLock)
{
ScopeInfo info;
if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null;
if (s_staticScopeInfos.TryGetValue(scope, out ScopeInfo info) == false)
{
info = null;
}
if (info == null)
{
if (context == null) return;
if (context == null)
{
return;
}
throw new Exception("oops: unregistered scope.");
}
var sb = new StringBuilder();
var s = scope;
IScope s = scope;
while (s != null)
{
if (sb.Length > 0) sb.Append(" < ");
if (sb.Length > 0)
{
sb.Append(" < ");
}
sb.Append(s.InstanceId.ToString("N").Substring(0, 8));
var ss = s as Scope;
s = ss?.ParentScope;
}
Current.Logger.Debug<ScopeProvider>("Register " + (context ?? "null") + " context " + sb);
if (context == null) info.NullStack = Environment.StackTrace;
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 16));
logger?.LogTrace("Register " + (context ?? "null") + " context " + sb);
if (context == null)
{
info.NullStack = Environment.StackTrace;
}
logger?.LogTrace("At:\r\n" + Head(Environment.StackTrace, 16));
info.Context = context;
}
}
@@ -433,20 +470,25 @@ namespace Umbraco.Cms.Core.Scoping
pos = s.IndexOf("\r\n", pos + 1, StringComparison.OrdinalIgnoreCase);
i++;
}
if (pos < 0) return s;
if (pos < 0)
{
return s;
}
return s.Substring(0, pos);
}
public void Disposed(IScope scope)
{
lock (StaticScopeInfosLock)
lock (s_staticScopeInfosLock)
{
if (StaticScopeInfos.ContainsKey(scope))
if (s_staticScopeInfos.ContainsKey(scope))
{
// enable this by default
//Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8));
StaticScopeInfos.Remove(scope);
_logger.Debug<ScopeProvider>("Remove " + scope.InstanceId.ToString("N").Substring(0, 8));
s_staticScopeInfos.Remove(scope);
_logger.LogDebug("Remove " + scope.InstanceId.ToString("N").Substring(0, 8));
// instead, enable this to keep *all* scopes
// beware, there can be a lot of scopes!
@@ -466,20 +508,38 @@ namespace Umbraco.Cms.Core.Scoping
Scope = scope;
Created = DateTime.Now;
CtorStack = ctorStack;
CreatedThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
}
public IScope Scope { get; } // the scope itself
// the scope's parent identifier
public Guid Parent => ((Scope) Scope).ParentScope == null ? Guid.Empty : ((Scope) Scope).ParentScope.InstanceId;
public Guid Parent => ((Scope)Scope).ParentScope == null ? Guid.Empty : ((Scope)Scope).ParentScope.InstanceId;
public int CreatedThreadId { get; } // the thread id that created this scope
public DateTime Created { get; } // the date time the scope was created
public bool Disposed { get; set; } // whether the scope has been disposed already
public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc")
public string CtorStack { get; } // the stacktrace of the scope ctor
public string DisposedStack { get; set; } // the stacktrace when disposed
//public string DisposedStack { get; set; } // the stacktrace when disposed
public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null
public override string ToString() => new StringBuilder()
.AppendLine("ScopeInfo:")
.Append("Instance Id: ")
.AppendLine(Scope.InstanceId.ToString())
.Append("Instance Id: ")
.AppendLine(Parent.ToString())
.Append("Created Thread Id: ")
.AppendLine(CreatedThreadId.ToInvariantString())
.Append("Created At: ")
.AppendLine(Created.ToString("O"))
.Append("Disposed: ")
.AppendLine(Disposed.ToString())
.Append("CTOR stack: ")
.AppendLine(CtorStack)
.ToString();
}
#endif
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Examine;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
@@ -27,7 +28,7 @@ namespace Umbraco.Cms.Infrastructure.Search
private readonly IValueSetBuilder<IMedia> _mediaValueSetBuilder;
private readonly IValueSetBuilder<IMember> _memberValueSetBuilder;
private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder;
private readonly TaskHelper _taskHelper;
private readonly FireAndForgetTasks _taskHelper;
private readonly IScopeProvider _scopeProvider;
private readonly ServiceContext _services;
private readonly IMainDom _mainDom;
@@ -41,16 +42,18 @@ namespace Umbraco.Cms.Infrastructure.Search
private const int EnlistPriority = 80;
public ExamineComponent(IMainDom mainDom,
IExamineManager examineManager, IProfilingLogger profilingLogger,
IExamineManager examineManager,
IProfilingLogger profilingLogger,
ILoggerFactory loggerFactory,
IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator,
IScopeProvider scopeProvider,
IUmbracoIndexesCreator indexCreator,
ServiceContext services,
IContentValueSetBuilder contentValueSetBuilder,
IPublishedContentValueSetBuilder publishedContentValueSetBuilder,
IValueSetBuilder<IMedia> mediaValueSetBuilder,
IValueSetBuilder<IMember> memberValueSetBuilder,
BackgroundIndexRebuilder backgroundIndexRebuilder,
TaskHelper taskHelper)
FireAndForgetTasks taskHelper)
{
_services = services;
_scopeProvider = scopeProvider;
@@ -88,8 +91,10 @@ namespace Umbraco.Cms.Infrastructure.Search
}
//create the indexes and register them with the manager
foreach(var index in _indexCreator.Create())
foreach (IIndex index in _indexCreator.Create())
{
_examineManager.AddIndex(index);
}
_logger.LogDebug("Examine shutdown registered with MainDom");
@@ -99,7 +104,9 @@ namespace Umbraco.Cms.Infrastructure.Search
// don't bind event handlers if we're not suppose to listen
if (registeredIndexers == 0)
{
return;
}
// bind to distributed cache events - this ensures that this logic occurs on ALL servers
// that are taking part in a load balanced environment.
@@ -129,10 +136,14 @@ namespace Umbraco.Cms.Infrastructure.Search
private void ContentCacheRefresherUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
}
if (args.MessageType != MessageType.RefreshByPayload)
{
throw new NotSupportedException();
}
var contentService = _services.ContentService;
@@ -167,10 +178,14 @@ namespace Umbraco.Cms.Infrastructure.Search
IContent published = null;
if (content.Published && contentService.IsPathPublished(content))
{
published = content;
}
if (published == null)
{
DeleteIndexForEntity(payload.Id, true);
}
// just that content
ReIndexForContent(content, published != null);
@@ -194,10 +209,14 @@ namespace Umbraco.Cms.Infrastructure.Search
if (masked != null) // else everything is masked
{
if (masked.Contains(descendant.ParentId) || !descendant.Published)
{
masked.Add(descendant.Id);
}
else
{
published = descendant;
}
}
ReIndexForContent(descendant, published != null);
}
@@ -221,7 +240,9 @@ namespace Umbraco.Cms.Infrastructure.Search
private void MemberCacheRefresherUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
}
switch (args.MessageType)
{
@@ -256,7 +277,7 @@ namespace Umbraco.Cms.Infrastructure.Search
case MessageType.RefreshByPayload:
var payload = (MemberCacheRefresher.JsonPayload[])args.MessageObject;
var members = payload.Select(x => _services.MemberService.GetById(x.Id));
foreach(var m in members)
foreach (var m in members)
{
ReIndexForMember(m);
}
@@ -272,10 +293,14 @@ namespace Umbraco.Cms.Infrastructure.Search
private void MediaCacheRefresherUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
}
if (args.MessageType != MessageType.RefreshByPayload)
{
throw new NotSupportedException();
}
var mediaService = _services.MediaService;
@@ -303,7 +328,9 @@ namespace Umbraco.Cms.Infrastructure.Search
}
if (media.Trashed)
{
DeleteIndexForEntity(payload.Id, true);
}
// just that media
ReIndexForMedia(media, !media.Trashed);
@@ -330,9 +357,14 @@ namespace Umbraco.Cms.Infrastructure.Search
private void LanguageCacheRefresherUpdated(LanguageCacheRefresher sender, CacheRefresherEventArgs e)
{
if (!(e.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads))
{
return;
}
if (payloads.Length == 0) return;
if (payloads.Length == 0)
{
return;
}
var removedOrCultureChanged = payloads.Any(x =>
x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture
@@ -354,10 +386,14 @@ namespace Umbraco.Cms.Infrastructure.Search
private void ContentTypeCacheRefresherUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs args)
{
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
}
if (args.MessageType != MessageType.RefreshByPayload)
{
throw new NotSupportedException();
}
var changedIds = new Dictionary<string, (List<int> removedIds, List<int> refreshedIds, List<int> otherIds)>();
@@ -370,12 +406,18 @@ namespace Umbraco.Cms.Infrastructure.Search
}
if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove))
{
idLists.removedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain))
{
idLists.refreshedIds.Add(payload.Id);
}
else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther))
{
idLists.otherIds.Add(payload.Id);
}
}
const int pageSize = 500;
@@ -413,9 +455,14 @@ namespace Umbraco.Cms.Infrastructure.Search
total = results.TotalItemCount;
var paged = results.Skip(page * pageSize);
foreach (var item in paged)
if (int.TryParse(item.Id, out var contentId))
foreach (ISearchResult item in paged)
{
if (int.TryParse(item.Id, out int contentId))
{
DeleteIndexForEntity(contentId, false);
}
}
page++;
}
}
@@ -427,18 +474,18 @@ namespace Umbraco.Cms.Infrastructure.Search
{
const int pageSize = 500;
var memberTypes = _services.MemberTypeService.GetAll(memberTypeIds);
foreach (var memberType in memberTypes)
IEnumerable<IMemberType> memberTypes = _services.MemberTypeService.GetAll(memberTypeIds);
foreach (IMemberType memberType in memberTypes)
{
var page = 0;
var total = long.MaxValue;
while (page * pageSize < total)
{
var memberToRefresh = _services.MemberService.GetAll(
IEnumerable<IMember> memberToRefresh = _services.MemberService.GetAll(
page++, pageSize, out total, "LoginName", Direction.Ascending,
memberType.Alias);
foreach (var c in memberToRefresh)
foreach (IMember c in memberToRefresh)
{
ReIndexForMember(c);
}
@@ -453,13 +500,13 @@ namespace Umbraco.Cms.Infrastructure.Search
var total = long.MaxValue;
while (page * pageSize < total)
{
var mediaToRefresh = _services.MediaService.GetPagedOfTypes(
IEnumerable<IMedia> mediaToRefresh = _services.MediaService.GetPagedOfTypes(
//Re-index all content of these types
mediaTypeIds,
page++, pageSize, out total, null,
Ordering.By("Path", Direction.Ascending));
foreach (var c in mediaToRefresh)
foreach (IMedia c in mediaToRefresh)
{
ReIndexForMedia(c, c.Trashed == false);
}
@@ -473,7 +520,7 @@ namespace Umbraco.Cms.Infrastructure.Search
var total = long.MaxValue;
while (page * pageSize < total)
{
var contentToRefresh = _services.ContentService.GetPagedOfTypes(
IEnumerable<IContent> contentToRefresh = _services.ContentService.GetPagedOfTypes(
//Re-index all content of these types
contentTypeIds,
page++, pageSize, out total, null,
@@ -483,7 +530,7 @@ namespace Umbraco.Cms.Infrastructure.Search
//track which Ids have their paths are published
var publishChecked = new Dictionary<int, bool>();
foreach (var c in contentToRefresh)
foreach (IContent c in contentToRefresh)
{
var isPublished = false;
if (c.Published)
@@ -508,28 +555,40 @@ namespace Umbraco.Cms.Infrastructure.Search
{
var actions = DeferedActions.Get(_scopeProvider);
if (actions != null)
{
actions.Add(new DeferedReIndexForContent(_taskHelper, this, sender, isPublished));
}
else
{
DeferedReIndexForContent.Execute(_taskHelper, this, sender, isPublished);
}
}
private void ReIndexForMember(IMember member)
{
var actions = DeferedActions.Get(_scopeProvider);
if (actions != null)
{
actions.Add(new DeferedReIndexForMember(_taskHelper, this, member));
}
else
{
DeferedReIndexForMember.Execute(_taskHelper, this, member);
}
}
private void ReIndexForMedia(IMedia sender, bool isPublished)
{
var actions = DeferedActions.Get(_scopeProvider);
if (actions != null)
{
actions.Add(new DeferedReIndexForMedia(_taskHelper, this, sender, isPublished));
}
else
{
DeferedReIndexForMedia.Execute(_taskHelper, this, sender, isPublished);
}
}
/// <summary>
/// Remove items from an index
@@ -543,10 +602,14 @@ namespace Umbraco.Cms.Infrastructure.Search
{
var actions = DeferedActions.Get(_scopeProvider);
if (actions != null)
{
actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished));
}
else
{
DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished);
}
}
#endregion
#region Deferred Actions
@@ -556,27 +619,29 @@ namespace Umbraco.Cms.Infrastructure.Search
public static DeferedActions Get(IScopeProvider scopeProvider)
{
var scopeContext = scopeProvider.Context;
IScopeContext scopeContext = scopeProvider.Context;
return scopeContext?.Enlist("examineEvents",
() => new DeferedActions(), // creator
(completed, actions) => // action
{
if (completed) actions.Execute();
if (completed)
{
actions.Execute();
}
}, EnlistPriority);
}
public void Add(DeferedAction action)
{
_actions.Add(action);
}
public void Add(DeferedAction action) => _actions.Add(action);
private void Execute()
{
foreach (var action in _actions)
foreach (DeferedAction action in _actions)
{
action.Execute();
}
}
}
/// <summary>
/// An action that will execute at the end of the Scope being completed
@@ -592,12 +657,12 @@ namespace Umbraco.Cms.Infrastructure.Search
/// </summary>
private class DeferedReIndexForContent : DeferedAction
{
private readonly TaskHelper _taskHelper;
private readonly FireAndForgetTasks _taskHelper;
private readonly ExamineComponent _examineComponent;
private readonly IContent _content;
private readonly bool _isPublished;
public DeferedReIndexForContent(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
public DeferedReIndexForContent(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
{
_taskHelper = taskHelper;
_examineComponent = examineComponent;
@@ -605,15 +670,13 @@ namespace Umbraco.Cms.Infrastructure.Search
_isPublished = isPublished;
}
public override void Execute()
{
Execute(_taskHelper, _examineComponent, _content, _isPublished);
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _content, _isPublished);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
{
taskHelper.RunBackgroundTask(async () =>
public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
=> taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
// for content we have a different builder for published vs unpublished
// we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published
var builders = new Dictionary<bool, Lazy<List<ValueSet>>>
@@ -622,17 +685,16 @@ namespace Umbraco.Cms.Infrastructure.Search
[false] = new Lazy<List<ValueSet>>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList())
};
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
{
var valueSet = builders[index.PublishedValuesOnly].Value;
List<ValueSet> valueSet = builders[index.PublishedValuesOnly].Value;
index.IndexItems(valueSet);
}
return Task.CompletedTask;
});
}
}
/// <summary>
@@ -640,12 +702,12 @@ namespace Umbraco.Cms.Infrastructure.Search
/// </summary>
private class DeferedReIndexForMedia : DeferedAction
{
private readonly TaskHelper _taskHelper;
private readonly FireAndForgetTasks _taskHelper;
private readonly ExamineComponent _examineComponent;
private readonly IMedia _media;
private readonly bool _isPublished;
public DeferedReIndexForMedia(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished)
public DeferedReIndexForMedia(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished)
{
_taskHelper = taskHelper;
_examineComponent = examineComponent;
@@ -653,28 +715,27 @@ namespace Umbraco.Cms.Infrastructure.Search
_isPublished = isPublished;
}
public override void Execute()
{
Execute(_taskHelper, _examineComponent, _media, _isPublished);
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _media, _isPublished);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished)
{
public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) =>
// perform the ValueSet lookup on a background thread
taskHelper.RunBackgroundTask(async () =>
taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
{
index.IndexItems(valueSet);
}
return Task.CompletedTask;
});
}
}
/// <summary>
/// Re-indexes an <see cref="IMember"/> item on a background thread
@@ -683,35 +744,34 @@ namespace Umbraco.Cms.Infrastructure.Search
{
private readonly ExamineComponent _examineComponent;
private readonly IMember _member;
private readonly TaskHelper _taskHelper;
private readonly FireAndForgetTasks _taskHelper;
public DeferedReIndexForMember(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member)
public DeferedReIndexForMember(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMember member)
{
_examineComponent = examineComponent;
_member = member;
_taskHelper = taskHelper;
}
public override void Execute()
{
Execute(_taskHelper, _examineComponent, _member);
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _member);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member)
{
public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMember member) =>
// perform the ValueSet lookup on a background thread
taskHelper.RunBackgroundTask(async () =>
taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => x.EnableDefaultEventHandler))
{
index.IndexItems(valueSet);
}
return Task.CompletedTask;
});
}
}
private class DeferedDeleteIndex : DeferedAction
{
@@ -726,10 +786,7 @@ namespace Umbraco.Cms.Infrastructure.Search
_keepIfUnpublished = keepIfUnpublished;
}
public override void Execute()
{
Execute(_examineComponent, _id, _keepIfUnpublished);
}
public override void Execute() => Execute(_examineComponent, _id, _keepIfUnpublished);
public static void Execute(ExamineComponent examineComponent, int id, bool keepIfUnpublished)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Text;
@@ -34,7 +34,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.IO
[SetUp]
public void SetUp()
{
SafeCallContext.Clear();
ClearFiles(HostingEnvironment);
FileSystems.ResetShadowId();
}
@@ -42,7 +41,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.IO
[TearDown]
public void TearDown()
{
SafeCallContext.Clear();
ClearFiles(HostingEnvironment);
FileSystems.ResetShadowId();
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.AspNetCore.Http;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache
{
[TestFixture]
public class HttpContextRequestAppCacheTests : AppCacheTests
{
private HttpContextRequestAppCache _appCache;
private IHttpContextAccessor _httpContextAccessor;
public override void Setup()
{
base.Setup();
var httpContext = new DefaultHttpContext();
_httpContextAccessor = Mock.Of<IHttpContextAccessor>(x => x.HttpContext == httpContext);
_appCache = new HttpContextRequestAppCache(_httpContextAccessor);
}
internal override IAppCache AppCache => _appCache;
protected override int GetTotalItemCount => _httpContextAccessor.HttpContext.Items.Count;
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture.NUnit3;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Tests.Common.TestHelpers;
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core
{
[TestFixture]
public class FireAndForgetTasksTests
{
[Test]
[AutoMoqData]
public void RunBackgroundTask__Suppress_Execution_Context(
[Frozen] ILogger<FireAndForgetTasks> logger,
FireAndForgetTasks sut)
{
var local = new AsyncLocal<string>
{
Value = "hello"
};
string taskResult = null;
Task t = sut.RunBackgroundTask(() =>
{
// FireAndForgetTasks ensure that flow is suppressed therefore this value will be null
taskResult = local.Value;
return Task.CompletedTask;
});
Task.WaitAll(t);
Assert.IsNull(taskResult);
}
[Test]
[AutoMoqData]
public void RunBackgroundTask__Must_Run_Func(
[Frozen] ILogger<FireAndForgetTasks> logger,
FireAndForgetTasks sut)
{
var i = 0;
Task t = sut.RunBackgroundTask(() =>
{
Interlocked.Increment(ref i);
return Task.CompletedTask;
});
Task.WaitAll(t);
Assert.AreEqual(1, i);
}
[Test]
[AutoMoqData]
public void RunBackgroundTask__Log_Error_When_Exception_Happen_In_Background_Task(
[Frozen] ILogger<FireAndForgetTasks> logger,
Exception exception,
FireAndForgetTasks sut)
{
Task t = sut.RunBackgroundTask(() => throw exception);
Task.WaitAll(t);
Mock.Get(logger).VerifyLogError(exception, "Exception thrown in a background thread", Times.Once());
}
}
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture.NUnit3;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Tests.Common.TestHelpers;
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core
{
[TestFixture]
public class TaskHelperTests
{
[Test]
[AutoMoqData]
public void RunBackgroundTask__must_run_func([Frozen] ILogger<TaskHelper> logger, TaskHelper sut)
{
var i = 0;
sut.RunBackgroundTask(() =>
{
Interlocked.Increment(ref i);
return Task.CompletedTask;
});
Thread.Sleep(5); // Wait for background task to execute
Assert.AreEqual(1, i);
}
[Test]
[AutoMoqData]
public void RunBackgroundTask__Log_error_when_exception_happen_in_background_task([Frozen] ILogger<TaskHelper> logger, Exception exception, TaskHelper sut)
{
sut.RunBackgroundTask(() => throw exception);
Thread.Sleep(5); // Wait for background task to execute
Mock.Get(logger).VerifyLogError(exception, "Exception thrown in a background thread", Times.Once());
}
}
}

View File

@@ -1,7 +1,10 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
#if DEBUG_SCOPES
using System.Collections.Generic;
#endif
using System.Data;
using Microsoft.Extensions.Logging;
using Moq;

View File

@@ -34,6 +34,7 @@ using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
using Umbraco.Cms.Web.BackOffice.Controllers;
using Umbraco.Cms.Web.BackOffice.Mapping;
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Security;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
using MemberMapDefinition = Umbraco.Cms.Web.BackOffice.Mapping.MemberMapDefinition;
@@ -330,6 +331,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
IDataTypeService dataTypeService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
var httpContextAccessor = new HttpContextAccessor();
var mockShortStringHelper = new MockShortStringHelper();
var textService = new Mock<ILocalizedTextService>();
@@ -337,7 +340,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny<IContentBase>())).Returns(new ContentType(mockShortStringHelper, 123));
var contentAppFactories = new Mock<List<IContentAppFactory>>();
var mockContentAppFactoryCollection = new Mock<ILogger<ContentAppFactoryCollection>>();
var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache());
var hybridBackOfficeSecurityAccessor = new BackOfficeSecurityAccessor(httpContextAccessor);
var contentAppFactoryCollection = new ContentAppFactoryCollection(
contentAppFactories.Object,
mockContentAppFactoryCollection.Object,
@@ -372,7 +375,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
mockPasswordConfig.Object,
contentTypeBaseServiceProvider.Object,
propertyEditorCollection),
new HttpContextAccessor());
httpContextAccessor);
var map = new MapDefinitionCollection(new List<IMapDefinition>()
{
@@ -396,7 +399,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
return new MemberController(
new DefaultCultureDictionary(
new Mock<ILocalizationService>().Object,
new HttpRequestAppCache(() => null)),
NoAppCache.Instance),
new LoggerFactory(),
mockShortStringHelper,
new DefaultEventMessagesFactory(

View File

@@ -327,8 +327,10 @@ namespace Umbraco.Web.Scheduling
// create a new token source since this is a new process
_shutdownTokenSource = new CancellationTokenSource();
_shutdownToken = _shutdownTokenSource.Token;
using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread
{
_runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken);
}
_logger.LogDebug("{LogPrefix} Starting", _logPrefix);
}
@@ -350,7 +352,9 @@ namespace Umbraco.Web.Scheduling
var hasTasks = TaskCount > 0;
if (!force && hasTasks)
{
_logger.LogInformation("{LogPrefix} Waiting for tasks to complete", _logPrefix);
}
// complete the queue
// will stop waiting on the queue or on a latch
@@ -552,18 +556,23 @@ namespace Umbraco.Web.Scheduling
try
{
if (bgTask.IsAsync)
{
// configure await = false since we don't care about the context, we're on a background thread.
await bgTask.RunAsync(token).ConfigureAwait(false);
}
else
{
bgTask.Run();
}
}
finally // ensure we disposed - unless latched again ie wants to re-run
{
var lbgTask = bgTask as ILatchedBackgroundTask;
if (lbgTask == null || lbgTask.IsLatched == false)
if (!(bgTask is ILatchedBackgroundTask lbgTask) || lbgTask.IsLatched == false)
{
bgTask.Dispose();
}
}
}
catch (Exception e)
{
OnTaskError(new TaskEventArgs<T>(bgTask, e));

View File

@@ -1,36 +1,41 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
/// <summary>
/// Implements a fast <see cref="IAppCache"/> on top of HttpContext.Items.
/// Implements a <see cref="IAppCache"/> on top of <see cref="IHttpContextAccessor"/>
/// </summary>
/// <remarks>
/// <para>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.</para>
/// <para>The HttpContext is not thread safe and no part of it is which means we need to include our own thread
/// safety mechanisms. This relies on notifications: <see cref="UmbracoRequestBegin"/> and <see cref="UmbracoRequestEnd"/>
/// in order to facilitate the correct locking and releasing allocations.
/// </para>
/// </remarks>
public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache
{
//private static readonly string s_contextItemsLockKey = $"{typeof(HttpContextRequestAppCache).FullName}::LockEntered";
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="HttpRequestAppCache"/> class with a context, for unit tests!
/// </summary>
public GenericDictionaryRequestAppCache(Func<IDictionary<object, object>> requestItems) : base()
{
ContextItems = requestItems;
}
private Func<IDictionary<object, object>> ContextItems { get; }
public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
public bool IsAvailable => TryGetContextItems(out _);
private bool TryGetContextItems(out IDictionary<object, object> items)
{
items = ContextItems?.Invoke();
items = _httpContextAccessor.HttpContext?.Items;
return items != null;
}
@@ -38,7 +43,10 @@ namespace Umbraco.Cms.Core.Cache
public override object Get(string key, Func<object> factory)
{
//no place to cache so just return the callback result
if (!TryGetContextItems(out var items)) return factory();
if (!TryGetContextItems(out var items))
{
return factory();
}
key = GetCacheKey(key);
@@ -140,33 +148,67 @@ namespace Umbraco.Cms.Core.Cache
#region Lock
private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered";
protected override void EnterReadLock()
{
if (!TryGetContextItems(out _))
{
return;
}
protected override void EnterReadLock() => EnterWriteLock();
ReaderWriterLockSlim locker = GetLock();
locker.EnterReadLock();
}
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, ref entered);
items[ContextItemsLockKey] = entered;
if (!TryGetContextItems(out _))
{
return;
}
protected override void ExitReadLock() => ExitWriteLock();
ReaderWriterLockSlim locker = GetLock();
locker.EnterWriteLock();
//// 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, ref entered);
//items[s_contextItemsLockKey] = entered;
}
protected override void ExitReadLock()
{
if (!TryGetContextItems(out _))
{
return;
}
ReaderWriterLockSlim locker = GetLock();
if (locker.IsReadLockHeld)
{
locker.ExitReadLock();
}
}
protected override void ExitWriteLock()
{
if (!TryGetContextItems(out var items)) return;
if (!TryGetContextItems(out _))
{
return;
}
var entered = (bool?)items[ContextItemsLockKey] ?? false;
if (entered)
Monitor.Exit(items);
items.Remove(ContextItemsLockKey);
ReaderWriterLockSlim locker = GetLock();
if (locker.IsWriteLockHeld)
{
locker.ExitWriteLock();
}
//var entered = (bool?)items[s_contextItemsLockKey] ?? false;
//if (entered)
// Monitor.Exit(items);
//items.Remove(s_contextItemsLockKey);
}
#endregion
@@ -185,5 +227,40 @@ namespace Umbraco.Cms.Core.Cache
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Ensures and returns the current lock
/// </summary>
/// <returns></returns>
private ReaderWriterLockSlim GetLock() => _httpContextAccessor.GetRequiredHttpContext().RequestServices.GetRequiredService<RequestLock>().Locker;
/// <summary>
/// Used as Scoped instance to allow locking within a request
/// </summary>
internal class RequestLock : IDisposable
{
private bool _disposedValue;
public ReaderWriterLockSlim Locker { get; } = new ReaderWriterLockSlim();
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
Locker.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
}
}
}
}

View File

@@ -54,6 +54,7 @@ using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Web.Common.Templates;
using Umbraco.Cms.Web.Common.UmbracoContext;
using Umbraco.Core.Security;
using static Umbraco.Cms.Core.Cache.HttpContextRequestAppCache;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Extensions
@@ -86,15 +87,18 @@ namespace Umbraco.Extensions
IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config);
var loggingDir = tempHostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles);
var loggingDir = tempHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles);
var loggingConfig = new LoggingConfiguration(loggingDir);
services.AddLogger(tempHostingEnvironment, loggingConfig, config);
// TODO: This doesn't seem right? The HttpContextAccessor is normally added to the container
// with ASP.NET Core's own ext methods. Is there a chance we can end up with a different
// accessor registered and resolved?
IHttpContextAccessor httpContextAccessor = new HttpContextAccessor();
services.AddSingleton(httpContextAccessor);
var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext?.Items);
var requestCache = new HttpContextRequestAppCache(httpContextAccessor);
var appCaches = AppCaches.Create(requestCache);
services.AddUnique(appCaches);
@@ -263,9 +267,9 @@ namespace Umbraco.Extensions
builder.Services.AddUnique<IUmbracoContextFactory, UmbracoContextFactory>();
builder.Services.AddUnique<IBackOfficeSecurityFactory, BackOfficeSecurityFactory>();
builder.Services.AddUnique<IBackOfficeSecurityAccessor, HybridBackofficeSecurityAccessor>();
builder.Services.AddUnique<IBackOfficeSecurityAccessor, BackOfficeSecurityAccessor>();
builder.AddNotificationHandler<UmbracoRoutedRequest, UmbracoWebsiteSecurityFactory>();
builder.Services.AddUnique<IUmbracoWebsiteSecurityAccessor, HybridUmbracoWebsiteSecurityAccessor>();
builder.Services.AddUnique<IUmbracoWebsiteSecurityAccessor, UmbracoWebsiteSecurityAccessor>();
var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList();
builder.WithCollectionBuilder<UmbracoApiControllerTypeCollectionBuilder>()
@@ -285,6 +289,7 @@ namespace Umbraco.Extensions
builder.Services.AddSingleton<ContentModelBinder>();
builder.Services.AddScoped<UmbracoHelper>();
builder.Services.AddScoped<RequestLock>();
builder.AddHttpClients();

View File

@@ -777,6 +777,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder
{
_watcher.EnableRaisingEvents = false;
_watcher.Dispose();
_locker.Dispose();
_hostingLifetime.UnregisterObject(this);
}

View File

@@ -69,9 +69,10 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder
/// This is used so that when new PureLive models are built, the entire razor stack is re-constructed so all razor
/// caches and assembly references, etc... are cleared.
/// </remarks>
internal class RefreshingRazorViewEngine : IRazorViewEngine
internal class RefreshingRazorViewEngine : IRazorViewEngine, IDisposable
{
private IRazorViewEngine _current;
private bool _disposedValue;
private readonly PureLiveModelFactory _pureLiveModelFactory;
private readonly Func<IRazorViewEngine> _defaultRazorViewEngineFactory;
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
@@ -172,5 +173,24 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder
_locker.ExitReadLock();
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_locker.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
}
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
public class BackOfficeSecurityAccessor : IBackOfficeSecurityAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeSecurityAccessor"/> class.
/// </summary>
public BackOfficeSecurityAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
/// <summary>
/// Gets or sets the <see cref="IBackOfficeSecurity"/> object.
/// </summary>
public IBackOfficeSecurity BackOfficeSecurity
{
get => _httpContextAccessor.HttpContext?.Features.Get<IBackOfficeSecurity>();
set => _httpContextAccessor.HttpContext?.Features.Set(value);
}
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
public class UmbracoWebsiteSecurityAccessor : IUmbracoWebsiteSecurityAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoWebsiteSecurityAccessor"/> class.
/// </summary>
public UmbracoWebsiteSecurityAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
/// <summary>
/// Gets or sets the <see cref="IUmbracoWebsiteSecurity"/> object.
/// </summary>
public IUmbracoWebsiteSecurity WebsiteSecurity
{
get => _httpContextAccessor.HttpContext?.Features.Get<IUmbracoWebsiteSecurity>();
set => _httpContextAccessor.HttpContext?.Features.Set(value);
}
}
}