diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs new file mode 100644 index 0000000000..9bf9c365f0 --- /dev/null +++ b/src/Umbraco.Core/Collections/StackQueue.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Collections +{ + /// + /// Collection that can be both a queue and a stack. + /// + /// + public class StackQueue + { + private readonly LinkedList _linkedList = new LinkedList(); + + public void Clear() + { + _linkedList.Clear(); + } + + public void Push(T obj) + { + _linkedList.AddFirst(obj); + } + + public void Enqueue(T obj) + { + _linkedList.AddFirst(obj); + } + + public T Pop() + { + var obj = _linkedList.First.Value; + _linkedList.RemoveFirst(); + return obj; + } + + public T Dequeue() + { + var obj = _linkedList.Last.Value; + _linkedList.RemoveLast(); + return obj; + } + + public T PeekStack() + { + return _linkedList.First.Value; + } + + public T PeekQueue() + { + return _linkedList.Last.Value; + } + + public int Count + { + get + { + return _linkedList.Count; + } + } + } +} diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 10594fc970..25c69899c0 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core public static IGridConfig Grids(this Configs configs) => configs.GetConfig(); - internal static CoreDebug CoreDebug(this Configs configs) - => configs.GetConfig(); + public static ICoreDebug CoreDebug(this Configs configs) + => configs.GetConfig(); public static void AddCoreConfigs(this Configs configs) { @@ -39,7 +39,7 @@ namespace Umbraco.Core configs.Add("umbracoConfiguration/settings"); configs.Add("umbracoConfiguration/HealthChecks"); - configs.Add(() => new CoreDebug()); + configs.Add(() => new CoreDebug()); // GridConfig depends on runtime caches, manifest parsers... and cannot be available during composition configs.Add(factory => new GridConfig( diff --git a/src/Umbraco.Core/Configuration/CoreDebug.cs b/src/Umbraco.Core/Configuration/CoreDebug.cs index b24e8a3329..c9f4f81417 100644 --- a/src/Umbraco.Core/Configuration/CoreDebug.cs +++ b/src/Umbraco.Core/Configuration/CoreDebug.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration { - internal class CoreDebug + internal class CoreDebug : ICoreDebug { public CoreDebug() { diff --git a/src/Umbraco.Core/Configuration/ICoreDebug.cs b/src/Umbraco.Core/Configuration/ICoreDebug.cs new file mode 100644 index 0000000000..2ddb066f26 --- /dev/null +++ b/src/Umbraco.Core/Configuration/ICoreDebug.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Configuration +{ + public interface ICoreDebug + { + bool DumpOnTimeoutThreadAbort { get; } + bool LogUncompletedScopes { get; } + } +} diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 4c058cbdb7..c5e59b4a7c 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.Data; +using System.Linq; using System.Text; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -18,9 +21,15 @@ namespace Umbraco.Core.Scoping /// Not thread-safe obviously. internal class Scope : IScope2 { + private enum LockType + { + ReadLock, + WriteLock + } + private readonly ScopeProvider _scopeProvider; private readonly ILogger _logger; - + private readonly ICoreDebug _coreDebug; private readonly IsolationLevel _isolationLevel; private readonly RepositoryCacheMode _repositoryCacheMode; private readonly bool? _scopeFileSystem; @@ -37,14 +46,20 @@ namespace Umbraco.Core.Scoping private IEventDispatcher _eventDispatcher; private object _dictionaryLocker; + private readonly object _lockQueueLocker = new object(); + + // This is all used to safely track read/write locks at given Scope levels so that + // when we dispose we can verify that everything has been cleaned up correctly. private HashSet _readLocks; private HashSet _writeLocks; - internal Dictionary> ReadLocks; - internal Dictionary> WriteLocks; + private Dictionary> _readLocksDictionary; + private Dictionary> _writeLocksDictionary; + + private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; // initializes a new scope private Scope(ScopeProvider scopeProvider, - ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, + ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, ICoreDebug coreDebug, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, @@ -54,7 +69,6 @@ namespace Umbraco.Core.Scoping { _scopeProvider = scopeProvider; _logger = logger; - Context = scopeContext; _isolationLevel = isolationLevel; @@ -63,9 +77,8 @@ namespace Umbraco.Core.Scoping _scopeFileSystem = scopeFileSystems; _callContext = callContext; _autoComplete = autoComplete; - Detachable = detachable; - + _coreDebug = coreDebug; _dictionaryLocker = new object(); #if DEBUG_SCOPES @@ -120,31 +133,77 @@ namespace Umbraco.Core.Scoping // initializes a new scope public Scope(ScopeProvider scopeProvider, - ILogger logger, FileSystems fileSystems, bool detachable, ScopeContext scopeContext, + ILogger logger, FileSystems fileSystems, bool detachable, ScopeContext scopeContext, ICoreDebug coreDebug, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) + : this(scopeProvider, logger, fileSystems, null, scopeContext, detachable, coreDebug, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) { } // initializes a new scope in a nested scopes chain, with its parent public Scope(ScopeProvider scopeProvider, - ILogger logger, FileSystems fileSystems, Scope parent, + ILogger logger, FileSystems fileSystems, Scope parent, ICoreDebug coreDebug, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) + : this(scopeProvider, logger, fileSystems, parent, null, false, coreDebug, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) { } + /// + /// Used for testing. Ensures and gets any queued read locks. + /// + /// + internal Dictionary> GetReadLocks() + { + EnsureDbLocks(); + // always delegate to root/parent scope. + if (ParentScope is not null) + { + return ParentScope.GetReadLocks(); + } + else + { + return _readLocksDictionary; + } + } + + /// + /// Used for testing. Ensures and gets and queued write locks. + /// + /// + internal Dictionary> GetWriteLocks() + { + EnsureDbLocks(); + // always delegate to root/parent scope. + if (ParentScope is not null) + { + return ParentScope.GetWriteLocks(); + } + else + { + return _writeLocksDictionary; + } + } + public Guid InstanceId { get; } = Guid.NewGuid(); - public ISqlContext SqlContext => _scopeProvider.SqlContext; + public ISqlContext SqlContext + { + get + { + if (_scopeProvider.SqlContext == null) + { + throw new InvalidOperationException($"The {nameof(_scopeProvider.SqlContext)} on the scope provider is null"); + } + return _scopeProvider.SqlContext; + } + } // a value indicating whether to force call-context public bool CallContext @@ -214,7 +273,7 @@ namespace Umbraco.Core.Scoping { if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel; if (ParentScope != null) return ParentScope.IsolationLevel; - return Database.SqlContext.SqlSyntax.DefaultIsolationLevel; + return SqlContext.SqlSyntax.DefaultIsolationLevel; } } @@ -226,7 +285,24 @@ namespace Umbraco.Core.Scoping EnsureNotDisposed(); if (_database != null) + { + // If the database has already been resolved, we are already in a + // transaction, but it's possible that more locks have been requested + // so acquire them. + + // TODO: This is the issue we face with non-eager locks. If locks are + // requested after the Database property is resolved, those locks may + // not get executed because the developer may be using their local Database variable + // instead of accessing via scope.Database. + // In our case within Umbraco, I don't think this ever occurs, all locks are requested + // up-front, however, that might not be the case for others. + // The other way to deal with this would be to bake this callback somehow into the + // UmbracoDatabase instance directly and ensure it's called when OnExecutingCommand + // (so long as the executing command isn't a lock command itself!) + // If we could do that, that would be the ultimate lazy executed locks. + EnsureDbLocks(); return _database; + } if (ParentScope != null) { @@ -244,6 +320,7 @@ namespace Umbraco.Core.Scoping try { _database.BeginTransaction(IsolationLevel); + EnsureDbLocks(); return _database; } catch @@ -260,7 +337,16 @@ namespace Umbraco.Core.Scoping get { EnsureNotDisposed(); - return ParentScope == null ? _database : ParentScope.DatabaseOrNull; + if (ParentScope == null) + { + if (_database != null) + { + EnsureDbLocks(); + } + return _database; + } + + return ParentScope.DatabaseOrNull; } } @@ -320,13 +406,92 @@ namespace Umbraco.Core.Scoping // if child did not complete we cannot complete if (completed.HasValue == false || completed.Value == false) { - if (LogUncompletedScopes) + if (_coreDebug.LogUncompletedScopes) + { _logger.Debug("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace); + } _completed = false; } } + /// + /// When we require a ReadLock or a WriteLock we don't immediately request these locks from the database, + /// instead we only request them when necessary (lazily). + /// To do this, we queue requests for read/write locks. + /// This is so that if there's a request for either of these + /// locks, but the service/repository returns an item from the cache, we don't end up making a DB call to make the + /// read/write lock. + /// This executes the queue of requested locks in order in an efficient way lazily whenever the database instance is + /// resolved. + /// + private void EnsureDbLocks() + { + // always delegate to the root parent + if (ParentScope is not null) + { + ParentScope.EnsureDbLocks(); + } + else + { + lock (_lockQueueLocker) + { + if (_queuedLocks?.Count > 0) + { + var currentType = LockType.ReadLock; + var currentTimeout = TimeSpan.Zero; + var currentInstanceId = InstanceId; + var collectedIds = new HashSet(); + + var i = 0; + while (_queuedLocks.Count > 0) + { + var (lockType, timeout, instanceId, lockId) = _queuedLocks.Dequeue(); + + if (i == 0) + { + currentType = lockType; + currentTimeout = timeout; + currentInstanceId = instanceId; + } + else if (lockType != currentType || timeout != currentTimeout || instanceId != currentInstanceId) + { + // the lock type, instanceId or timeout switched. + // process the lock ids collected + switch (currentType) + { + case LockType.ReadLock: + EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + break; + case LockType.WriteLock: + EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + break; + } + // clear the collected and set new type + collectedIds.Clear(); + currentType = lockType; + currentTimeout = timeout; + currentInstanceId = instanceId; + } + collectedIds.Add(lockId); + i++; + } + + // process the remaining + switch (currentType) + { + case LockType.ReadLock: + EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + break; + case LockType.WriteLock: + EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); + break; + } + } + } + } + } + private void EnsureNotDisposed() { if (_disposed) @@ -366,7 +531,7 @@ namespace Umbraco.Core.Scoping { // We're the parent scope, make sure that locks of all scopes has been cleared // Since we're only reading we don't have to be in a lock - if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) + if (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0) { var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); _logger.Error(exception, GenerateUnclearedScopesLogMessage()); @@ -389,6 +554,11 @@ namespace Umbraco.Core.Scoping else DisposeLastScope(); + lock (_lockQueueLocker) + { + _queuedLocks?.Clear(); + } + _disposed = true; GC.SuppressFinalize(this); } @@ -402,8 +572,8 @@ namespace Umbraco.Core.Scoping // Dump the dicts into a message for the locks. StringBuilder builder = new StringBuilder(); builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); - WriteLockDictionaryToString(ReadLocks, builder, "read locks"); - WriteLockDictionaryToString(WriteLocks, builder, "write locks"); + WriteLockDictionaryToString(_readLocksDictionary, builder, "read locks"); + WriteLockDictionaryToString(_writeLocksDictionary, builder, "write locks"); return builder.ToString(); } @@ -478,7 +648,7 @@ namespace Umbraco.Core.Scoping // to ensure we don't leave a scope around, etc private void RobustExit(bool completed, bool onException) { - if (onException) completed = false; + if (onException) completed = false; TryFinally(() => { @@ -540,14 +710,6 @@ namespace Umbraco.Core.Scoping } } - // backing field for LogUncompletedScopes - private static bool? _logUncompletedScopes; - - // caching config - // true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true" - private static bool LogUncompletedScopes => (_logUncompletedScopes - ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; - /// /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, /// for a specific scope instance and lock identifier. Must be called within a lock. @@ -590,23 +752,122 @@ namespace Umbraco.Core.Scoping { lock (_dictionaryLocker) { - ReadLocks?.Remove(instanceId); - WriteLocks?.Remove(instanceId); + _readLocksDictionary?.Remove(instanceId); + _writeLocksDictionary?.Remove(instanceId); + + // remove any queued locks for this instance that weren't used. + while (_queuedLocks?.Count > 0) + { + // It's safe to assume that the locks on the top of the stack belong to this instance, + // since any child scopes that might have added locks to the stack must be disposed before we try and dispose this instance. + var top = _queuedLocks.PeekStack(); + if (top.instanceId == instanceId) + { + _queuedLocks.Pop(); + } + else + { + break; + } + } } } } - /// - public void ReadLock(params int[] lockIds) => ReadLockInner(InstanceId, null, lockIds); + public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds); /// - public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId); + public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); + + public void EagerReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(Database, InstanceId, timeout, lockId); /// - public void WriteLock(params int[] lockIds) => WriteLockInner(InstanceId, null, lockIds); + public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); + + public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds); /// - public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId); + public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); + + public void EagerWriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(Database, InstanceId, timeout, lockId); + + /// + public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); + + public void LazyReadLockInner(Guid instanceId, params int[] lockIds) + { + if (ParentScope != null) + { + ParentScope.LazyReadLockInner(instanceId, lockIds); + } + else + { + LazyLockInner(LockType.ReadLock, instanceId, lockIds); + } + } + + public void LazyReadLockInner(Guid instanceId, TimeSpan timeout, int lockId) + { + if (ParentScope != null) + { + ParentScope.LazyReadLockInner(instanceId, timeout, lockId); + } + else + { + LazyLockInner(LockType.ReadLock, instanceId, timeout, lockId); + } + } + + public void LazyWriteLockInner(Guid instanceId, params int[] lockIds) + { + if (ParentScope != null) + { + ParentScope.LazyWriteLockInner(instanceId, lockIds); + } + else + { + LazyLockInner(LockType.WriteLock, instanceId, lockIds); + } + } + + public void LazyWriteLockInner(Guid instanceId, TimeSpan timeout, int lockId) + { + if (ParentScope != null) + { + ParentScope.LazyWriteLockInner(instanceId, timeout, lockId); + } + else + { + LazyLockInner(LockType.WriteLock, instanceId, timeout, lockId); + } + } + + private void LazyLockInner(LockType lockType, Guid instanceId, params int[] lockIds) + { + lock (_lockQueueLocker) + { + if (_queuedLocks == null) + { + _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + } + foreach (var lockId in lockIds) + { + _queuedLocks.Enqueue((lockType, TimeSpan.Zero, instanceId, lockId)); + } + } + } + + private void LazyLockInner(LockType lockType, Guid instanceId, TimeSpan timeout, int lockId) + { + lock (_lockQueueLocker) + { + if (_queuedLocks == null) + { + _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + } + _queuedLocks.Enqueue((lockType, timeout, instanceId, lockId)); + } + } /// /// Handles acquiring a read lock, will delegate it to the parent if there are any. @@ -614,17 +875,17 @@ namespace Umbraco.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void ReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) + private void EagerReadLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.ReadLockInner(instanceId, timeout, lockIds); + ParentScope.EagerReadLockInner(db, instanceId, timeout, lockIds); } else { // We are the outermost scope, handle the lock request. - LockInner(instanceId, ref ReadLocks, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); + LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); } } @@ -634,17 +895,17 @@ namespace Umbraco.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void WriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) + private void EagerWriteLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.WriteLockInner(instanceId, timeout, lockIds); + ParentScope.EagerWriteLockInner(db, instanceId, timeout, lockIds); } else { // We are the outermost scope, handle the lock request. - LockInner(instanceId, ref WriteLocks, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); + LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); } } @@ -658,8 +919,8 @@ namespace Umbraco.Core.Scoping /// Delegate used to request the lock from the database with a timeout. /// Optional timeout parameter to specify a timeout. /// Lock identifiers to lock on. - private void LockInner(Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, - Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout = null, + private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, + Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout = null, params int[] lockIds) { lock (_dictionaryLocker) @@ -677,12 +938,12 @@ namespace Umbraco.Core.Scoping if (timeout is null) { // We just want an ordinary lock. - obtainLock(lockId); + obtainLock(db, lockId); } else { // We want a lock with a custom timeout - obtainLockTimeout(lockId, timeout.Value); + obtainLockTimeout(db, lockId, timeout.Value); } } catch @@ -708,9 +969,9 @@ namespace Umbraco.Core.Scoping /// Obtains an ordinary read lock. /// /// Lock object identifier to lock. - private void ObtainReadLock(int lockId) + private void ObtainReadLock(IUmbracoDatabase db, int lockId) { - Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); + SqlContext.SqlSyntax.ReadLock(db, lockId); } /// @@ -718,24 +979,24 @@ namespace Umbraco.Core.Scoping /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutReadLock(int lockId, TimeSpan timeout) + private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) { - var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + var syntax2 = SqlContext.SqlSyntax as ISqlSyntaxProvider2; if (syntax2 is null) { - throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + throw new InvalidOperationException($"{SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - syntax2.ReadLock(Database, timeout, lockId); + syntax2.ReadLock(db, timeout, lockId); } /// /// Obtains an ordinary write lock. /// /// Lock object identifier to lock. - private void ObtainWriteLock(int lockId) + private void ObtainWriteLock(IUmbracoDatabase db, int lockId) { - Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); + SqlContext.SqlSyntax.WriteLock(db, lockId); } /// @@ -743,15 +1004,15 @@ namespace Umbraco.Core.Scoping /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) + private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) { - var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + var syntax2 = SqlContext.SqlSyntax as ISqlSyntaxProvider2; if (syntax2 is null) { - throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + throw new InvalidOperationException($"{SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } - syntax2.WriteLock(Database, timeout, lockId); + syntax2.WriteLock(db, timeout, lockId); } } } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index a1cc128181..c07e89d29f 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -6,6 +6,7 @@ using System.Data; using System.Runtime.Remoting.Messaging; using System.Web; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -23,13 +24,15 @@ namespace Umbraco.Core.Scoping internal class ScopeProvider : IScopeProvider, IScopeAccessor { private readonly ILogger _logger; + private readonly ICoreDebug _coreDebug; private readonly FileSystems _fileSystems; - public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, ILogger logger) + public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, ILogger logger, ICoreDebug coreDebug) { DatabaseFactory = databaseFactory; _fileSystems = fileSystems; _logger = logger; + _coreDebug = coreDebug; // take control of the FileSystems _fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems; @@ -327,7 +330,7 @@ namespace Umbraco.Core.Scoping IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null) { - return new Scope(this, _logger, _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); + return new Scope(this, _logger, _fileSystems, true, null, _coreDebug, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); } /// @@ -383,13 +386,13 @@ namespace Umbraco.Core.Scoping { var ambientContext = AmbientContext; var newContext = ambientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, _logger, _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); + var scope = new Scope(this, _logger, _fileSystems, false, newContext, _coreDebug, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); // assign only if scope creation did not throw! SetAmbient(scope, newContext ?? ambientContext); return scope; } - var nested = new Scope(this, _logger, _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); + var nested = new Scope(this, _logger, _fileSystems, ambientScope, _coreDebug, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); SetAmbient(nested, AmbientContext); return nested; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f2b57e8c96..7ad209efdf 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,8 @@ --> + + diff --git a/src/Umbraco.Tests/Collections/StackQueueTests.cs b/src/Umbraco.Tests/Collections/StackQueueTests.cs new file mode 100644 index 0000000000..52d0370f0d --- /dev/null +++ b/src/Umbraco.Tests/Collections/StackQueueTests.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using Umbraco.Core.Collections; + +namespace Umbraco.Tests.Collections +{ + [TestFixture] + public class StackQueueTests + { + [Test] + public void Queue() + { + var sq = new StackQueue(); + for (int i = 0; i < 3; i++) + { + sq.Enqueue(i); + } + + var expected = 0; + while(sq.Count > 0) + { + var next = sq.Dequeue(); + Assert.AreEqual(expected, next); + expected++; + } + } + + [Test] + public void Stack() + { + var sq = new StackQueue(); + for (int i = 0; i < 3; i++) + { + sq.Push(i); + } + + var expected = 2; + while (sq.Count > 0) + { + var next = sq.Pop(); + Assert.AreEqual(expected, next); + expected--; + } + } + + [Test] + public void Stack_And_Queue() + { + var sq = new StackQueue(); + for (int i = 0; i < 5; i++) + { + if (i % 2 == 0) + { + sq.Push(i); + } + else + { + sq.Enqueue(i); + } + } + + // 4 (push) + // 3 (enqueue) + // 2 (push) + // 1 (enqueue) + // 0 (push) + + Assert.AreEqual(4, sq.Pop()); + Assert.AreEqual(0, sq.Dequeue()); + Assert.AreEqual(3, sq.Pop()); + Assert.AreEqual(1, sq.Dequeue()); + Assert.AreEqual(2, sq.Pop()); + } + } +} diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index 042cac1281..b9a9b7dcb4 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -30,7 +31,7 @@ namespace Umbraco.Tests.Components var logger = Mock.Of(); var f = new UmbracoDatabaseFactory(logger, new Lazy(() => new MapperCollection(Enumerable.Empty()))); var fs = new FileSystems(mock.Object, logger); - var p = new ScopeProvider(f, fs, logger); + var p = new ScopeProvider(f, fs, logger, Mock.Of(x => x.LogUncompletedScopes == true)); mock.Setup(x => x.GetInstance(typeof (ILogger))).Returns(logger); mock.Setup(x => x.GetInstance(typeof (IProfilingLogger))).Returns(new ProfilingLogger(Mock.Of(), Mock.Of())); diff --git a/src/Umbraco.Tests/Persistence/LocksTests.cs b/src/Umbraco.Tests/Persistence/LocksTests.cs index 8872329284..d0837f5e62 100644 --- a/src/Umbraco.Tests/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests/Persistence/LocksTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using NPoco; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; @@ -36,9 +37,9 @@ namespace Umbraco.Tests.Persistence [Test] public void SingleReadLockTest() { - using (var scope = ScopeProvider.CreateScope()) + using (var scope = (Scope)ScopeProvider.CreateScope()) { - scope.ReadLock(Constants.Locks.Servers); + scope.EagerReadLock(Constants.Locks.Servers); scope.Complete(); } } @@ -59,11 +60,11 @@ namespace Umbraco.Tests.Persistence var ic = i; // capture threads[i] = new Thread(() => { - using (var scope = ScopeProvider.CreateScope()) + using (var scope = (Scope)ScopeProvider.CreateScope()) { try { - scope.ReadLock(Constants.Locks.Servers); + scope.EagerReadLock(Constants.Locks.Servers); lock (locker) { acquired++; @@ -104,6 +105,35 @@ namespace Umbraco.Tests.Persistence Assert.IsNull(exceptions[i]); } + [Test] + public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted() + { + var sqlCount = 0; + + using (var scope = (Scope)ScopeProvider.CreateScope()) + { + var db = (UmbracoDatabase)scope.Database; + try + { + db.EnableSqlCount = true; + + // Issue a lock request, but we are using non-eager + // locks so this only queues the request. + // The lock will not be issued unless we resolve + // scope.Database + scope.WriteLock(Constants.Locks.Servers); + + sqlCount = db.SqlCount; + } + finally + { + db.EnableSqlCount = false; + } + } + + Assert.AreEqual(0, sqlCount); + } + [Test] public void ConcurrentWritersTest() { @@ -122,7 +152,7 @@ namespace Umbraco.Tests.Persistence var ic = i; // capture threads[i] = new Thread(() => { - using (var scope = ScopeProvider.CreateScope()) + using (var scope = (Scope)ScopeProvider.CreateScope()) { try { @@ -132,7 +162,7 @@ namespace Umbraco.Tests.Persistence if (entered == threadCount) m1.Set(); } ms[ic].WaitOne(); - scope.WriteLock(Constants.Locks.Servers); + scope.EagerWriteLock(Constants.Locks.Servers); lock (locker) { acquired++; @@ -161,8 +191,10 @@ namespace Umbraco.Tests.Persistence m1.Wait(); // all threads have entered ms[0].Set(); // let 0 go + // TODO: This timing is flaky Thread.Sleep(100); for (var i = 1; i < threadCount; i++) ms[i].Set(); // let others go + // TODO: This timing is flaky Thread.Sleep(500); // only 1 thread has locked Assert.AreEqual(1, acquired); @@ -216,13 +248,13 @@ namespace Umbraco.Tests.Persistence private void DeadLockTestThread(int id1, int id2, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { - using (var scope = ScopeProvider.CreateScope()) + using (var scope = (Scope)ScopeProvider.CreateScope()) { try { otherEv.WaitOne(); Console.WriteLine($"[{id1}] WAIT {id1}"); - scope.WriteLock(id1); + scope.EagerWriteLock(id1); Console.WriteLine($"[{id1}] GRANT {id1}"); WriteLocks(scope.Database); myEv.Set(); @@ -233,7 +265,7 @@ namespace Umbraco.Tests.Persistence Thread.Sleep(200); // cannot wait due to deadlock... just give it a bit of time Console.WriteLine($"[{id1}] WAIT {id2}"); - scope.WriteLock(id2); + scope.EagerWriteLock(id2); Console.WriteLine($"[{id1}] GRANT {id2}"); WriteLocks(scope.Database); } @@ -288,7 +320,7 @@ namespace Umbraco.Tests.Persistence Console.WriteLine("Write lock A"); // This will acquire right away - realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + realScope.EagerWriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); Thread.Sleep(6000); // Wait longer than the Read Lock B timeout scope.Complete(); Console.WriteLine("Finished Write lock A"); @@ -308,7 +340,7 @@ namespace Umbraco.Tests.Persistence // This will wait for the write lock to release but it isn't going to wait long // enough so an exception will be thrown. Assert.Throws(() => - realScope.ReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + realScope.EagerReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); scope.Complete(); Console.WriteLine("Finished Read lock B"); @@ -326,7 +358,7 @@ namespace Umbraco.Tests.Persistence // This will wait for the write lock to release but it isn't going to wait long // enough so an exception will be thrown. Assert.Throws(() => - realScope.WriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + realScope.EagerWriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); scope.Complete(); Console.WriteLine("Finished Write lock C"); @@ -349,7 +381,7 @@ namespace Umbraco.Tests.Persistence Console.WriteLine("Write lock A"); // This will acquire right away - realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + realScope.EagerWriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); Thread.Sleep(4000); // Wait less than the Read Lock B timeout scope.Complete(); Interlocked.Increment(ref locksCompleted); @@ -369,7 +401,7 @@ namespace Umbraco.Tests.Persistence // This will wait for the write lock to release Assert.DoesNotThrow(() => - realScope.ReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); + realScope.EagerReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); Assert.GreaterOrEqual(locksCompleted, 1); @@ -389,7 +421,7 @@ namespace Umbraco.Tests.Persistence // This will wait for the write lock to release Assert.DoesNotThrow(() => - realScope.ReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); + realScope.EagerReadLock(TimeSpan.FromMilliseconds(6000), Constants.Locks.ContentTree)); Assert.GreaterOrEqual(locksCompleted, 1); @@ -417,7 +449,7 @@ namespace Umbraco.Tests.Persistence Console.WriteLine("Write lock A"); // TODO: In theory this would throw - realScope.WriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree); + realScope.EagerWriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree); scope.Complete(); Console.WriteLine("Finished Write lock A"); } @@ -425,13 +457,13 @@ namespace Umbraco.Tests.Persistence private void NoDeadLockTestThread(int id, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { - using (var scope = ScopeProvider.CreateScope()) + using (var scope = (Scope)ScopeProvider.CreateScope()) { try { otherEv.WaitOne(); Console.WriteLine($"[{id}] WAIT {id}"); - scope.WriteLock(id); + scope.EagerWriteLock(id); Console.WriteLine($"[{id}] GRANT {id}"); WriteLocks(scope.Database); myEv.Set(); diff --git a/src/Umbraco.Tests/Persistence/UnitOfWorkTests.cs b/src/Umbraco.Tests/Persistence/UnitOfWorkTests.cs index 02ad6b3971..2b60807088 100644 --- a/src/Umbraco.Tests/Persistence/UnitOfWorkTests.cs +++ b/src/Umbraco.Tests/Persistence/UnitOfWorkTests.cs @@ -1,6 +1,7 @@ using System; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -16,9 +17,9 @@ namespace Umbraco.Tests.Persistence var provider = TestObjects.GetScopeProvider(Logger); Assert.Throws(() => { - using (var scope = provider.CreateScope()) + using (var scope = (Scope)provider.CreateScope()) { - scope.ReadLock(-666); + scope.EagerReadLock(-666); scope.Complete(); } }); @@ -28,9 +29,9 @@ namespace Umbraco.Tests.Persistence public void ReadLockExisting() { var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (var scope = (Scope)provider.CreateScope()) { - scope.ReadLock(Constants.Locks.Servers); + scope.EagerReadLock(Constants.Locks.Servers); scope.Complete(); } } @@ -41,9 +42,9 @@ namespace Umbraco.Tests.Persistence var provider = TestObjects.GetScopeProvider(Logger); Assert.Throws(() => { - using (var scope = provider.CreateScope()) + using (var scope = (Scope)provider.CreateScope()) { - scope.WriteLock(-666); + scope.EagerWriteLock(-666); scope.Complete(); } }); @@ -53,9 +54,9 @@ namespace Umbraco.Tests.Persistence public void WriteLockExisting() { var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) + using (var scope = (Scope)provider.CreateScope()) { - scope.WriteLock(Constants.Locks.Servers); + scope.EagerWriteLock(Constants.Locks.Servers); scope.Complete(); } } diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs index 038376f71c..a776a5bae6 100644 --- a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -5,6 +5,7 @@ using NPoco; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -34,6 +35,7 @@ namespace Umbraco.Tests.Scoping // Setup mock of database factory to return mock of database. databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object); + databaseFactory.Setup(x => x.SqlContext).Returns(sqlContext.Object); // Setup mock of database to return mock of sql SqlContext database.Setup(x => x.SqlContext).Returns(sqlContext.Object); @@ -41,7 +43,40 @@ namespace Umbraco.Tests.Scoping // Setup mock of ISqlContext to return syntaxProviderMock sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); - return new ScopeProvider(databaseFactory.Object, fileSystem, logger); + return new ScopeProvider(databaseFactory.Object, fileSystem, logger, Mock.Of(x => x.LogUncompletedScopes == true)); + } + + [Test] + public void Unused_Lazy_Locks_Cleared_At_Child_Scope() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + var outerScope = (Scope)scopeProvider.CreateScope(); + outerScope.ReadLock(Constants.Locks.Domains); + outerScope.ReadLock(Constants.Locks.Languages); + + using (var innerScope1 = (Scope)scopeProvider.CreateScope()) + { + innerScope1.ReadLock(Constants.Locks.Domains); + innerScope1.ReadLock(Constants.Locks.Languages); + + innerScope1.Complete(); + } + + using (var innerScope2 = (Scope)scopeProvider.CreateScope()) + { + innerScope2.ReadLock(Constants.Locks.Domains); + innerScope2.ReadLock(Constants.Locks.Languages); + + // force resolving the locks + var locks = innerScope2.GetReadLocks(); + + innerScope2.Complete(); + } + + outerScope.Complete(); + + Assert.DoesNotThrow(() => outerScope.Dispose()); } [Test] @@ -49,20 +84,20 @@ namespace Umbraco.Tests.Scoping { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - using (var outerScope = scopeProvider.CreateScope()) + using (var outerScope = (Scope)scopeProvider.CreateScope()) { - outerScope.WriteLock(Constants.Locks.Domains); - outerScope.WriteLock(Constants.Locks.Languages); + outerScope.EagerWriteLock(Constants.Locks.Domains); + outerScope.EagerWriteLock(Constants.Locks.Languages); - using (var innerScope1 = scopeProvider.CreateScope()) + using (var innerScope1 = (Scope)scopeProvider.CreateScope()) { - innerScope1.WriteLock(Constants.Locks.Domains); - innerScope1.WriteLock(Constants.Locks.Languages); + innerScope1.EagerWriteLock(Constants.Locks.Domains); + innerScope1.EagerWriteLock(Constants.Locks.Languages); - using (var innerScope2 = scopeProvider.CreateScope()) + using (var innerScope2 = (Scope)scopeProvider.CreateScope()) { - innerScope2.WriteLock(Constants.Locks.Domains); - innerScope2.WriteLock(Constants.Locks.Languages); + innerScope2.EagerWriteLock(Constants.Locks.Domains); + innerScope2.EagerWriteLock(Constants.Locks.Languages); innerScope2.Complete(); } innerScope1.Complete(); @@ -79,18 +114,18 @@ namespace Umbraco.Tests.Scoping { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - using (var outerScope = scopeProvider.CreateScope()) + using (var outerScope = (Scope)scopeProvider.CreateScope()) { - outerScope.WriteLock(Constants.Locks.Languages); + outerScope.EagerWriteLock(Constants.Locks.Languages); - using (var innerScope = scopeProvider.CreateScope()) + using (var innerScope = (Scope)scopeProvider.CreateScope()) { - innerScope.WriteLock(Constants.Locks.Languages); - innerScope.WriteLock(Constants.Locks.ContentTree); + innerScope.EagerWriteLock(Constants.Locks.Languages); + innerScope.EagerWriteLock(Constants.Locks.ContentTree); innerScope.Complete(); } - outerScope.WriteLock(Constants.Locks.ContentTree); + outerScope.EagerWriteLock(Constants.Locks.ContentTree); outerScope.Complete(); } @@ -99,27 +134,25 @@ namespace Umbraco.Tests.Scoping } [Test] - public void WriteLock_With_Timeout_Acquired_Only_Once_Per_Key(){ + public void WriteLock_With_Timeout_Acquired_Only_Once_Per_Key() + { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); var timeout = TimeSpan.FromMilliseconds(10000); - using (var outerScope = scopeProvider.CreateScope()) + using (var outerScope = (Scope)scopeProvider.CreateScope()) { - var realScope = (Scope) outerScope; - realScope.WriteLock(timeout, Constants.Locks.Domains); - realScope.WriteLock(timeout, Constants.Locks.Languages); + outerScope.EagerWriteLock(timeout, Constants.Locks.Domains); + outerScope.EagerWriteLock(timeout, Constants.Locks.Languages); - using (var innerScope1 = scopeProvider.CreateScope()) + using (var innerScope1 = (Scope)scopeProvider.CreateScope()) { - var realInnerScope1 = (Scope) outerScope; - realInnerScope1.WriteLock(timeout, Constants.Locks.Domains); - realInnerScope1.WriteLock(timeout, Constants.Locks.Languages); + innerScope1.EagerWriteLock(timeout, Constants.Locks.Domains); + innerScope1.EagerWriteLock(timeout, Constants.Locks.Languages); - using (var innerScope2 = scopeProvider.CreateScope()) + using (var innerScope2 = (Scope)scopeProvider.CreateScope()) { - var realInnerScope2 = (Scope) innerScope2; - realInnerScope2.WriteLock(timeout, Constants.Locks.Domains); - realInnerScope2.WriteLock(timeout, Constants.Locks.Languages); + innerScope2.EagerWriteLock(timeout, Constants.Locks.Domains); + innerScope2.EagerWriteLock(timeout, Constants.Locks.Languages); innerScope2.Complete(); } innerScope1.Complete(); @@ -137,20 +170,20 @@ namespace Umbraco.Tests.Scoping { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - using (var outerScope = scopeProvider.CreateScope()) + using (var outerScope = (Scope)scopeProvider.CreateScope()) { - outerScope.ReadLock(Constants.Locks.Domains); - outerScope.ReadLock(Constants.Locks.Languages); + outerScope.EagerReadLock(Constants.Locks.Domains); + outerScope.EagerReadLock(Constants.Locks.Languages); - using (var innerScope1 = scopeProvider.CreateScope()) + using (var innerScope1 = (Scope)scopeProvider.CreateScope()) { - innerScope1.ReadLock(Constants.Locks.Domains); - innerScope1.ReadLock(Constants.Locks.Languages); + innerScope1.EagerReadLock(Constants.Locks.Domains); + innerScope1.EagerReadLock(Constants.Locks.Languages); - using (var innerScope2 = scopeProvider.CreateScope()) + using (var innerScope2 = (Scope)scopeProvider.CreateScope()) { - innerScope2.ReadLock(Constants.Locks.Domains); - innerScope2.ReadLock(Constants.Locks.Languages); + innerScope2.EagerReadLock(Constants.Locks.Domains); + innerScope2.EagerReadLock(Constants.Locks.Languages); innerScope2.Complete(); } @@ -171,23 +204,20 @@ namespace Umbraco.Tests.Scoping var scopeProvider = GetScopeProvider(out var syntaxProviderMock); var timeOut = TimeSpan.FromMilliseconds(10000); - using (var outerScope = scopeProvider.CreateScope()) + using (var outerScope = (Scope)scopeProvider.CreateScope()) { - var realOuterScope = (Scope) outerScope; - realOuterScope.ReadLock(timeOut, Constants.Locks.Domains); - realOuterScope.ReadLock(timeOut, Constants.Locks.Languages); + outerScope.EagerReadLock(timeOut, Constants.Locks.Domains); + outerScope.EagerReadLock(timeOut, Constants.Locks.Languages); - using (var innerScope1 = scopeProvider.CreateScope()) + using (var innerScope1 = (Scope)scopeProvider.CreateScope()) { - var realInnerScope1 = (Scope) innerScope1; - realInnerScope1.ReadLock(timeOut, Constants.Locks.Domains); - realInnerScope1.ReadLock(timeOut, Constants.Locks.Languages); + innerScope1.EagerReadLock(timeOut, Constants.Locks.Domains); + innerScope1.EagerReadLock(timeOut, Constants.Locks.Languages); - using (var innerScope2 = scopeProvider.CreateScope()) + using (var innerScope2 = (Scope)scopeProvider.CreateScope()) { - var realInnerScope2 = (Scope) innerScope2; - realInnerScope2.ReadLock(timeOut, Constants.Locks.Domains); - realInnerScope2.ReadLock(timeOut, Constants.Locks.Languages); + innerScope2.EagerReadLock(timeOut, Constants.Locks.Domains); + innerScope2.EagerReadLock(timeOut, Constants.Locks.Languages); innerScope2.Complete(); } @@ -211,10 +241,10 @@ namespace Umbraco.Tests.Scoping { outerScope.ReadLock(Constants.Locks.Languages); - using (var innerScope = scopeProvider.CreateScope()) + using (var innerScope = (Scope)scopeProvider.CreateScope()) { - innerScope.ReadLock(Constants.Locks.Languages); - innerScope.ReadLock(Constants.Locks.ContentTree); + innerScope.EagerReadLock(Constants.Locks.Languages); + innerScope.EagerReadLock(Constants.Locks.ContentTree); innerScope.Complete(); } @@ -232,28 +262,27 @@ namespace Umbraco.Tests.Scoping var scopeProvider = GetScopeProvider(out var syntaxProviderMock); Guid innerscopeId; - using (var outerscope = scopeProvider.CreateScope()) + using (var outerscope = (Scope)scopeProvider.CreateScope()) { - var realOuterScope = (Scope) outerscope; - outerscope.WriteLock(Constants.Locks.ContentTree); - outerscope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + outerscope.EagerWriteLock(Constants.Locks.ContentTree); + outerscope.EagerWriteLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, outerscope.GetWriteLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); - using (var innerScope = scopeProvider.CreateScope()) + using (var innerScope = (Scope)scopeProvider.CreateScope()) { innerscopeId = innerScope.InstanceId; - innerScope.WriteLock(Constants.Locks.ContentTree); - innerScope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); - Assert.AreEqual(2, realOuterScope.WriteLocks[innerscopeId][Constants.Locks.ContentTree]); + innerScope.EagerWriteLock(Constants.Locks.ContentTree); + innerScope.EagerWriteLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, outerscope.GetWriteLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, outerscope.GetWriteLocks()[innerscopeId][Constants.Locks.ContentTree]); - innerScope.WriteLock(Constants.Locks.Languages); - innerScope.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.WriteLocks[innerScope.InstanceId][Constants.Locks.Languages]); + innerScope.EagerWriteLock(Constants.Locks.Languages); + innerScope.EagerWriteLock(Constants.Locks.Languages); + Assert.AreEqual(2, outerscope.GetWriteLocks()[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(2, realOuterScope.WriteLocks[realOuterScope.InstanceId][Constants.Locks.ContentTree]); - Assert.IsFalse(realOuterScope.WriteLocks.ContainsKey(innerscopeId)); + Assert.AreEqual(2, outerscope.GetWriteLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(outerscope.GetWriteLocks().ContainsKey(innerscopeId)); outerscope.Complete(); } } @@ -264,28 +293,27 @@ namespace Umbraco.Tests.Scoping var scopeProvider = GetScopeProvider(out var syntaxProviderMock); Guid innerscopeId; - using (var outerscope = scopeProvider.CreateScope()) + using (var outerscope = (Scope)scopeProvider.CreateScope()) { - var realOuterScope = (Scope) outerscope; - outerscope.ReadLock(Constants.Locks.ContentTree); - outerscope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + outerscope.EagerReadLock(Constants.Locks.ContentTree); + outerscope.EagerReadLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, outerscope.GetReadLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); - using (var innerScope = scopeProvider.CreateScope()) + using (var innerScope = (Scope)scopeProvider.CreateScope()) { innerscopeId = innerScope.InstanceId; - innerScope.ReadLock(Constants.Locks.ContentTree); - innerScope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); - Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.ContentTree]); + innerScope.EagerReadLock(Constants.Locks.ContentTree); + innerScope.EagerReadLock(Constants.Locks.ContentTree); + Assert.AreEqual(2, outerscope.GetReadLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, outerscope.GetReadLocks()[innerScope.InstanceId][Constants.Locks.ContentTree]); - innerScope.ReadLock(Constants.Locks.Languages); - innerScope.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.Languages]); + innerScope.EagerReadLock(Constants.Locks.Languages); + innerScope.EagerReadLock(Constants.Locks.Languages); + Assert.AreEqual(2, outerscope.GetReadLocks()[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); - Assert.IsFalse(realOuterScope.ReadLocks.ContainsKey(innerscopeId)); + Assert.AreEqual(2, outerscope.GetReadLocks()[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(outerscope.GetReadLocks().ContainsKey(innerscopeId)); outerscope.Complete(); @@ -300,12 +328,12 @@ namespace Umbraco.Tests.Scoping using (var parentScope = scopeProvider.CreateScope()) { - var realParentScope = (Scope) parentScope; + var realParentScope = (Scope)parentScope; parentScope.WriteLock(Constants.Locks.ContentTree); parentScope.WriteLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innerScope1 = scopeProvider.CreateScope()) { @@ -314,11 +342,11 @@ namespace Umbraco.Tests.Scoping innerScope1.WriteLock(Constants.Locks.ContentTypes); innerScope1.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { @@ -326,29 +354,29 @@ namespace Umbraco.Tests.Scoping innerScope2.WriteLock(Constants.Locks.ContentTree); innerScope2.WriteLock(Constants.Locks.MediaTypes); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.Languages)}"); - Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.GetWriteLocks().ContainsKey(innerScope2Id)); innerScope1.Complete(); } - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); - Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope1Id)); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetWriteLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.GetWriteLocks().ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.GetWriteLocks().ContainsKey(innerScope1Id)); parentScope.Complete(); } @@ -362,11 +390,11 @@ namespace Umbraco.Tests.Scoping using (var parentScope = scopeProvider.CreateScope()) { - var realParentScope = (Scope) parentScope; + var realParentScope = (Scope)parentScope; parentScope.ReadLock(Constants.Locks.ContentTree); parentScope.ReadLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innserScope1 = scopeProvider.CreateScope()) { @@ -374,42 +402,42 @@ namespace Umbraco.Tests.Scoping innserScope1.ReadLock(Constants.Locks.ContentTree); innserScope1.ReadLock(Constants.Locks.ContentTypes); innserScope1.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { innerScope2Id = innerScope2.InstanceId; innerScope2.ReadLock(Constants.Locks.ContentTree); innerScope2.ReadLock(Constants.Locks.MediaTypes); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.Languages)}"); - Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.GetReadLocks().ContainsKey(innerScope2Id)); innserScope1.Complete(); } - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); - Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope1Id)); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.GetReadLocks()[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.GetReadLocks().ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.GetReadLocks().ContainsKey(innerScope1Id)); parentScope.Complete(); } @@ -421,12 +449,10 @@ namespace Umbraco.Tests.Scoping var scopeProvider = GetScopeProvider(out var syntaxProviderMock); syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); - using (var scope = scopeProvider.CreateScope()) + using (var scope = (Scope)scopeProvider.CreateScope()) { - var realScope = (Scope) scope; - - Assert.Throws(() => scope.WriteLock(Constants.Locks.Languages)); - Assert.IsFalse(realScope.WriteLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + Assert.Throws(() => scope.EagerWriteLock(Constants.Locks.Languages)); + Assert.IsFalse(scope.GetWriteLocks()[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); scope.Complete(); } } @@ -437,12 +463,10 @@ namespace Umbraco.Tests.Scoping var scopeProvider = GetScopeProvider(out var syntaxProviderMock); syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); - using (var scope = scopeProvider.CreateScope()) + using (var scope = (Scope)scopeProvider.CreateScope()) { - var realScope = (Scope) scope; - - Assert.Throws(() => scope.ReadLock(Constants.Locks.Languages)); - Assert.IsFalse(realScope.ReadLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + Assert.Throws(() => scope.EagerReadLock(Constants.Locks.Languages)); + Assert.IsFalse(scope.GetReadLocks()[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); scope.Complete(); } } @@ -451,7 +475,7 @@ namespace Umbraco.Tests.Scoping public void Scope_Throws_If_ReadLocks_Not_Cleared() { var scopeprovider = GetScopeProvider(out var syntaxProviderMock); - var scope = (Scope) scopeprovider.CreateScope(); + var scope = (Scope)scopeprovider.CreateScope(); try { @@ -460,14 +484,14 @@ namespace Umbraco.Tests.Scoping var readDict = new Dictionary(); readDict[Constants.Locks.Languages] = 1; - scope.ReadLocks[Guid.NewGuid()] = readDict; + scope.GetReadLocks()[Guid.NewGuid()] = readDict; Assert.Throws(() => scope.Dispose()); } finally { // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. - scope.ReadLocks?.Clear(); + scope.GetReadLocks()?.Clear(); scope.Dispose(); } } @@ -476,7 +500,7 @@ namespace Umbraco.Tests.Scoping public void Scope_Throws_If_WriteLocks_Not_Cleared() { var scopeprovider = GetScopeProvider(out var syntaxProviderMock); - var scope = (Scope) scopeprovider.CreateScope(); + var scope = (Scope)scopeprovider.CreateScope(); try { @@ -485,14 +509,14 @@ namespace Umbraco.Tests.Scoping var writeDict = new Dictionary(); writeDict[Constants.Locks.Languages] = 1; - scope.WriteLocks[Guid.NewGuid()] = writeDict; + scope.GetWriteLocks()[Guid.NewGuid()] = writeDict; Assert.Throws(() => scope.Dispose()); } finally { // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. - scope.WriteLocks?.Clear(); + scope.GetWriteLocks()?.Clear(); scope.Dispose(); } } @@ -504,8 +528,8 @@ namespace Umbraco.Tests.Scoping using (var scope = scopeProvider.CreateScope()) { - var realScope = (Scope) scope; - Assert.IsNull(realScope.WriteLocks); + var realScope = (Scope)scope; + Assert.IsNull(realScope.GetWriteLocks()); } } @@ -516,8 +540,8 @@ namespace Umbraco.Tests.Scoping using (var scope = scopeProvider.CreateScope()) { - var realScope = (Scope) scope; - Assert.IsNull(realScope.ReadLocks); + var realScope = (Scope)scope; + Assert.IsNull(realScope.GetReadLocks()); } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index cc1cfa6a1d..0740c947f6 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -242,7 +242,7 @@ namespace Umbraco.Tests.TestHelpers } fileSystems = fileSystems ?? new FileSystems(Current.Factory, logger); - var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, logger); + var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, logger, Mock.Of(x => x.LogUncompletedScopes == true)); return scopeProvider; } } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7bb90e9f93..3f4daa4df2 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -116,6 +116,7 @@ +