Entity Framework Core Support (#14109)

* Add UmbracoEFCore project

* Add EFCore composer

* Add Locking Mechanisms

* Add scope interfaces

* Add excecute scalar extension method

* fix up query in locking mechanism

* Add scoping

* Add scoping

* Add test DbContext classes

* add locking test of EFCore

* Creat ScopedFileSystemsTests

* Add EFCoreScopeInfrastructureScopeLockTests

* Add EFCoreScopeInfrastructureScopeTests

* Add EFCoreScopeNotificationsTest.cs

* Add EFCoreScopeTest.cs

* Remake AddUmbracoEFCoreContext to use connection string

* Remove unused code from extension method

* Reference EFCore reference to Cms.csproj

* Remove unused parameter

* Dont have default implementation, breaking change instead

* Add compatability suppression file

* Updated EFCore packages

* Use timespan for timeout

* Allow overriding default EF Core actions

* Option lifetime needs to be singleton

* Use given timeout in database call

* dont use timespan.zero, use null instead

* Use variable timeout

* Update test to use locking mechanism

* Remove unneccesary duplicate code

* Change to catch proper exception number

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Nikolaj Geisle
2023-05-12 09:25:19 +02:00
committed by GitHub
parent 36fe2f7e49
commit 487e85cacd
35 changed files with 3930 additions and 681 deletions

View File

@@ -29,4 +29,11 @@
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Umbraco.Cms.Core.Scoping.ICoreScope.Locks</Target>
<Left>lib/net7.0/Umbraco.Core.dll</Left>
<Right>lib/net7.0/Umbraco.Core.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>

View File

@@ -0,0 +1,272 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Core.Scoping;
public class CoreScope : ICoreScope
{
protected bool? Completed;
private ICompletable? _scopedFileSystem;
private IScopedNotificationPublisher? _notificationPublisher;
private IsolatedCaches? _isolatedCaches;
private ICoreScope? _parentScope;
private readonly RepositoryCacheMode _repositoryCacheMode;
private readonly bool? _shouldScopeFileSystems;
private readonly IEventAggregator _eventAggregator;
private bool _disposed;
protected CoreScope(
IDistributedLockingMechanismFactory distributedLockingMechanismFactory,
ILoggerFactory loggerFactory,
FileSystems scopedFileSystem,
IEventAggregator eventAggregator,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
bool? shouldScopeFileSystems = null,
IScopedNotificationPublisher? notificationPublisher = null)
{
_eventAggregator = eventAggregator;
InstanceId = Guid.NewGuid();
CreatedThreadId = Environment.CurrentManagedThreadId;
Locks = ParentScope is null
? new LockingMechanism(distributedLockingMechanismFactory, loggerFactory.CreateLogger<LockingMechanism>())
: ResolveLockingMechanism();
_repositoryCacheMode = repositoryCacheMode;
_shouldScopeFileSystems = shouldScopeFileSystems;
_notificationPublisher = notificationPublisher;
if (_shouldScopeFileSystems is true)
{
_scopedFileSystem = scopedFileSystem.Shadow();
}
}
protected CoreScope(
ICoreScope? parentScope,
IDistributedLockingMechanismFactory distributedLockingMechanismFactory,
ILoggerFactory loggerFactory,
FileSystems scopedFileSystem,
IEventAggregator eventAggregator,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
bool? shouldScopeFileSystems = null,
IScopedNotificationPublisher? notificationPublisher = null)
{
_eventAggregator = eventAggregator;
InstanceId = Guid.NewGuid();
CreatedThreadId = Environment.CurrentManagedThreadId;
_repositoryCacheMode = repositoryCacheMode;
_shouldScopeFileSystems = shouldScopeFileSystems;
_notificationPublisher = notificationPublisher;
if (parentScope is null)
{
Locks = new LockingMechanism(distributedLockingMechanismFactory, loggerFactory.CreateLogger<LockingMechanism>());
if (_shouldScopeFileSystems is true)
{
_scopedFileSystem = scopedFileSystem.Shadow();
}
return;
}
Locks = parentScope.Locks;
// cannot specify a different mode!
// TODO: means that it's OK to go from L2 to None for reading purposes, but writing would be BAD!
// this is for XmlStore that wants to bypass caches when rebuilding XML (same for NuCache)
if (repositoryCacheMode != RepositoryCacheMode.Unspecified &&
parentScope.RepositoryCacheMode > repositoryCacheMode)
{
throw new ArgumentException(
$"Value '{repositoryCacheMode}' cannot be lower than parent value '{parentScope.RepositoryCacheMode}'.", nameof(repositoryCacheMode));
}
// Only the outermost scope can specify the notification publisher
if (_notificationPublisher != null)
{
throw new ArgumentException("Value cannot be specified on nested scope.", nameof(_notificationPublisher));
}
_parentScope = parentScope;
// cannot specify a different fs scope!
// can be 'true' only on outer scope (and false does not make much sense)
if (_shouldScopeFileSystems != null && ParentScope?._shouldScopeFileSystems != _shouldScopeFileSystems)
{
throw new ArgumentException(
$"Value '{_shouldScopeFileSystems.Value}' be different from parent value '{ParentScope?._shouldScopeFileSystems}'.",
nameof(_shouldScopeFileSystems));
}
}
private CoreScope? ParentScope => (CoreScope?)_parentScope;
public int Depth
{
get
{
if (ParentScope == null)
{
return 0;
}
return ParentScope.Depth + 1;
}
}
public Guid InstanceId { get; }
public int CreatedThreadId { get; }
public ILockingMechanism Locks { get; }
public IScopedNotificationPublisher Notifications
{
get
{
EnsureNotDisposed();
if (ParentScope != null)
{
return ParentScope.Notifications;
}
return _notificationPublisher ??= new ScopedNotificationPublisher(_eventAggregator);
}
}
public RepositoryCacheMode RepositoryCacheMode
{
get
{
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified)
{
return _repositoryCacheMode;
}
return ParentScope?.RepositoryCacheMode ?? RepositoryCacheMode.Default;
}
}
public IsolatedCaches IsolatedCaches
{
get
{
if (ParentScope != null)
{
return ParentScope.IsolatedCaches;
}
return _isolatedCaches ??= new IsolatedCaches(_ => new DeepCloneAppCache(new ObjectCacheAppCache()));
}
}
public bool ScopedFileSystems
{
get
{
if (ParentScope != null)
{
return ParentScope.ScopedFileSystems;
}
return _scopedFileSystem != null;
}
}
/// <summary>
/// Completes a scope
/// </summary>
/// <returns>A value indicating whether the scope is completed or not.</returns>
public bool Complete()
{
if (Completed.HasValue == false)
{
Completed = true;
}
return Completed.Value;
}
public void ReadLock(params int[] lockIds) => Locks.ReadLock(InstanceId, null, lockIds);
public void WriteLock(params int[] lockIds) => Locks.WriteLock(InstanceId, null, lockIds);
public void WriteLock(TimeSpan timeout, int lockId) => Locks.ReadLock(InstanceId, timeout, lockId);
public void ReadLock(TimeSpan timeout, int lockId) => Locks.WriteLock(InstanceId, timeout, lockId);
public void EagerWriteLock(params int[] lockIds) => Locks.EagerWriteLock(InstanceId, null, lockIds);
public void EagerWriteLock(TimeSpan timeout, int lockId) => Locks.EagerWriteLock(InstanceId, timeout, lockId);
public void EagerReadLock(TimeSpan timeout, int lockId) => Locks.EagerReadLock(InstanceId, timeout, lockId);
public void EagerReadLock(params int[] lockIds) => Locks.EagerReadLock(InstanceId, TimeSpan.Zero, lockIds);
public virtual void Dispose()
{
if (ParentScope is null)
{
HandleScopedFileSystems();
HandleScopedNotifications();
}
else
{
ParentScope.ChildCompleted(Completed);
}
_disposed = true;
}
protected void ChildCompleted(bool? completed)
{
// if child did not complete we cannot complete
if (completed.HasValue == false || completed.Value == false)
{
Completed = false;
}
}
private void HandleScopedFileSystems()
{
if (_shouldScopeFileSystems == true)
{
if (Completed.HasValue && Completed.Value)
{
_scopedFileSystem?.Complete();
}
_scopedFileSystem?.Dispose();
_scopedFileSystem = null;
}
}
protected void SetParentScope(ICoreScope coreScope)
{
_parentScope = coreScope;
}
private void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value);
private void EnsureNotDisposed()
{
// We can't be disposed
if (_disposed)
{
throw new ObjectDisposedException($"The {nameof(CoreScope)} with ID ({InstanceId}) is already disposed");
}
// And neither can our ancestors if we're trying to be disposed since
// a child must always be disposed before it's parent.
// This is a safety check, it's actually not entirely possible that a parent can be
// disposed before the child since that will end up with a "not the Ambient" exception.
ParentScope?.EnsureNotDisposed();
}
private ILockingMechanism ResolveLockingMechanism() =>
ParentScope is not null ? ParentScope.ResolveLockingMechanism() : Locks;
}

View File

@@ -16,6 +16,8 @@ public interface ICoreScope : IDisposable, IInstanceIdentifiable
/// </remarks>
public int Depth => -1;
public ILockingMechanism Locks { get; }
/// <summary>
/// Gets the scope notification publisher
/// </summary>

View File

@@ -0,0 +1,58 @@
namespace Umbraco.Cms.Core.Scoping;
public interface ILockingMechanism : IDisposable
{
/// <summary>
/// Read-locks some lock objects lazily.
/// </summary>
/// <param name="instanceId">Instance id of the scope who is requesting the lock</param>
/// <param name="lockIds">Array of lock object identifiers.</param>
void ReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
void ReadLock(Guid instanceId, params int[] lockIds);
/// <summary>
/// Write-locks some lock objects lazily.
/// </summary>
/// <param name="instanceId">Instance id of the scope who is requesting the lock</param>
/// <param name="lockIds">Array of object identifiers.</param>
void WriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
void WriteLock(Guid instanceId, params int[] lockIds);
/// <summary>
/// Eagerly acquires a read-lock
/// </summary>
/// <param name="instanceId"></param>
/// <param name="lockIds"></param>
void EagerReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
void EagerReadLock(Guid instanceId, params int[] lockIds);
/// <summary>
/// Eagerly acquires a write-lock
/// </summary>
/// <param name="instanceId"></param>
/// <param name="lockIds"></param>
void EagerWriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
void EagerWriteLock(Guid instanceId, params int[] lockIds);
/// <summary>
/// Clears all the locks held
/// </summary>
/// <param name="instanceId"></param>
void ClearLocks(Guid instanceId);
/// <summary>
/// Acquires all the non-eagerly requested locks.
/// </summary>
/// <param name="scopeInstanceId"></param>
void EnsureLocks(Guid scopeInstanceId);
void EnsureLocksCleared(Guid instanceId);
Dictionary<Guid, Dictionary<int, int>>? GetReadLocks();
Dictionary<Guid, Dictionary<int, int>>? GetWriteLocks();
}

View File

@@ -0,0 +1,433 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Scoping;
/// <summary>
/// Mechanism for handling read and write locks
/// </summary>
public class LockingMechanism : ILockingMechanism
{
private readonly IDistributedLockingMechanismFactory _distributedLockingMechanismFactory;
private readonly ILogger<LockingMechanism> _logger;
private readonly object _lockQueueLocker = new();
private readonly object _dictionaryLocker = new();
private StackQueue<(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId)>? _queuedLocks;
private HashSet<int>? _readLocks;
private Dictionary<Guid, Dictionary<int, int>>? _readLocksDictionary;
private HashSet<int>? _writeLocks;
private Dictionary<Guid, Dictionary<int, int>>? _writeLocksDictionary;
private Queue<IDistributedLock>? _acquiredLocks;
/// <summary>
/// Constructs an instance of LockingMechanism
/// </summary>
/// <param name="distributedLockingMechanismFactory"></param>
/// <param name="logger"></param>
public LockingMechanism(IDistributedLockingMechanismFactory distributedLockingMechanismFactory, ILogger<LockingMechanism> logger)
{
_distributedLockingMechanismFactory = distributedLockingMechanismFactory;
_logger = logger;
_acquiredLocks = new Queue<IDistributedLock>();
}
/// <inheritdoc />
public void ReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => LazyReadLockInner(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, params int[] lockIds) => WriteLock(instanceId, null, lockIds);
/// <inheritdoc />
public void EagerReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => EagerReadLockInner(instanceId, timeout, lockIds);
public void EagerReadLock(Guid instanceId, params int[] lockIds) =>
EagerReadLock(instanceId, null, lockIds);
/// <inheritdoc />
public void EagerWriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) => EagerWriteLockInner(instanceId, timeout, lockIds);
public void EagerWriteLock(Guid instanceId, params int[] lockIds) =>
EagerWriteLock(instanceId, null, lockIds);
/// <summary>
/// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any.
/// </summary>
/// <param name="instanceId">Instance ID of the requesting scope.</param>
/// <param name="timeout">Optional database timeout in milliseconds.</param>
/// <param name="lockIds">Array of lock object identifiers.</param>
private void EagerWriteLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds)
{
lock (_dictionaryLocker)
{
foreach (var lockId in lockIds)
{
IncrementLock(lockId, instanceId, ref _writeLocksDictionary);
// We are the outermost scope, handle the lock request.
LockInner(
instanceId,
ref _writeLocksDictionary!,
ref _writeLocks!,
ObtainWriteLock,
timeout,
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 ObtainWriteLock(int lockId, TimeSpan? timeout)
{
if (_acquiredLocks == null)
{
throw new InvalidOperationException(
$"Cannot obtain a write lock as the {nameof(_acquiredLocks)} queue is null.");
}
_acquiredLocks.Enqueue(_distributedLockingMechanismFactory.DistributedLockingMechanism.WriteLock(lockId, timeout));
}
/// <summary>
/// Handles acquiring a read lock, will delegate it to the parent if there are any.
/// </summary>
/// <param name="instanceId">The id of the scope requesting the lock.</param>
/// <param name="timeout">Optional database timeout in milliseconds.</param>
/// <param name="lockIds">Array of lock object identifiers.</param>
private void EagerReadLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds)
{
lock (_dictionaryLocker)
{
foreach (var lockId in lockIds)
{
IncrementLock(lockId, instanceId, ref _readLocksDictionary);
// We are the outermost scope, handle the lock request.
LockInner(
instanceId,
ref _readLocksDictionary!,
ref _readLocks!,
ObtainReadLock,
timeout,
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 ObtainReadLock(int lockId, TimeSpan? timeout)
{
if (_acquiredLocks == null)
{
throw new InvalidOperationException(
$"Cannot obtain a read lock as the {nameof(_acquiredLocks)} queue is null.");
}
_acquiredLocks.Enqueue(
_distributedLockingMechanismFactory.DistributedLockingMechanism.ReadLock(lockId, timeout));
}
/// <summary>
/// Handles acquiring a lock, this should only be called from the outermost scope.
/// </summary>
/// <param name="instanceId">Instance ID of the scope requesting the lock.</param>
/// <param name="locks">Reference to the applicable locks dictionary (ReadLocks or WriteLocks).</param>
/// <param name="locksSet">Reference to the applicable locks hashset (_readLocks or _writeLocks).</param>
/// <param name="obtainLock">Delegate used to request the lock from the locking mechanism.</param>
/// <param name="timeout">Optional timeout parameter to specify a timeout.</param>
/// <param name="lockId">Lock identifier.</param>
private void LockInner(
Guid instanceId,
ref Dictionary<Guid, Dictionary<int, int>> locks,
ref HashSet<int>? locksSet,
Action<int, TimeSpan?> obtainLock,
TimeSpan? timeout,
int lockId)
{
locksSet ??= new HashSet<int>();
// Only acquire the lock if we haven't done so yet.
if (locksSet.Contains(lockId))
{
return;
}
locksSet.Add(lockId);
try
{
obtainLock(lockId, timeout);
}
catch
{
// Something went wrong and we didn't get the lock
// Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing.
locks[instanceId].Remove(lockId);
// It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock.
locksSet.Remove(lockId);
throw;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="lockId">Lock ID to increment.</param>
/// <param name="instanceId">Instance ID of the scope requesting the lock.</param>
/// <param name="locks">Reference to the dictionary to increment on</param>
private void IncrementLock(int lockId, Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>>? locks)
{
// Since we've already checked that we're the parent in the WriteLockInner method, we don't need to check again.
// If it's the very first time a lock has been requested the WriteLocks dict hasn't been instantiated yet.
locks ??= new Dictionary<Guid, Dictionary<int, int>>();
// Try and get the dict associated with the scope id.
var locksDictFound = locks.TryGetValue(instanceId, out Dictionary<int, int>? locksDict);
if (locksDictFound)
{
locksDict!.TryGetValue(lockId, out var value);
locksDict[lockId] = value + 1;
}
else
{
// The scope hasn't requested a lock yet, so we have to create a dict for it.
locks.Add(instanceId, new Dictionary<int, int>());
locks[instanceId][lockId] = 1;
}
}
private void LazyWriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) =>
LazyLockInner(DistributedLockType.WriteLock, instanceId, timeout, lockIds);
private void LazyReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) =>
LazyLockInner(DistributedLockType.ReadLock, instanceId, timeout, lockIds);
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan? timeout = null, params int[] lockIds)
{
lock (_lockQueueLocker)
{
if (_queuedLocks == null)
{
_queuedLocks = new StackQueue<(DistributedLockType, TimeSpan, Guid, int)>();
}
foreach (var lockId in lockIds)
{
_queuedLocks.Enqueue((lockType, timeout ?? TimeSpan.Zero, instanceId, lockId));
}
}
}
/// <summary>
/// Clears all lock counters for a given scope instance, signalling that the scope has been disposed.
/// </summary>
/// <param name="instanceId">Instance ID of the scope to clear.</param>
public void ClearLocks(Guid instanceId)
{
lock (_dictionaryLocker)
{
_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.
(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top =
_queuedLocks.PeekStack();
if (top.instanceId == instanceId)
{
_queuedLocks.Pop();
}
else
{
break;
}
}
}
}
public void EnsureLocksCleared(Guid instanceId)
{
while (!_acquiredLocks?.IsCollectionEmpty() ?? false)
{
_acquiredLocks?.Dequeue().Dispose();
}
// 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 (!(_readLocksDictionary?.Count > 0) && !(_writeLocksDictionary?.Count > 0))
{
return;
}
var exception = new InvalidOperationException(
$"All scopes has not been disposed from parent scope: {instanceId}, see log for more details.");
throw exception;
}
/// <summary>
/// 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.
/// </summary>
public void EnsureLocks(Guid scopeInstanceId)
{
lock (_lockQueueLocker)
{
if (!(_queuedLocks?.Count > 0))
{
return;
}
DistributedLockType currentType = DistributedLockType.ReadLock;
TimeSpan currentTimeout = TimeSpan.Zero;
Guid currentInstanceId = scopeInstanceId;
var collectedIds = new HashSet<int>();
var i = 0;
while (_queuedLocks.Count > 0)
{
(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, var 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 DistributedLockType.ReadLock:
EagerReadLockInner(
currentInstanceId,
currentTimeout == TimeSpan.Zero ? null : currentTimeout,
collectedIds.ToArray());
break;
case DistributedLockType.WriteLock:
EagerWriteLockInner(
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 DistributedLockType.ReadLock:
EagerReadLockInner(
currentInstanceId,
currentTimeout == TimeSpan.Zero ? null : currentTimeout,
collectedIds.ToArray());
break;
case DistributedLockType.WriteLock:
EagerWriteLockInner(
currentInstanceId,
currentTimeout == TimeSpan.Zero ? null : currentTimeout,
collectedIds.ToArray());
break;
}
}
}
public Dictionary<Guid, Dictionary<int, int>>? GetReadLocks() => _readLocksDictionary;
public Dictionary<Guid, Dictionary<int, int>>? GetWriteLocks() => _writeLocksDictionary;
/// <inheritdoc />
public void Dispose()
{
while (!_acquiredLocks?.IsCollectionEmpty() ?? false)
{
_acquiredLocks?.Dequeue().Dispose();
}
// 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 (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0)
{
var exception = new InvalidOperationException(
$"All locks have not been cleared, this usually means that all scopes have not been disposed from the parent scope");
_logger.LogError(exception, GenerateUnclearedScopesLogMessage());
throw exception;
}
}
/// <summary>
/// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they
/// have requested.
/// </summary>
/// <returns>Log message.</returns>
private string GenerateUnclearedScopesLogMessage()
{
// Dump the dicts into a message for the locks.
var builder = new StringBuilder();
builder.AppendLine(
$"Lock counters aren't empty, suggesting a scope hasn't been properly disposed");
WriteLockDictionaryToString(_readLocksDictionary!, builder, "read locks");
WriteLockDictionaryToString(_writeLocksDictionary!, builder, "write locks");
return builder.ToString();
}
/// <summary>
/// Writes a locks dictionary to a <see cref="StringBuilder" /> for logging purposes.
/// </summary>
/// <param name="dict">Lock dictionary to report on.</param>
/// <param name="builder">String builder to write to.</param>
/// <param name="dictName">The name to report the dictionary as.</param>
private void WriteLockDictionaryToString(Dictionary<Guid, Dictionary<int, int>> dict, StringBuilder builder, string dictName)
{
if (dict?.Count > 0)
{
builder.AppendLine($"Remaining {dictName}:");
foreach (KeyValuePair<Guid, Dictionary<int, int>> instance in dict)
{
builder.AppendLine($"Scope {instance.Key}");
foreach (KeyValuePair<int, int> lockCounter in instance.Value)
{
builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}");
}
}
}
}
}