Files
Umbraco-CMS/src/Umbraco.Infrastructure/Scoping/Scope.cs
Mole c9ebaadf23 Netcore: File systems rework (#10181)
* Allow IMediaFileSystem to be replace in the DI, or registered with inner filesystem

* Remove GetFileSystem from Filesystems

It was only used by tests.

* Make MediaFileSystem inherit from PhysicalFileSystem directly

* Remove FileSystemWrapper

* Remove inner filesystem from MediaFileSystem

* Add MediaFileManager and bare minimum to make it testable

* Remove MediaFileSystem

* Fix unit tests using MediaFileManager

* Remove IFileSystem and rely only on FileSystem

* Hide dangerous methods in FileSystems and do some cleaning

* Apply stylecop warnings to MediaFileManager

* Add FilesystemsCreator to Tests.Common

This allows you to create an instance if FileSystems with your own specified IFileSystem for testing purposes outside our own test suite.

* Allow the stylesheet filesystem to be replaced.

* Fix tests

* Don't save stylesheetWrapper in a temporary var

* refactor(FileSystems): change how stylesheet filesystem is registered

* fix(FileSystems): unable to overwrite media filesystem

SetMediaFileSystem added the MediaManager as a Singleton instead of
replacing the existing instance.

* fix(FileSystems): calling AddFileSystems replaces MediaManager

When calling AddFileSystems after SetMediaFileSystem the MediaManager
gets replaced by the default PhysicalFileSystem, so instead of calling
SetMediaFileSystem in AddFileSystems we now call TrySetMediaFileSystem
instead. This method will not replace any existing instance of the
MediaManager if there's already a MediaManager registered.

* Use SetMediaFileSystem instead of TrySet, and rename AddFilesystems to ConfigureFileSystems

Also don't call AddFileSystems again in ConfigureFilesystems

* Don't wrap CSS filesystem twice

* Add CreateShadowWrapperInternal to avoid casting

* Throw UnauthorizedAccessException isntead of InvalidOperationException

* Remove ResetShadowId

Co-authored-by: Rasmus John Pedersen <mail@rjp.dk>
2021-04-27 09:52:17 +02:00

892 lines
34 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading;
using Microsoft.Extensions.Logging;
using Umbraco.Extensions;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Infrastructure.Persistence;
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
namespace Umbraco.Cms.Core.Scoping
{
/// <summary>
/// Implements <see cref="IScope"/>.
/// </summary>
/// <remarks>Not thread-safe obviously.</remarks>
internal class Scope : IScope
{
private readonly ScopeProvider _scopeProvider;
private readonly CoreDebugSettings _coreDebugSettings;
private readonly MediaFileManager _mediaFileManager;
private readonly IEventAggregator _eventAggregator;
private readonly ILogger<Scope> _logger;
private readonly IsolationLevel _isolationLevel;
private readonly RepositoryCacheMode _repositoryCacheMode;
private readonly bool? _scopeFileSystem;
private readonly bool _autoComplete;
private bool _callContext;
private bool _disposed;
private bool? _completed;
private IsolatedCaches _isolatedCaches;
private IUmbracoDatabase _database;
private EventMessages _messages;
private ICompletable _fscope;
private IEventDispatcher _eventDispatcher;
// eventually this may need to be injectable - for now we'll create it explicitly and let future needs determine if it should be injectable
private IScopedNotificationPublisher _notificationPublisher;
private readonly object _dictionaryLocker;
private HashSet<int> _readLocks;
private HashSet<int> _writeLocks;
private Dictionary<Guid, Dictionary<int, int>> _readLockDictionary;
private Dictionary<Guid, Dictionary<int, int>> _writeLockDictionary;
internal Dictionary<Guid, Dictionary<int, int>> ReadLocks => _readLockDictionary;
internal Dictionary<Guid, Dictionary<int, int>> WriteLocks => _writeLockDictionary;
// initializes a new scope
private Scope(
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
MediaFileManager mediaFileManager,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
Scope parent,
IScopeContext scopeContext,
bool detachable,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
{
_scopeProvider = scopeProvider;
_coreDebugSettings = coreDebugSettings;
_mediaFileManager = mediaFileManager;
_eventAggregator = eventAggregator;
_logger = logger;
Context = scopeContext;
_isolationLevel = isolationLevel;
_repositoryCacheMode = repositoryCacheMode;
_eventDispatcher = eventDispatcher;
_scopeFileSystem = scopeFileSystems;
_callContext = callContext;
_autoComplete = autoComplete;
Detachable = detachable;
_dictionaryLocker = new object();
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
#endif
logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8), Thread.CurrentThread.ManagedThreadId);
if (detachable)
{
if (parent != null)
{
throw new ArgumentException("Cannot set parent on detachable scope.", nameof(parent));
}
if (scopeContext != null)
{
throw new ArgumentException("Cannot set context on detachable scope.", nameof(scopeContext));
}
if (autoComplete)
{
throw new ArgumentException("Cannot auto-complete a detachable scope.", nameof(autoComplete));
}
// detachable creates its own scope context
Context = new ScopeContext();
// see note below
if (scopeFileSystems == true)
{
_fscope = fileSystems.Shadow();
}
return;
}
if (parent != null)
{
ParentScope = parent;
// 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 && parent.RepositoryCacheMode > repositoryCacheMode)
{
throw new ArgumentException($"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", nameof(repositoryCacheMode));
}
// cannot specify a dispatcher!
if (_eventDispatcher != null)
{
throw new ArgumentException("Value cannot be specified on nested scope.", nameof(eventDispatcher));
}
// cannot specify a different fs scope!
// can be 'true' only on outer scope (and false does not make much sense)
if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems)
{
throw new ArgumentException($"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.", nameof(scopeFileSystems));
}
}
else
{
// the FS scope cannot be "on demand" like the rest, because we would need to hook into
// every scoped FS to trigger the creation of shadow FS "on demand", and that would be
// pretty pointless since if scopeFileSystems is true, we *know* we want to shadow
if (scopeFileSystems == true)
{
_fscope = fileSystems.Shadow();
}
}
}
// initializes a new scope
public Scope(
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
MediaFileManager mediaFileManager,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
bool detachable,
IScopeContext scopeContext,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
// initializes a new scope in a nested scopes chain, with its parent
public Scope(
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
MediaFileManager mediaFileManager,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
Scope parent,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
public Guid InstanceId { get; } = Guid.NewGuid();
public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
public ISqlContext SqlContext => _scopeProvider.SqlContext;
// a value indicating whether to force call-context
public bool CallContext
{
get
{
if (_callContext)
{
return true;
}
if (ParentScope != null)
{
return ParentScope.CallContext;
}
return false;
}
set => _callContext = value;
}
public bool ScopedFileSystems
{
get
{
if (ParentScope != null)
{
return ParentScope.ScopedFileSystems;
}
return _fscope != null;
}
}
/// <inheritdoc />
public RepositoryCacheMode RepositoryCacheMode
{
get
{
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified)
{
return _repositoryCacheMode;
}
if (ParentScope != null)
{
return ParentScope.RepositoryCacheMode;
}
return RepositoryCacheMode.Default;
}
}
/// <inheritdoc />
public IsolatedCaches IsolatedCaches
{
get
{
if (ParentScope != null)
{
return ParentScope.IsolatedCaches;
}
return _isolatedCaches ??= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()));
}
}
// a value indicating whether the scope is detachable
// ie whether it was created by CreateDetachedScope
public bool Detachable { get; }
// the parent scope (in a nested scopes chain)
public Scope ParentScope { get; set; }
public bool Attached { get; set; }
// the original scope (when attaching a detachable scope)
public Scope OrigScope { get; set; }
// the original context (when attaching a detachable scope)
public IScopeContext OrigContext { get; set; }
// the context (for attaching & detaching only)
public IScopeContext Context { get; }
public IsolationLevel IsolationLevel
{
get
{
if (_isolationLevel != IsolationLevel.Unspecified)
{
return _isolationLevel;
}
if (ParentScope != null)
{
return ParentScope.IsolationLevel;
}
return Database.SqlContext.SqlSyntax.DefaultIsolationLevel;
}
}
/// <inheritdoc />
public IUmbracoDatabase Database
{
get
{
EnsureNotDisposed();
if (_database != null)
{
return _database;
}
if (ParentScope != null)
{
IUmbracoDatabase database = ParentScope.Database;
IsolationLevel currentLevel = database.GetCurrentTransactionIsolationLevel();
if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel)
{
throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent.");
}
return _database = database;
}
// create a new database
_database = _scopeProvider.DatabaseFactory.CreateDatabase();
// enter a transaction, as a scope implies a transaction, always
try
{
_database.BeginTransaction(IsolationLevel);
return _database;
}
catch
{
_database.Dispose();
_database = null;
throw;
}
}
}
public IUmbracoDatabase DatabaseOrNull
{
get
{
EnsureNotDisposed();
return ParentScope == null ? _database : ParentScope.DatabaseOrNull;
}
}
/// <inheritdoc />
public EventMessages Messages
{
get
{
EnsureNotDisposed();
if (ParentScope != null)
{
return ParentScope.Messages;
}
return _messages ??= new EventMessages();
// TODO: event messages?
// this may be a problem: the messages collection will be cleared at the end of the scope
// how shall we process it in controllers etc? if we don't want the global factory from v7?
// it'd need to be captured by the controller
//
// + rename // EventMessages = ServiceMessages or something
}
}
public EventMessages MessagesOrNull
{
get
{
EnsureNotDisposed();
return ParentScope == null ? _messages : ParentScope.MessagesOrNull;
}
}
/// <inheritdoc />
public IEventDispatcher Events
{
get
{
EnsureNotDisposed();
if (ParentScope != null)
{
return ParentScope.Events;
}
return _eventDispatcher ??= new QueuingEventDispatcher(_mediaFileManager);
}
}
public IScopedNotificationPublisher Notifications
{
get
{
EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Notifications;
return _notificationPublisher ?? (_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator));
}
}
/// <inheritdoc />
public bool Complete()
{
if (_completed.HasValue == false)
{
_completed = true;
}
return _completed.Value;
}
public void Reset() => _completed = null;
public void ChildCompleted(bool? completed)
{
// if child did not complete we cannot complete
if (completed.HasValue == false || completed.Value == false)
{
if (LogUncompletedScopes)
{
_logger.LogWarning("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
}
_completed = false;
}
}
private void EnsureNotDisposed()
{
// We can't be disposed
if (_disposed)
{
throw new ObjectDisposedException($"The {nameof(Scope)} ({this.GetDebugInfo()}) 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();
// TODO: safer?
//if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
// throw new ObjectDisposedException(GetType().FullName);
}
public void Dispose()
{
EnsureNotDisposed();
if (this != _scopeProvider.AmbientScope)
{
var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow().";
#if DEBUG_SCOPES
Scope ambient = _scopeProvider.AmbientScope;
_logger.LogWarning("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
if (ambient == null)
{
throw new InvalidOperationException("Not the ambient scope (no ambient scope).");
}
ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient);
ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this);
throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n"
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
#else
throw new InvalidOperationException(failedMessage);
#endif
}
// Decrement the lock counters on the parent if any.
ClearLocks(InstanceId);
if (ParentScope is null)
{
// 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)
{
var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details.");
_logger.LogError(exception, GenerateUnclearedScopesLogMessage());
throw exception;
}
}
_scopeProvider.PopAmbientScope(this); // might be null = this is how scopes are removed from context objects
#if DEBUG_SCOPES
_scopeProvider.Disposed(this);
#endif
if (_autoComplete && _completed == null)
{
_completed = true;
}
if (ParentScope != null)
{
ParentScope.ChildCompleted(_completed);
}
else
{
DisposeLastScope();
}
_disposed = true;
}
/// <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.
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");
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 (var instance in dict)
{
builder.AppendLine($"Scope {instance.Key}");
foreach (var lockCounter in instance.Value)
{
builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}");
}
}
}
}
private void DisposeLastScope()
{
// figure out completed
var completed = _completed.HasValue && _completed.Value;
// deal with database
var databaseException = false;
if (_database != null)
{
try
{
if (completed)
{
_database.CompleteTransaction();
}
else
{
_database.AbortTransaction();
}
}
catch
{
databaseException = true;
throw;
}
finally
{
_database.Dispose();
_database = null;
if (databaseException)
{
RobustExit(false, true);
}
}
}
RobustExit(completed, false);
}
// this chains some try/finally blocks to
// - complete and dispose the scoped filesystems
// - deal with events if appropriate
// - remove the scope context if it belongs to this scope
// - deal with detachable scopes
// here,
// - completed indicates whether the scope has been completed
// can be true or false, but in both cases the scope is exiting
// in a normal way
// - onException indicates whether completing/aborting the database
// transaction threw an exception, in which case 'completed' has
// to be false + events don't trigger and we just to some cleanup
// to ensure we don't leave a scope around, etc
private void RobustExit(bool completed, bool onException)
{
if (onException)
{
completed = false;
}
TryFinally(() =>
{
if (_scopeFileSystem == true)
{
if (completed)
{
_fscope.Complete();
}
_fscope.Dispose();
_fscope = null;
}
}, () =>
{
// deal with events
if (onException == false)
{
_eventDispatcher?.ScopeExit(completed);
_notificationPublisher?.ScopeExit(completed);
}
}, () =>
{
// if *we* created it, then get rid of it
if (_scopeProvider.AmbientContext == Context)
{
try
{
_scopeProvider.AmbientContext.ScopeExit(completed);
}
finally
{
// removes the ambient context (ambient scope already gone)
_scopeProvider.PopAmbientScopeContext();
}
}
}, () =>
{
if (Detachable)
{
// get out of the way, restore original
// TODO: Difficult to know if this is correct since this is all required
// by Deploy which I don't fully understand since there is limited tests on this in the CMS
if (OrigScope != _scopeProvider.AmbientScope)
{
_scopeProvider.PopAmbientScope(_scopeProvider.AmbientScope);
}
if (OrigContext != _scopeProvider.AmbientContext)
{
_scopeProvider.PopAmbientScopeContext();
}
Attached = false;
OrigScope = null;
OrigContext = null;
}
});
}
private static void TryFinally(params Action[] actions) => TryFinally(0, actions);
private static void TryFinally(int index, Action[] actions)
{
if (index == actions.Length)
{
return;
}
try
{
actions[index]();
}
finally
{
TryFinally(index + 1, actions);
}
}
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes;
/// <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 var 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;
}
}
/// <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>
private void ClearLocks(Guid instanceId)
{
if (ParentScope is not null)
{
ParentScope.ClearLocks(instanceId);
}
else
{
lock (_dictionaryLocker)
{
ReadLocks?.Remove(instanceId);
WriteLocks?.Remove(instanceId);
}
}
}
/// <inheritdoc />
public void ReadLock(int lockId) => ReadLockInner(InstanceId, null, lockId);
/// <inheritdoc />
public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId);
/// <inheritdoc />
public void WriteLock(int lockId) => WriteLockInner(InstanceId, null, lockId);
/// <inheritdoc />
public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId);
/// <summary>
/// Handles acquiring a read lock, 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="lockId">Array of lock object identifiers.</param>
private void ReadLockInner(Guid instanceId, TimeSpan? timeout, int lockId)
{
if (ParentScope is not null)
{
// If we have a parent we delegate lock creation to parent.
ParentScope.ReadLockInner(instanceId, timeout, lockId);
}
else
{
// We are the outermost scope, handle the lock request.
LockInner(instanceId, ref _readLockDictionary, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockId);
}
}
/// <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="lockId">Array of lock object identifiers.</param>
private void WriteLockInner(Guid instanceId, TimeSpan? timeout, int lockId)
{
if (ParentScope is not null)
{
// If we have a parent we delegate lock creation to parent.
ParentScope.WriteLockInner(instanceId, timeout, lockId);
}
else
{
// We are the outermost scope, handle the lock request.
LockInner(instanceId, ref _writeLockDictionary, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockId);
}
}
/// <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 database without a timeout.</param>
/// <param name="obtainLockTimeout">Delegate used to request the lock from the database with a timeout.</param>
/// <param name="timeout">Optional timeout parameter to specify a timeout.</param>
/// <param name="lockIds">Lock identifiers to lock on.</param>
private void LockInner(Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>> locks, ref HashSet<int> locksSet,
Action<int> obtainLock, Action<int, TimeSpan> obtainLockTimeout, TimeSpan? timeout,
int lockId)
{
lock (_dictionaryLocker)
{
locksSet ??= new HashSet<int>();
// Only acquire the lock if we haven't done so yet.
if (!locksSet.Contains(lockId))
{
IncrementLock(lockId, instanceId, ref locks);
locksSet.Add(lockId);
try
{
if (timeout is null)
{
// We just want an ordinary lock.
obtainLock(lockId);
}
else
{
// We want a lock with a custom timeout
obtainLockTimeout(lockId, timeout.Value);
}
}
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;
}
}
else
{
// We already have a lock, but need to update the dictionary for debugging purposes.
IncrementLock(lockId, instanceId, ref locks);
}
}
}
/// <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 ObtainTimeoutReadLock(int lockId, TimeSpan timeout)
{
Database.SqlContext.SqlSyntax.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)
{
Database.SqlContext.SqlSyntax.WriteLock(Database, timeout, lockId);
}
}
}