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:
@@ -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 () =>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,12 +53,16 @@ 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)
|
||||
private static void SetCallContextObject<T>(string key, T value, ILogger<ScopeProvider> logger)
|
||||
where T : class, IInstanceIdentifiable
|
||||
{
|
||||
#if DEBUG_SCOPES
|
||||
@@ -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,16 +137,27 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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,6 +508,7 @@ 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
|
||||
@@ -473,13 +516,30 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// the scope's parent identifier
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -777,6 +777,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Dispose();
|
||||
_locker.Dispose();
|
||||
_hostingLifetime.UnregisterObject(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user