Merge branch 'v8/dev' into v8/contrib

This commit is contained in:
Sebastiaan Janssen
2021-03-12 09:30:30 +01:00
8 changed files with 646 additions and 38 deletions

View File

@@ -18,5 +18,5 @@ using System.Resources;
[assembly: AssemblyVersion("8.0.0")]
// these are FYI and changed automatically
[assembly: AssemblyFileVersion("8.12.0")]
[assembly: AssemblyInformationalVersion("8.12.0")]
[assembly: AssemblyFileVersion("8.12.1")]
[assembly: AssemblyInformationalVersion("8.12.1")]

View File

@@ -409,26 +409,34 @@ namespace Umbraco.Core.Configuration
{
if (_sqlWriteLockTimeOut != default) return _sqlWriteLockTimeOut;
var timeOut = 5000; // 5 seconds
var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut];
if(int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut))
{
// Only apply this setting if it's not excessively high or low
const int minimumTimeOut = 100;
const int maximumTimeOut = 20000;
if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds
{
timeOut = configuredTimeOut;
}
else
{
Current.Logger.Warn<GlobalSettings>($"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}");
}
}
var timeOut = GetSqlWriteLockTimeoutFromConfigFile(Current.Logger);
_sqlWriteLockTimeOut = timeOut;
return _sqlWriteLockTimeOut;
}
}
internal static int GetSqlWriteLockTimeoutFromConfigFile(ILogger logger)
{
var timeOut = 5000; // 5 seconds
var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut];
if (int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut))
{
// Only apply this setting if it's not excessively high or low
const int minimumTimeOut = 100;
const int maximumTimeOut = 20000;
if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds
{
timeOut = configuredTimeOut;
}
else
{
logger.Warn<GlobalSettings>(
$"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}");
}
}
return timeOut;
}
}
}

View File

@@ -1,5 +1,6 @@
using NPoco;
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
@@ -7,6 +8,7 @@ using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
@@ -18,6 +20,7 @@ namespace Umbraco.Core.Runtime
{
internal class SqlMainDomLock : IMainDomLock
{
private readonly TimeSpan _lockTimeout;
private string _lockId;
private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom";
private const string UpdatedSuffix = "_updated";
@@ -40,6 +43,8 @@ namespace Umbraco.Core.Runtime
Constants.System.UmbracoConnectionName,
_logger,
new Lazy<IMapperCollection>(() => new MapperCollection(Enumerable.Empty<BaseMapper>())));
_lockTimeout = TimeSpan.FromMilliseconds(GlobalSettings.GetSqlWriteLockTimeoutFromConfigFile(logger));
}
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
@@ -198,7 +203,7 @@ namespace Umbraco.Core.Runtime
db.BeginTransaction(IsolationLevel.ReadCommitted);
// get a read lock
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom);
if (!IsMainDomValue(_lockId, db))
{
@@ -284,7 +289,7 @@ namespace Umbraco.Core.Runtime
{
transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
// get a read lock
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom);
// the row
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
@@ -296,7 +301,7 @@ namespace Umbraco.Core.Runtime
// which indicates that we
// can acquire it and it has shutdown.
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId, db);
@@ -355,7 +360,7 @@ namespace Umbraco.Core.Runtime
{
transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId, db);
@@ -438,7 +443,7 @@ namespace Umbraco.Core.Runtime
db.BeginTransaction(IsolationLevel.ReadCommitted);
// get a write lock
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom);
// When we are disposed, it means we have released the MainDom lock
// and called all MainDom release callbacks, in this case

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Data;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
@@ -34,6 +35,13 @@ namespace Umbraco.Core.Scoping
private ICompletable _fscope;
private IEventDispatcher _eventDispatcher;
private object _dictionaryLocker;
// ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain
// If we're a child scope it's those that we have requested.
internal readonly Dictionary<int, int> ReadLocks;
internal readonly Dictionary<int, int> WriteLocks;
// initializes a new scope
private Scope(ScopeProvider scopeProvider,
ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable,
@@ -58,6 +66,10 @@ namespace Umbraco.Core.Scoping
Detachable = detachable;
_dictionaryLocker = new object();
ReadLocks = new Dictionary<int, int>();
WriteLocks = new Dictionary<int, int>();
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8));
@@ -348,6 +360,23 @@ namespace Umbraco.Core.Scoping
#endif
}
// Decrement the lock counters on the parent if any.
if (ParentScope != null)
{
lock (_dictionaryLocker)
{
foreach (var readLockPair in ReadLocks)
{
DecrementReadLock(readLockPair.Key, readLockPair.Value);
}
foreach (var writeLockPair in WriteLocks)
{
DecrementWriteLock(writeLockPair.Key, writeLockPair.Value);
}
}
}
var parent = ParentScope;
_scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects
@@ -486,31 +515,259 @@ namespace Umbraco.Core.Scoping
private static bool LogUncompletedScopes => (_logUncompletedScopes
?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value;
/// <summary>
/// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold
/// </summary>
/// <param name="lockId">Lock object identifier to decrement</param>
/// <param name="amountToDecrement">Amount to decrement the lock count with</param>
public void DecrementReadLock(int lockId, int amountToDecrement)
{
// If we aren't the outermost scope, pass it on to the parent.
if (ParentScope != null)
{
ParentScope.DecrementReadLock(lockId, amountToDecrement);
return;
}
lock (_dictionaryLocker)
{
ReadLocks[lockId] -= amountToDecrement;
}
}
/// <summary>
/// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold.
/// </summary>
/// <param name="lockId">Lock object identifier to decrement.</param>
/// <param name="amountToDecrement">Amount to decrement the lock count with</param>
public void DecrementWriteLock(int lockId, int amountToDecrement)
{
// If we aren't the outermost scope, pass it on to the parent.
if (ParentScope != null)
{
ParentScope.DecrementWriteLock(lockId, amountToDecrement);
return;
}
lock (_dictionaryLocker)
{
WriteLocks[lockId] -= amountToDecrement;
}
}
/// <summary>
/// Increment the count of the read locks we've requested
/// </summary>
/// <remarks>
/// This should only be done on child scopes since it's then used to decrement the count later.
/// </remarks>
/// <param name="lockIds"></param>
private void IncrementRequestedReadLock(params int[] lockIds)
{
// We need to keep track of what lockIds we have requested locks for to be able to decrement them.
if (ParentScope != null)
{
foreach (var lockId in lockIds)
{
lock (_dictionaryLocker)
{
if (ReadLocks.ContainsKey(lockId))
{
ReadLocks[lockId] += 1;
}
else
{
ReadLocks[lockId] = 1;
}
}
}
}
}
/// <summary>
/// Increment the count of the write locks we've requested
/// </summary>
/// <remarks>
/// This should only be done on child scopes since it's then used to decrement the count later.
/// </remarks>
/// <param name="lockIds"></param>
private void IncrementRequestedWriteLock(params int[] lockIds)
{
// We need to keep track of what lockIds we have requested locks for to be able to decrement them.
if (ParentScope != null)
{
foreach (var lockId in lockIds)
{
lock (_dictionaryLocker)
{
if (WriteLocks.ContainsKey(lockId))
{
WriteLocks[lockId] += 1;
}
else
{
WriteLocks[lockId] = 1;
}
}
}
}
}
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
public void ReadLock(params int[] lockIds)
{
IncrementRequestedReadLock(lockIds);
ReadLockInner(null, lockIds);
}
/// <inheritdoc />
public void ReadLock(TimeSpan timeout, int lockId)
{
var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2;
if (syntax2 == null)
{
throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}");
}
syntax2.ReadLock(Database, timeout, lockId);
IncrementRequestedReadLock(lockId);
ReadLockInner(timeout, lockId);
}
/// <inheritdoc />
public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds);
public void WriteLock(params int[] lockIds)
{
IncrementRequestedWriteLock(lockIds);
WriteLockInner(null, lockIds);
}
/// <inheritdoc />
public void WriteLock(TimeSpan timeout, int lockId)
{
IncrementRequestedWriteLock(lockId);
WriteLockInner(timeout, lockId);
}
/// <summary>
/// Handles acquiring a read lock, will delegate it to the parent if there are any.
/// </summary>
/// <param name="timeout">Optional database timeout in milliseconds.</param>
/// <param name="lockIds">Array of lock object identifiers.</param>
internal void ReadLockInner(TimeSpan? timeout = null, params int[] lockIds)
{
if (ParentScope != null)
{
// Delegate acquiring the lock to the parent if any.
ParentScope.ReadLockInner(timeout, lockIds);
return;
}
// If we are the parent, then handle the lock request.
foreach (var lockId in lockIds)
{
lock (_dictionaryLocker)
{
// Only acquire the lock if we haven't done so yet.
if (!ReadLocks.ContainsKey(lockId))
{
if (timeout is null)
{
// We want a lock with a custom timeout
ObtainReadLock(lockId);
}
else
{
// We just want an ordinary lock.
ObtainTimoutReadLock(lockId, timeout.Value);
}
// Add the lockId as a key to the dict.
ReadLocks[lockId] = 0;
}
ReadLocks[lockId] += 1;
}
}
}
/// <summary>
/// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any.
/// </summary>
/// <param name="timeout">Optional database timeout in milliseconds.</param>
/// <param name="lockIds">Array of lock object identifiers.</param>
internal void WriteLockInner(TimeSpan? timeout = null, params int[] lockIds)
{
if (ParentScope != null)
{
// If we have a parent we delegate lock creation to parent.
ParentScope.WriteLockInner(timeout, lockIds);
return;
}
foreach (var lockId in lockIds)
{
lock (_dictionaryLocker)
{
// Only acquire lock if we haven't yet (WriteLocks not containing the key)
if (!WriteLocks.ContainsKey(lockId))
{
if (timeout is null)
{
ObtainWriteLock(lockId);
}
else
{
ObtainTimeoutWriteLock(lockId, timeout.Value);
}
// Add the lockId as a key to the dict.
WriteLocks[lockId] = 0;
}
// Increment count of the lock by 1.
WriteLocks[lockId] += 1;
}
}
}
/// <summary>
/// Obtains an ordinary read lock.
/// </summary>
/// <param name="lockId">Lock object identifier to lock.</param>
private void ObtainReadLock(int lockId)
{
Database.SqlContext.SqlSyntax.ReadLock(Database, lockId);
}
/// <summary>
/// Obtains a read lock with a custom timeout.
/// </summary>
/// <param name="lockId">Lock object identifier to lock.</param>
/// <param name="timeout">TimeSpan specifying the timout period.</param>
private void ObtainTimoutReadLock(int lockId, TimeSpan timeout)
{
var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2;
if (syntax2 == null)
{
throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}");
}
syntax2.ReadLock(Database, timeout, lockId);
}
/// <summary>
/// Obtains an ordinary write lock.
/// </summary>
/// <param name="lockId">Lock object identifier to lock.</param>
private void ObtainWriteLock(int lockId)
{
Database.SqlContext.SqlSyntax.WriteLock(Database, lockId);
}
/// <summary>
/// Obtains a write lock with a custom timeout.
/// </summary>
/// <param name="lockId">Lock object identifier to lock.</param>
/// <param name="timeout">TimeSpan specifying the timout period.</param>
private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout)
{
var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2;
if (syntax2 == null)
{
throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}");
}
syntax2.WriteLock(Database, timeout, lockId);
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Linq;
using System.Threading;
@@ -280,7 +279,7 @@ namespace Umbraco.Tests.Persistence
[Test]
public void Throws_When_Lock_Timeout_Is_Exceeded()
{
{
var t1 = Task.Run(() =>
{
using (var scope = ScopeProvider.CreateScope())
@@ -288,7 +287,7 @@ namespace Umbraco.Tests.Persistence
var realScope = (Scope)scope;
Console.WriteLine("Write lock A");
// This will acquire right away
// This will acquire right away
realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree);
Thread.Sleep(6000); // Wait longer than the Read Lock B timeout
scope.Complete();
@@ -349,7 +348,7 @@ namespace Umbraco.Tests.Persistence
var realScope = (Scope)scope;
Console.WriteLine("Write lock A");
// This will acquire right away
// This will acquire right away
realScope.WriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree);
Thread.Sleep(4000); // Wait less than the Read Lock B timeout
scope.Complete();
@@ -377,7 +376,7 @@ namespace Umbraco.Tests.Persistence
scope.Complete();
Interlocked.Increment(ref locksCompleted);
Console.WriteLine("Finished Read lock B");
}
}
});
var t3 = Task.Run(() =>

View File

@@ -0,0 +1,338 @@
using System;
using Moq;
using NPoco;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Scoping;
namespace Umbraco.Tests.Scoping
{
[TestFixture]
public class ScopeUnitTests
{
/// <summary>
/// Creates a ScopeProvider with mocked internals.
/// </summary>
/// <param name="syntaxProviderMock">The mock of the ISqlSyntaxProvider2, used to count method calls.</param>
/// <returns></returns>
private ScopeProvider GetScopeProvider(out Mock<ISqlSyntaxProvider2> syntaxProviderMock)
{
var logger = Mock.Of<ILogger>();
var fac = Mock.Of<IFactory>();
var fileSystem = new FileSystems(fac, logger);
var databaseFactory = new Mock<IUmbracoDatabaseFactory>();
var database = new Mock<IUmbracoDatabase>();
var sqlContext = new Mock<ISqlContext>();
syntaxProviderMock = new Mock<ISqlSyntaxProvider2>();
// Setup mock of database factory to return mock of database.
databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object);
// Setup mock of database to return mock of sql SqlContext
database.Setup(x => x.SqlContext).Returns(sqlContext.Object);
// Setup mock of ISqlContext to return syntaxProviderMock
sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object);
return new ScopeProvider(databaseFactory.Object, fileSystem, logger);
}
[Test]
public void WriteLock_Acquired_Only_Once_Per_Key()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerScope = scopeProvider.CreateScope())
{
outerScope.WriteLock(Constants.Locks.Domains);
outerScope.WriteLock(Constants.Locks.Languages);
using (var innerScope1 = scopeProvider.CreateScope())
{
innerScope1.WriteLock(Constants.Locks.Domains);
innerScope1.WriteLock(Constants.Locks.Languages);
using (var innerScope2 = scopeProvider.CreateScope())
{
innerScope2.WriteLock(Constants.Locks.Domains);
innerScope2.WriteLock(Constants.Locks.Languages);
innerScope2.Complete();
}
innerScope1.Complete();
}
outerScope.Complete();
}
syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny<IDatabase>(), Constants.Locks.Domains), Times.Once);
syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny<IDatabase>(), Constants.Locks.Languages), Times.Once);
}
[Test]
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())
{
var realScope = (Scope) outerScope;
realScope.WriteLock(timeout, Constants.Locks.Domains);
realScope.WriteLock(timeout, Constants.Locks.Languages);
using (var innerScope1 = scopeProvider.CreateScope())
{
var realInnerScope1 = (Scope) outerScope;
realInnerScope1.WriteLock(timeout, Constants.Locks.Domains);
realInnerScope1.WriteLock(timeout, Constants.Locks.Languages);
using (var innerScope2 = scopeProvider.CreateScope())
{
var realInnerScope2 = (Scope) innerScope2;
realInnerScope2.WriteLock(timeout, Constants.Locks.Domains);
realInnerScope2.WriteLock(timeout, Constants.Locks.Languages);
innerScope2.Complete();
}
innerScope1.Complete();
}
outerScope.Complete();
}
syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny<IDatabase>(), timeout, Constants.Locks.Domains), Times.Once);
syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny<IDatabase>(), timeout, Constants.Locks.Languages), Times.Once);
}
[Test]
public void ReadLock_Acquired_Only_Once_Per_Key()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerScope = scopeProvider.CreateScope())
{
outerScope.ReadLock(Constants.Locks.Domains);
outerScope.ReadLock(Constants.Locks.Languages);
using (var innerScope1 = scopeProvider.CreateScope())
{
innerScope1.ReadLock(Constants.Locks.Domains);
innerScope1.ReadLock(Constants.Locks.Languages);
using (var innerScope2 = scopeProvider.CreateScope())
{
innerScope2.ReadLock(Constants.Locks.Domains);
innerScope2.ReadLock(Constants.Locks.Languages);
innerScope2.Complete();
}
innerScope1.Complete();
}
outerScope.Complete();
}
syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny<IDatabase>(), Constants.Locks.Domains), Times.Once);
syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny<IDatabase>(), Constants.Locks.Languages), Times.Once);
}
[Test]
public void ReadLock_With_Timeout_Acquired_Only_Once_Per_Key()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
var timeOut = TimeSpan.FromMilliseconds(10000);
using (var outerScope = scopeProvider.CreateScope())
{
var realOuterScope = (Scope) outerScope;
realOuterScope.ReadLock(timeOut, Constants.Locks.Domains);
realOuterScope.ReadLock(timeOut, Constants.Locks.Languages);
using (var innerScope1 = scopeProvider.CreateScope())
{
var realInnerScope1 = (Scope) innerScope1;
realInnerScope1.ReadLock(timeOut, Constants.Locks.Domains);
realInnerScope1.ReadLock(timeOut, Constants.Locks.Languages);
using (var innerScope2 = scopeProvider.CreateScope())
{
var realInnerScope2 = (Scope) innerScope2;
realInnerScope2.ReadLock(timeOut, Constants.Locks.Domains);
realInnerScope2.ReadLock(timeOut, Constants.Locks.Languages);
innerScope2.Complete();
}
innerScope1.Complete();
}
outerScope.Complete();
}
syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny<IDatabase>(), timeOut, Constants.Locks.Domains), Times.Once);
syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny<IDatabase>(), timeOut, Constants.Locks.Languages), Times.Once);
}
[Test]
public void WriteLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerscope = scopeProvider.CreateScope())
{
var realOuterScope = (Scope) outerscope;
outerscope.WriteLock(Constants.Locks.ContentTree);
outerscope.WriteLock(Constants.Locks.ContentTree);
Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]);
using (var innerScope = scopeProvider.CreateScope())
{
innerScope.WriteLock(Constants.Locks.ContentTree);
innerScope.WriteLock(Constants.Locks.ContentTree);
Assert.AreEqual(4, realOuterScope.WriteLocks[Constants.Locks.ContentTree]);
innerScope.WriteLock(Constants.Locks.Languages);
innerScope.WriteLock(Constants.Locks.Languages);
Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.Languages]);
innerScope.Complete();
}
Assert.AreEqual(0, realOuterScope.WriteLocks[Constants.Locks.Languages]);
Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]);
outerscope.Complete();
}
}
[Test]
public void ReadLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerscope = scopeProvider.CreateScope())
{
var realOuterScope = (Scope) outerscope;
outerscope.ReadLock(Constants.Locks.ContentTree);
outerscope.ReadLock(Constants.Locks.ContentTree);
Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]);
using (var innerScope = scopeProvider.CreateScope())
{
innerScope.ReadLock(Constants.Locks.ContentTree);
innerScope.ReadLock(Constants.Locks.ContentTree);
Assert.AreEqual(4, realOuterScope.ReadLocks[Constants.Locks.ContentTree]);
innerScope.ReadLock(Constants.Locks.Languages);
innerScope.ReadLock(Constants.Locks.Languages);
Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.Languages]);
innerScope.Complete();
}
Assert.AreEqual(0, realOuterScope.ReadLocks[Constants.Locks.Languages]);
Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]);
outerscope.Complete();
}
}
[Test]
public void Nested_Scopes_WriteLocks_Count_Correctly()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerScope = scopeProvider.CreateScope())
{
var parentScope = (Scope) outerScope;
outerScope.WriteLock(Constants.Locks.ContentTree);
outerScope.WriteLock(Constants.Locks.ContentTypes);
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
using (var innerScope1 = scopeProvider.CreateScope())
{
innerScope1.WriteLock(Constants.Locks.ContentTree);
innerScope1.WriteLock(Constants.Locks.ContentTypes);
innerScope1.WriteLock(Constants.Locks.Languages);
Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}");
using (var innerScope2 = scopeProvider.CreateScope())
{
innerScope2.WriteLock(Constants.Locks.ContentTree);
innerScope2.WriteLock(Constants.Locks.MediaTypes);
Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}");
innerScope2.Complete();
}
Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}");
innerScope1.Complete();
}
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}");
outerScope.Complete();
}
}
[Test]
public void Nested_Scopes_ReadLocks_Count_Correctly()
{
var scopeProvider = GetScopeProvider(out var syntaxProviderMock);
using (var outerScope = scopeProvider.CreateScope())
{
var parentScope = (Scope) outerScope;
outerScope.ReadLock(Constants.Locks.ContentTree);
outerScope.ReadLock(Constants.Locks.ContentTypes);
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
using (var innserScope1 = scopeProvider.CreateScope())
{
innserScope1.ReadLock(Constants.Locks.ContentTree);
innserScope1.ReadLock(Constants.Locks.ContentTypes);
innserScope1.ReadLock(Constants.Locks.Languages);
Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}");
using (var innerScope2 = scopeProvider.CreateScope())
{
innerScope2.ReadLock(Constants.Locks.ContentTree);
innerScope2.ReadLock(Constants.Locks.MediaTypes);
Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}");
innerScope2.Complete();
}
Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}");
innserScope1.Complete();
}
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}");
Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}");
Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}");
Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}");
outerScope.Complete();
}
}
}
}

View File

@@ -163,6 +163,7 @@
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Scoping\ScopeUnitTests.cs" />
<Compile Include="Services\AmbiguousEventTests.cs" />
<Compile Include="Services\ContentServiceEventTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />

View File

@@ -347,9 +347,9 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>8120</DevelopmentServerPort>
<DevelopmentServerPort>8121</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8120</IISUrl>
<IISUrl>http://localhost:8121</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>