Merge remote-tracking branch 'origin/v12/dev' into v13/dev
This commit is contained in:
@@ -136,9 +136,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
"A transaction with minimum ReadCommitted isolation level is required.");
|
||||
}
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync($"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};");
|
||||
|
||||
var number = await dbContext.Database.ExecuteScalarAsync<int?>($"SELECT value FROM dbo.umbracoLock WITH (REPEATABLEREAD) WHERE id={LockId}");
|
||||
var number = await dbContext.Database.ExecuteScalarAsync<int?>($"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};SELECT value FROM dbo.umbracoLock WITH (REPEATABLEREAD) WHERE id={LockId}");
|
||||
|
||||
if (number == null)
|
||||
{
|
||||
|
||||
@@ -143,9 +143,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
|
||||
const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id";
|
||||
|
||||
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
|
||||
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
|
||||
|
||||
var i = db.ExecuteScalar<int?>(query, new { id = LockId });
|
||||
// execute the lock timeout query and the actual query in a single server roundtrip
|
||||
var i = db.ExecuteScalar<int?>($"{lockTimeoutQuery};{query}", new { id = LockId });
|
||||
|
||||
if (i == null)
|
||||
{
|
||||
@@ -178,9 +179,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
const string query =
|
||||
@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id";
|
||||
|
||||
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
|
||||
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
|
||||
|
||||
var i = db.Execute(query, new { id = LockId });
|
||||
// execute the lock timeout query and the actual query in a single server roundtrip
|
||||
var i = db.Execute($"{lockTimeoutQuery};{query}", new { id = LockId });
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
|
||||
@@ -162,7 +162,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
|
||||
try
|
||||
{
|
||||
var i = command.ExecuteNonQuery();
|
||||
var i = db.ExecuteNonQuery(command);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace Umbraco.Cms.Core.Cache;
|
||||
/// </summary>
|
||||
public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan _readLockTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan _writeLockTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -33,7 +36,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
Lazy<object?>? result;
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
if (_locker.TryEnterReadLock(_readLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when getting item");
|
||||
}
|
||||
result = MemoryCache.Get(key) as Lazy<object?>; // null if key not found
|
||||
}
|
||||
finally
|
||||
@@ -195,7 +201,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing item");
|
||||
}
|
||||
if (MemoryCache[key] == null)
|
||||
{
|
||||
return;
|
||||
@@ -223,8 +232,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
var isInterface = type.IsInterface;
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by type");
|
||||
}
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
.Where(x =>
|
||||
@@ -259,7 +270,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by generic type");
|
||||
}
|
||||
Type typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
@@ -296,7 +310,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing generic type with predicate");
|
||||
}
|
||||
Type typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
@@ -338,7 +355,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing with prefix");
|
||||
}
|
||||
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
@@ -365,7 +385,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cach when clearing by regex");
|
||||
}
|
||||
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
|
||||
@@ -13,8 +13,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
{
|
||||
private readonly IDistributedLockingMechanismFactory _distributedLockingMechanismFactory;
|
||||
private readonly ILogger<LockingMechanism> _logger;
|
||||
private readonly object _lockQueueLocker = new();
|
||||
private readonly object _dictionaryLocker = new();
|
||||
private readonly object _locker = new();
|
||||
private StackQueue<(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId)>? _queuedLocks;
|
||||
private HashSet<int>? _readLocks;
|
||||
private Dictionary<Guid, Dictionary<int, int>>? _readLocksDictionary;
|
||||
@@ -35,12 +34,12 @@ public class LockingMechanism : ILockingMechanism
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => LazyReadLockInner(instanceId, timeout, lockIds);
|
||||
public void ReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => EagerReadLockInner(instanceId, timeout, lockIds);
|
||||
|
||||
public void ReadLock(Guid instanceId, params int[] lockIds) => ReadLock(instanceId, null, lockIds);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => LazyWriteLockInner(instanceId, timeout, lockIds);
|
||||
public void WriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => EagerReadLockInner(instanceId, timeout, lockIds);
|
||||
|
||||
public void WriteLock(Guid instanceId, params int[] lockIds) => WriteLock(instanceId, null, lockIds);
|
||||
|
||||
@@ -64,7 +63,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
/// <param name="lockIds">Array of lock object identifiers.</param>
|
||||
private void EagerWriteLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds)
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
@@ -106,7 +105,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
/// <param name="lockIds">Array of lock object identifiers.</param>
|
||||
private void EagerReadLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds)
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
@@ -219,7 +218,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
|
||||
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan? timeout = null, params int[] lockIds)
|
||||
{
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (_queuedLocks == null)
|
||||
{
|
||||
@@ -239,7 +238,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
/// <param name="instanceId">Instance ID of the scope to clear.</param>
|
||||
public void ClearLocks(Guid instanceId)
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
_readLocksDictionary?.Remove(instanceId);
|
||||
_writeLocksDictionary?.Remove(instanceId);
|
||||
@@ -294,7 +293,7 @@ public class LockingMechanism : ILockingMechanism
|
||||
/// </summary>
|
||||
public void EnsureLocks(Guid scopeInstanceId)
|
||||
{
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (!(_queuedLocks?.Count > 0))
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Data.Common;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
|
||||
@@ -33,4 +34,7 @@ public interface IUmbracoDatabase : IDatabase
|
||||
bool IsUmbracoInstalled();
|
||||
|
||||
DatabaseSchemaResult ValidateSchema();
|
||||
|
||||
/// <returns>The number of rows affected.</returns>
|
||||
int ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
@@ -223,6 +223,14 @@ public class UmbracoDatabase : Database, IUmbracoDatabase
|
||||
return databaseSchemaValidationResult ?? new DatabaseSchemaResult();
|
||||
}
|
||||
|
||||
public int ExecuteNonQuery(DbCommand command)
|
||||
{
|
||||
OnExecutingCommand(command);
|
||||
var i = command.ExecuteNonQuery();
|
||||
OnExecutedCommand(command);
|
||||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if Umbraco database tables are detected to be installed
|
||||
/// </summary>
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache;
|
||||
/// </remarks>
|
||||
public class ContentStore
|
||||
{
|
||||
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
// TODO: collection trigger (ok for now)
|
||||
// see SnapDictionary notes
|
||||
private const long CollectMinGenDelta = 8;
|
||||
@@ -330,7 +332,12 @@ public class ContentStore
|
||||
throw new InvalidOperationException("Recursive locks not allowed");
|
||||
}
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
|
||||
|
||||
if (Monitor.IsEntered(_wlocko) is false)
|
||||
{
|
||||
throw new TimeoutException("Could not enter monitor before timeout in content store");
|
||||
}
|
||||
|
||||
lock (_rlocko)
|
||||
{
|
||||
|
||||
@@ -127,9 +127,25 @@ public class NuCacheContentService : RepositoryService, INuCacheContentService
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null)
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree);
|
||||
}
|
||||
|
||||
if (contentTypeIds is not null && contentTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
}
|
||||
|
||||
if (mediaTypeIds is not null && mediaTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
}
|
||||
|
||||
if (memberTypeIds is not null && memberTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
}
|
||||
|
||||
_repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class SnapDictionary<TKey, TValue>
|
||||
where TValue : class
|
||||
where TKey : notnull
|
||||
{
|
||||
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
// minGenDelta to be adjusted
|
||||
// we may want to throttle collects even if delta is reached
|
||||
// we may want to force collect if delta is not reached but very old
|
||||
@@ -198,7 +200,12 @@ public class SnapDictionary<TKey, TValue>
|
||||
throw new InvalidOperationException("Recursive locks not allowed");
|
||||
}
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
|
||||
|
||||
if (Monitor.IsEntered(_wlocko) is false)
|
||||
{
|
||||
throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary");
|
||||
}
|
||||
|
||||
lock (_rlocko)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
@@ -14,7 +12,6 @@ using Umbraco.Cms.Persistence.Sqlite.Interceptors;
|
||||
using Umbraco.Cms.Tests.Common.Attributes;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence;
|
||||
|
||||
@@ -125,6 +122,7 @@ public class LocksTests : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")]
|
||||
[Test]
|
||||
public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted()
|
||||
{
|
||||
@@ -154,6 +152,37 @@ public class LocksTests : UmbracoIntegrationTest
|
||||
Assert.AreEqual(0, sqlCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenNonEagerLocking_WhenDbIsAccessed_ThenSqlIsExecuted()
|
||||
{
|
||||
var sqlCount = 0;
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var db = ScopeAccessor.AmbientScope.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);
|
||||
|
||||
scope.Database.ExecuteScalar<int>("SELECT 1");
|
||||
|
||||
sqlCount = db.SqlCount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db.EnableSqlCount = false;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(2,sqlCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[LongRunning]
|
||||
public void ConcurrentWritersTest()
|
||||
|
||||
Reference in New Issue
Block a user