Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/AB4227-move-files-to-infrastructure

This commit is contained in:
Shannon
2019-12-23 17:06:31 +11:00
8 changed files with 89 additions and 147 deletions
@@ -4,6 +4,14 @@ using System.Collections.Generic;
namespace Umbraco.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>>();
@@ -5,37 +5,35 @@ using System.Threading;
namespace Umbraco.Core.Scoping
{
/// <summary>
/// Provides a way to set contextual data that flows with the call and
/// async context of a test or invocation.
/// Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.
/// </summary>
public static class CallContext
/// <remarks>
/// This is just a simple wrapper around <seealso cref="AsyncLocal{T}"/>
/// </remarks>
public static class CallContext<T>
{
private static readonly ConcurrentDictionary<string, Guid?> _state = new ConcurrentDictionary<string, Guid?>();
private static ConcurrentDictionary<string, AsyncLocal<T>> _state = new ConcurrentDictionary<string, AsyncLocal<T>>();
/// <summary>
/// Stores a given object and associates it with the specified name.
/// </summary>
/// <param name="name">The name with which to associate the new item in the call context.</param>
/// <param name="data">The object to store in the call context.</param>
public static void SetData(string name, Guid? data)
{
_state[name + Thread.CurrentThread.ManagedThreadId] = data;
}
public static void SetData(string name, T data) => _state.GetOrAdd(name, _ => new AsyncLocal<T>()).Value = data;
/// <summary>
/// Retrieves an object with the specified name from the <see cref="CallContext"/>.
/// Retrieves an object with the specified name from the <see cref="CallContext{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the data being retrieved. Must match the type used when the <paramref name="name"/> was set via <see cref="SetData{T}(string, T)"/>.</typeparam>
/// <param name="name">The name of the item in the call context.</param>
/// <returns>The object in the call context associated with the specified name, or <see langword="null"/> if not found.</returns>
public static Guid? GetData(string name)
{
return _state.TryGetValue(name + Thread.CurrentThread.ManagedThreadId, out var data) ? data : null;
}
/// <returns>The object in the call context associated with the specified name, or a default value for <typeparamref name="T"/> if none is found.</returns>
public static T GetData(string name) => _state.TryGetValue(name, out var data) ? data.Value : default;
// NOTE: If you have used the old CallContext in the past you might be thinking you need to clean this up but that is not the case.
// With CallContext you had to call FreeNamedDataSlot to prevent leaks but with AsyncLocal this is not the case, there is no way to clean this up.
// The above dictionary is sort of a trick because sure, there is always going to be a string key that will exist in the collection but the values
// themselves are managed per ExecutionContext so they don't build up.
// There's an SO article relating to this here https://stackoverflow.com/questions/36511243/safety-of-asynclocal-in-asp-net-core
public static bool RemoveData(string name)
{
return _state.TryRemove(name+ Thread.CurrentThread.ManagedThreadId, out _);
}
}
}
@@ -88,7 +88,7 @@ namespace Umbraco.Core.Scoping
ISqlContext SqlContext { get; }
#if DEBUG_SCOPES
Dictionary<Guid, object> CallContextObjects { get; }
IEnumerable<ScopeInfo> ScopeInfos { get; }
ScopeInfo GetScopeInfo(IScope scope);
#endif
@@ -49,23 +49,23 @@ namespace Umbraco.Core.Scoping
SafeCallContext.Register(
() =>
{
var scope = GetCallContextObject<Scope>(ScopeItemKey);
var context = GetCallContextObject<ScopeContext>(ContextItemKey);
SetCallContextObject(ScopeItemKey, null);
SetCallContextObject(ContextItemKey, null);
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<Scope>(ScopeItemKey) != null)
if (GetCallContextObject<IScope>(ScopeItemKey) != null)
throw new Exception("Found leaked scope when restoring call context.");
if (GetCallContextObject<ScopeContext>(ContextItemKey) != null)
if (GetCallContextObject<IScopeContext>(ContextItemKey) != null)
throw new Exception("Found leaked context when restoring call context.");
var t = (Tuple<Scope, ScopeContext>) o;
SetCallContextObject(ScopeItemKey, t.Item1);
SetCallContextObject(ContextItemKey, t.Item2);
var t = (Tuple<IScope, IScopeContext>) o;
SetCallContextObject<IScope>(ScopeItemKey, t.Item1);
SetCallContextObject<IScopeContext>(ContextItemKey, t.Item2);
});
}
@@ -75,65 +75,16 @@ namespace Umbraco.Core.Scoping
#region Context
// objects that go into the logical call context better be serializable else they'll eventually
// cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running
// tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx),
// but we don't want to make all of our objects serializable since they are *not* meant to be
// used in cross-AppDomain scenario anyways.
// in addition, whatever goes into the logical call context is serialized back and forth any
// time cross-AppDomain code executes, so if we put an "object" there, we'll can *another*
// "object" instance - and so we cannot use a random object as a key.
// so what we do is: we register a guid in the call context, and we keep a table mapping those
// guids to the actual objects. the guid serializes back and forth without causing any issue,
// and we can retrieve the actual objects from the table.
// only issue: how are we supposed to clear the table? we can't, really. objects should take
// care of de-registering themselves from context.
private static readonly object StaticCallContextObjectsLock = new object();
private static readonly Dictionary<Guid, object> StaticCallContextObjects
= new Dictionary<Guid, object>();
#if DEBUG_SCOPES
public Dictionary<Guid, object> CallContextObjects
{
get
{
lock (StaticCallContextObjectsLock)
{
// capture in a dictionary
return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value);
}
}
}
#endif
private static T GetCallContextObject<T>(string key)
where T : class
where T : class, IInstanceIdentifiable
{
var objectKey = CallContext.GetData(key);
if (objectKey is null) return null;
lock (StaticCallContextObjectsLock)
{
if (StaticCallContextObjects.TryGetValue(objectKey.Value, out object callContextObject))
{
#if DEBUG_SCOPES
Current.Logger.Debug<ScopeProvider>("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8));
//_logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
return (T)callContextObject;
}
// hard to inject into a static method :(
Current.Logger.Warn<ScopeProvider>("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.Value.ToString("N").Substring(0, 8));
#if DEBUG_SCOPES
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
return null;
}
var obj = CallContext<T>.GetData(key);
if (obj == default(T)) return null;
return obj;
}
private static void SetCallContextObject(string key, IInstanceIdentifiable value)
private static void SetCallContextObject<T>(string key, T value)
where T: class, IInstanceIdentifiable
{
#if DEBUG_SCOPES
// manage the 'context' that contains the scope (null, "http" or "call")
@@ -141,14 +92,8 @@ namespace Umbraco.Core.Scoping
if (key == ScopeItemKey)
{
// first, null-register the existing value
var ambientKey = CallContext.GetData(ScopeItemKey).AsGuid();
object o = null;
lock (StaticCallContextObjectsLock)
{
if (ambientKey != default(Guid))
StaticCallContextObjects.TryGetValue(ambientKey, out o);
}
var ambientScope = o as IScope;
var ambientScope = CallContext<IScope>.GetData(ScopeItemKey);
if (ambientScope != null) RegisterContext(ambientScope, null);
// then register the new value
var scope = value as IScope;
@@ -157,33 +102,18 @@ namespace Umbraco.Core.Scoping
#endif
if (value == null)
{
var objectKey = CallContext.GetData(key);
CallContext.RemoveData(key);
if (objectKey is null) return;
lock (StaticCallContextObjectsLock)
{
#if DEBUG_SCOPES
Current.Logger.Debug<ScopeProvider>("Remove Object " + objectKey.ToString("N").Substring(0, 8));
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
#endif
StaticCallContextObjects.Remove(objectKey.Value);
}
var obj = CallContext<T>.GetData(key);
CallContext<T>.SetData(key, default); // aka remove
if (obj == null) return;
}
else
{
// note - we are *not* detecting an already-existing value
// because our code in this class *always* sets to null before
// setting to a real value
var objectKey = value.InstanceId;
lock (StaticCallContextObjectsLock)
{
#if DEBUG_SCOPES
Current.Logger.Debug<ScopeProvider>("AddObject " + objectKey.ToString("N").Substring(0, 8));
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
Current.Logger.Debug<ScopeProvider>("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
#endif
StaticCallContextObjects.Add(objectKey, value);
}
CallContext.SetData(key, objectKey);
CallContext<T>.SetData(key, value);
}
}
@@ -245,12 +175,12 @@ namespace Umbraco.Core.Scoping
{
// clear both
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null);
SetCallContextObject<IScopeContext>(ContextItemKey, null);
if (value == null) return;
// set http/call context
if (SetHttpContextObject(ContextItemKey, value, false) == false)
SetCallContextObject(ContextItemKey, value);
SetCallContextObject<IScopeContext>(ContextItemKey, value);
}
}
@@ -270,21 +200,22 @@ namespace Umbraco.Core.Scoping
public Scope AmbientScope
{
// try http context, fallback onto call context
get => GetHttpContextObject<Scope>(ScopeItemKey, false)
?? GetCallContextObject<Scope>(ScopeItemKey);
// we are casting here because we know its a concrete type
get => (Scope)GetHttpContextObject<IScope>(ScopeItemKey, false)
?? (Scope)GetCallContextObject<IScope>(ScopeItemKey);
set
{
// clear both
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null);
SetCallContextObject<IScope>(ScopeItemKey, null);
if (value == null) return;
// set http/call context
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
else
SetCallContextObject(ScopeItemKey, value);
SetCallContextObject<IScope>(ScopeItemKey, value);
}
}
@@ -295,9 +226,9 @@ namespace Umbraco.Core.Scoping
// clear all
SetHttpContextObject(ScopeItemKey, null, false);
SetHttpContextObject(ScopeRefItemKey, null, false);
SetCallContextObject(ScopeItemKey, null);
SetCallContextObject<IScope>(ScopeItemKey, null);
SetHttpContextObject(ContextItemKey, null, false);
SetCallContextObject(ContextItemKey, null);
SetCallContextObject<IScopeContext>(ContextItemKey, null);
if (scope == null)
{
if (context != null)
@@ -312,8 +243,8 @@ namespace Umbraco.Core.Scoping
}
else
{
SetCallContextObject(ScopeItemKey, scope);
SetCallContextObject(ContextItemKey, context);
SetCallContextObject<IScope>(ScopeItemKey, scope);
SetCallContextObject<IScopeContext>(ContextItemKey, context);
}
}
@@ -1,6 +1,7 @@
using System.Runtime.Remoting.Messaging;
using NUnit.Framework;
using NUnit.Framework;
using System;
using Umbraco.Core;
using Umbraco.Core.Scoping;
namespace Umbraco.Tests.CoreThings
{
@@ -13,10 +14,10 @@ namespace Umbraco.Tests.CoreThings
{
SafeCallContext.Register(() =>
{
CallContext.FreeNamedDataSlot("test1");
CallContext.FreeNamedDataSlot("test2");
CallContext<string>.SetData("test1", null);
CallContext<string>.SetData("test2", null);
return null;
}, o => {});
}, o => { });
}
[OneTimeSetUp]
@@ -44,10 +45,10 @@ namespace Umbraco.Tests.CoreThings
[Test]
public void Test1()
{
CallContext.LogicalSetData("test1", "test1");
Assert.IsNull(CallContext.LogicalGetData("test2"));
CallContext<string>.SetData("test1", "test1");
Assert.IsNull(CallContext<string>.GetData("test2"));
CallContext.LogicalSetData("test3b", "test3b");
CallContext<string>.SetData("test3b", "test3b");
if (_first)
{
@@ -55,21 +56,21 @@ namespace Umbraco.Tests.CoreThings
}
else
{
Assert.IsNotNull(CallContext.LogicalGetData("test3a")); // leak!
Assert.IsNotNull(CallContext<string>.GetData("test3a")); // leak!
}
}
[Test]
public void Test2()
{
CallContext.LogicalSetData("test2", "test2");
Assert.IsNull(CallContext.LogicalGetData("test1"));
CallContext<string>.SetData("test2", "test2");
Assert.IsNull(CallContext<string>.GetData("test1"));
}
[Test]
public void Test3()
{
CallContext.LogicalSetData("test3a", "test3a");
CallContext<string>.SetData("test3a", "test3a");
if (_first)
{
@@ -77,7 +78,7 @@ namespace Umbraco.Tests.CoreThings
}
else
{
Assert.IsNotNull(CallContext.LogicalGetData("test3b")); // leak!
Assert.IsNotNull(CallContext<string>.GetData("test3b")); // leak!
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Data;
using Moq;
using NUnit.Framework;
@@ -45,8 +46,18 @@ namespace Umbraco.Tests.Migrations
throw new NotImplementedException();
}
public IScopeContext Context { get; set; }
public ISqlContext SqlContext { get; set; }
#if DEBUG_SCOPES
public ScopeInfo GetScopeInfo(IScope scope)
{
throw new NotImplementedException();
}
public IEnumerable<ScopeInfo> ScopeInfos => throw new NotImplementedException();
#endif
}
[Test]
+1 -1
View File
@@ -13,7 +13,7 @@ namespace Umbraco.Tests.Persistence
{
[TestFixture]
[Timeout(60000)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Serilog)]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)]
public class LocksTests : TestWithDatabaseBase
{
protected override void Initialize()
+2 -9
View File
@@ -1,17 +1,10 @@
using System;
using System.Collections;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Persistence;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using CallContext = Umbraco.Core.Scoping.CallContext;
//using CallContext = Umbraco.Core.Scoping.CallContext;
namespace Umbraco.Tests.Scoping
{
@@ -126,8 +119,8 @@ namespace Umbraco.Tests.Scoping
Assert.AreSame(scope, ((Scope) nested).ParentScope);
// it's moved over to call context
var callContextKey = CallContext.GetData(ScopeProvider.ScopeItemKey).AsGuid();
Assert.AreNotEqual(Guid.Empty, callContextKey);
var callContextScope = CallContext<IScope>.GetData(ScopeProvider.ScopeItemKey);
Assert.IsNotNull(callContextScope);
// only if Core.DEBUG_SCOPES are defined
//var ccnested = scopeProvider.CallContextObjects[callContextKey];