|
|
|
|
@@ -5,65 +5,56 @@ using System.Linq;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using Umbraco.Extensions;
|
|
|
|
|
using Umbraco.Cms.Core.Cache;
|
|
|
|
|
using Umbraco.Cms.Core.Collections;
|
|
|
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
|
|
|
using Umbraco.Cms.Core.Events;
|
|
|
|
|
using Umbraco.Cms.Core.IO;
|
|
|
|
|
using Umbraco.Cms.Infrastructure.Persistence;
|
|
|
|
|
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
|
|
|
|
|
using Umbraco.Core.Collections;
|
|
|
|
|
using Umbraco.Extensions;
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Implements <see cref="IScope"/>.
|
|
|
|
|
/// Implements <see cref="IScope" />.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>Not thread-safe obviously.</remarks>
|
|
|
|
|
internal class Scope : IScope
|
|
|
|
|
{
|
|
|
|
|
private enum LockType
|
|
|
|
|
{
|
|
|
|
|
ReadLock,
|
|
|
|
|
WriteLock
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
private IScopedNotificationPublisher _notificationPublisher;
|
|
|
|
|
private readonly CoreDebugSettings _coreDebugSettings;
|
|
|
|
|
|
|
|
|
|
private readonly object _dictionaryLocker;
|
|
|
|
|
private readonly object _lockQueueLocker = new object();
|
|
|
|
|
private readonly IEventAggregator _eventAggregator;
|
|
|
|
|
private readonly IsolationLevel _isolationLevel;
|
|
|
|
|
private readonly object _lockQueueLocker = new();
|
|
|
|
|
private readonly ILogger<Scope> _logger;
|
|
|
|
|
private readonly MediaFileManager _mediaFileManager;
|
|
|
|
|
private readonly RepositoryCacheMode _repositoryCacheMode;
|
|
|
|
|
private readonly bool? _scopeFileSystem;
|
|
|
|
|
|
|
|
|
|
private readonly ScopeProvider _scopeProvider;
|
|
|
|
|
private bool _callContext;
|
|
|
|
|
private bool? _completed;
|
|
|
|
|
private IUmbracoDatabase _database;
|
|
|
|
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
private IEventDispatcher _eventDispatcher;
|
|
|
|
|
private ICompletable _fscope;
|
|
|
|
|
|
|
|
|
|
private IsolatedCaches _isolatedCaches;
|
|
|
|
|
private EventMessages _messages;
|
|
|
|
|
private IScopedNotificationPublisher _notificationPublisher;
|
|
|
|
|
|
|
|
|
|
private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks;
|
|
|
|
|
|
|
|
|
|
// This is all used to safely track read/write locks at given Scope levels so that
|
|
|
|
|
// when we dispose we can verify that everything has been cleaned up correctly.
|
|
|
|
|
private HashSet<int> _readLocks;
|
|
|
|
|
private HashSet<int> _writeLocks;
|
|
|
|
|
private Dictionary<Guid, Dictionary<int, int>> _readLocksDictionary;
|
|
|
|
|
private HashSet<int> _writeLocks;
|
|
|
|
|
private Dictionary<Guid, Dictionary<int, int>> _writeLocksDictionary;
|
|
|
|
|
|
|
|
|
|
private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks;
|
|
|
|
|
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> ReadLocks => _readLocksDictionary;
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> WriteLocks => _writeLocksDictionary;
|
|
|
|
|
|
|
|
|
|
// initializes a new scope
|
|
|
|
|
private Scope(
|
|
|
|
|
ScopeProvider scopeProvider,
|
|
|
|
|
@@ -103,7 +94,8 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
#if DEBUG_SCOPES
|
|
|
|
|
_scopeProvider.RegisterScope(this);
|
|
|
|
|
#endif
|
|
|
|
|
logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8), Thread.CurrentThread.ManagedThreadId);
|
|
|
|
|
logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8),
|
|
|
|
|
Thread.CurrentThread.ManagedThreadId);
|
|
|
|
|
|
|
|
|
|
if (detachable)
|
|
|
|
|
{
|
|
|
|
|
@@ -141,9 +133,12 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
// 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)
|
|
|
|
|
if (repositoryCacheMode != RepositoryCacheMode.Unspecified &&
|
|
|
|
|
parent.RepositoryCacheMode > repositoryCacheMode)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException($"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", nameof(repositoryCacheMode));
|
|
|
|
|
throw new ArgumentException(
|
|
|
|
|
$"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.",
|
|
|
|
|
nameof(repositoryCacheMode));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cannot specify a dispatcher!
|
|
|
|
|
@@ -155,14 +150,17 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
// Only the outermost scope can specify the notification publisher
|
|
|
|
|
if (_notificationPublisher != null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Value cannot be specified on nested scope.", nameof(notificationPublisher));
|
|
|
|
|
throw new ArgumentException("Value cannot be specified on nested scope.",
|
|
|
|
|
nameof(notificationPublisher));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
throw new ArgumentException(
|
|
|
|
|
$"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.",
|
|
|
|
|
nameof(scopeFileSystems));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
@@ -194,8 +192,11 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
bool? scopeFileSystems = null,
|
|
|
|
|
bool callContext = false,
|
|
|
|
|
bool autoComplete = false)
|
|
|
|
|
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete)
|
|
|
|
|
{ }
|
|
|
|
|
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, null,
|
|
|
|
|
scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher,
|
|
|
|
|
scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// initializes a new scope in a nested scopes chain, with its parent
|
|
|
|
|
public Scope(
|
|
|
|
|
@@ -213,60 +214,14 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
bool? scopeFileSystems = null,
|
|
|
|
|
bool callContext = false,
|
|
|
|
|
bool autoComplete = false)
|
|
|
|
|
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher, scopeFileSystems, callContext, autoComplete)
|
|
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Used for testing. Ensures and gets any queued read locks.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> GetReadLocks()
|
|
|
|
|
: this(scopeProvider, coreDebugSettings, mediaFileManager, eventAggregator, logger, fileSystems, parent,
|
|
|
|
|
null, false, isolationLevel, repositoryCacheMode, eventDispatcher, notificationPublisher,
|
|
|
|
|
scopeFileSystems, callContext, autoComplete)
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
// always delegate to root/parent scope.
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
return ParentScope.GetReadLocks();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return _readLocksDictionary;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Used for testing. Ensures and gets and queued write locks.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> GetWriteLocks()
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
// always delegate to root/parent scope.
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
return ParentScope.GetWriteLocks();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return _writeLocksDictionary;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Guid InstanceId { get; } = Guid.NewGuid();
|
|
|
|
|
|
|
|
|
|
public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
|
|
|
|
|
|
|
|
|
public ISqlContext SqlContext
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_scopeProvider.SqlContext == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"The {nameof(_scopeProvider.SqlContext)} on the scope provider is null");
|
|
|
|
|
}
|
|
|
|
|
return _scopeProvider.SqlContext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> ReadLocks => _readLocksDictionary;
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> WriteLocks => _writeLocksDictionary;
|
|
|
|
|
|
|
|
|
|
// a value indicating whether to force call-context
|
|
|
|
|
public bool CallContext
|
|
|
|
|
@@ -301,39 +256,6 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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; }
|
|
|
|
|
@@ -370,6 +292,88 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IUmbracoDatabase DatabaseOrNull
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
EnsureNotDisposed();
|
|
|
|
|
if (ParentScope == null)
|
|
|
|
|
{
|
|
|
|
|
if (_database != null)
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _database;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ParentScope.DatabaseOrNull;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public EventMessages MessagesOrNull
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
EnsureNotDisposed();
|
|
|
|
|
return ParentScope == null ? _messages : ParentScope.MessagesOrNull;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
|
|
|
|
|
private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes;
|
|
|
|
|
|
|
|
|
|
public Guid InstanceId { get; } = Guid.NewGuid();
|
|
|
|
|
|
|
|
|
|
public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
|
|
|
|
|
|
|
|
|
public ISqlContext SqlContext
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_scopeProvider.SqlContext == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"The {nameof(_scopeProvider.SqlContext)} on the scope provider is null");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _scopeProvider.SqlContext;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public IUmbracoDatabase Database
|
|
|
|
|
{
|
|
|
|
|
@@ -403,7 +407,8 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
IsolationLevel currentLevel = database.GetCurrentTransactionIsolationLevel();
|
|
|
|
|
if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent.");
|
|
|
|
|
throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " +
|
|
|
|
|
currentLevel + " from parent.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _database = database;
|
|
|
|
|
@@ -428,24 +433,6 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IUmbracoDatabase DatabaseOrNull
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
EnsureNotDisposed();
|
|
|
|
|
if (ParentScope == null)
|
|
|
|
|
{
|
|
|
|
|
if (_database != null)
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
}
|
|
|
|
|
return _database;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ParentScope.DatabaseOrNull;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public EventMessages Messages
|
|
|
|
|
{
|
|
|
|
|
@@ -468,15 +455,6 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public EventMessages MessagesOrNull
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
EnsureNotDisposed();
|
|
|
|
|
return ParentScope == null ? _messages : ParentScope.MessagesOrNull;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public IEventDispatcher Events
|
|
|
|
|
{
|
|
|
|
|
@@ -502,7 +480,8 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
return ParentScope.Notifications;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _notificationPublisher ?? (_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator));
|
|
|
|
|
return _notificationPublisher ??
|
|
|
|
|
(_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -517,125 +496,14 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
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 (_coreDebugSettings.LogIncompletedScopes)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_completed = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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>
|
|
|
|
|
private void EnsureDbLocks()
|
|
|
|
|
{
|
|
|
|
|
// always delegate to the root parent
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
ParentScope.EnsureDbLocks();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
lock (_lockQueueLocker)
|
|
|
|
|
{
|
|
|
|
|
if (_queuedLocks?.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var currentType = LockType.ReadLock;
|
|
|
|
|
var currentTimeout = TimeSpan.Zero;
|
|
|
|
|
var currentInstanceId = InstanceId;
|
|
|
|
|
var collectedIds = new HashSet<int>();
|
|
|
|
|
|
|
|
|
|
var i = 0;
|
|
|
|
|
while (_queuedLocks.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var (lockType, timeout, instanceId, lockId) = _queuedLocks.Dequeue();
|
|
|
|
|
|
|
|
|
|
if (i == 0)
|
|
|
|
|
{
|
|
|
|
|
currentType = lockType;
|
|
|
|
|
currentTimeout = timeout;
|
|
|
|
|
currentInstanceId = instanceId;
|
|
|
|
|
}
|
|
|
|
|
else if (lockType != currentType || timeout != currentTimeout || instanceId != currentInstanceId)
|
|
|
|
|
{
|
|
|
|
|
// the lock type, instanceId or timeout switched.
|
|
|
|
|
// process the lock ids collected
|
|
|
|
|
switch (currentType)
|
|
|
|
|
{
|
|
|
|
|
case LockType.ReadLock:
|
|
|
|
|
EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
case LockType.WriteLock:
|
|
|
|
|
EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// clear the collected and set new type
|
|
|
|
|
collectedIds.Clear();
|
|
|
|
|
currentType = lockType;
|
|
|
|
|
currentTimeout = timeout;
|
|
|
|
|
currentInstanceId = instanceId;
|
|
|
|
|
}
|
|
|
|
|
collectedIds.Add(lockId);
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// process the remaining
|
|
|
|
|
switch (currentType)
|
|
|
|
|
{
|
|
|
|
|
case LockType.ReadLock:
|
|
|
|
|
EagerReadLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
case LockType.WriteLock:
|
|
|
|
|
EagerWriteLockInner(_database, currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureNotDisposed()
|
|
|
|
|
{
|
|
|
|
|
// 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().";
|
|
|
|
|
var failedMessage =
|
|
|
|
|
$"The {nameof(Scope)} {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;
|
|
|
|
|
@@ -663,7 +531,8 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
// 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 scopes has not been disposed from parent scope: {InstanceId}, see log for more details.");
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
@@ -697,35 +566,213 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds);
|
|
|
|
|
|
|
|
|
|
public void EagerReadLock(TimeSpan timeout, int lockId) =>
|
|
|
|
|
EagerReadLockInner(Database, InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds);
|
|
|
|
|
|
|
|
|
|
public void EagerWriteLock(TimeSpan timeout, int lockId) =>
|
|
|
|
|
EagerWriteLockInner(Database, InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested.
|
|
|
|
|
/// Used for testing. Ensures and gets any queued read locks.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> GetReadLocks()
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
// always delegate to root/parent scope.
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
return ParentScope.GetReadLocks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _readLocksDictionary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Used for testing. Ensures and gets and queued write locks.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
internal Dictionary<Guid, Dictionary<int, int>> GetWriteLocks()
|
|
|
|
|
{
|
|
|
|
|
EnsureDbLocks();
|
|
|
|
|
// always delegate to root/parent scope.
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
return ParentScope.GetWriteLocks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _writeLocksDictionary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (_coreDebugSettings.LogIncompletedScopes)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_completed = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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>
|
|
|
|
|
private void EnsureDbLocks()
|
|
|
|
|
{
|
|
|
|
|
// always delegate to the root parent
|
|
|
|
|
if (ParentScope is not null)
|
|
|
|
|
{
|
|
|
|
|
ParentScope.EnsureDbLocks();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
lock (_lockQueueLocker)
|
|
|
|
|
{
|
|
|
|
|
if (_queuedLocks?.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
LockType currentType = LockType.ReadLock;
|
|
|
|
|
TimeSpan currentTimeout = TimeSpan.Zero;
|
|
|
|
|
Guid currentInstanceId = InstanceId;
|
|
|
|
|
var collectedIds = new HashSet<int>();
|
|
|
|
|
|
|
|
|
|
var i = 0;
|
|
|
|
|
while (_queuedLocks.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
(LockType 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 LockType.ReadLock:
|
|
|
|
|
EagerReadLockInner(_database, currentInstanceId,
|
|
|
|
|
currentTimeout == TimeSpan.Zero ? null : currentTimeout,
|
|
|
|
|
collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
case LockType.WriteLock:
|
|
|
|
|
EagerWriteLockInner(_database, currentInstanceId,
|
|
|
|
|
currentTimeout == TimeSpan.Zero ? null : currentTimeout,
|
|
|
|
|
collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clear the collected and set new type
|
|
|
|
|
collectedIds.Clear();
|
|
|
|
|
currentType = lockType;
|
|
|
|
|
currentTimeout = timeout;
|
|
|
|
|
currentInstanceId = instanceId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
collectedIds.Add(lockId);
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// process the remaining
|
|
|
|
|
switch (currentType)
|
|
|
|
|
{
|
|
|
|
|
case LockType.ReadLock:
|
|
|
|
|
EagerReadLockInner(_database, currentInstanceId,
|
|
|
|
|
currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
case LockType.WriteLock:
|
|
|
|
|
EagerWriteLockInner(_database, currentInstanceId,
|
|
|
|
|
currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureNotDisposed()
|
|
|
|
|
{
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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}");
|
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
builder.AppendLine(
|
|
|
|
|
$"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}");
|
|
|
|
|
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.
|
|
|
|
|
/// 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)
|
|
|
|
|
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)
|
|
|
|
|
foreach (KeyValuePair<Guid, Dictionary<int, int>> instance in dict)
|
|
|
|
|
{
|
|
|
|
|
builder.AppendLine($"Scope {instance.Key}");
|
|
|
|
|
foreach (var lockCounter in instance.Value)
|
|
|
|
|
foreach (KeyValuePair<int, int> lockCounter in instance.Value)
|
|
|
|
|
{
|
|
|
|
|
builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}");
|
|
|
|
|
}
|
|
|
|
|
@@ -840,6 +887,7 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
{
|
|
|
|
|
_scopeProvider.PopAmbientScope(_scopeProvider.AmbientScope);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (OrigContext != _scopeProvider.AmbientContext)
|
|
|
|
|
{
|
|
|
|
|
_scopeProvider.PopAmbientScopeContext();
|
|
|
|
|
@@ -871,12 +919,9 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
/// 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>
|
|
|
|
|
@@ -888,7 +933,7 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
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);
|
|
|
|
|
var locksDictFound = locks.TryGetValue(instanceId, out Dictionary<int, int> locksDict);
|
|
|
|
|
if (locksDictFound)
|
|
|
|
|
{
|
|
|
|
|
locksDict.TryGetValue(lockId, out var value);
|
|
|
|
|
@@ -903,7 +948,7 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Clears all lock counters for a given scope instance, signalling that the scope has been disposed.
|
|
|
|
|
/// 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)
|
|
|
|
|
@@ -924,7 +969,8 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
{
|
|
|
|
|
// It's safe to assume that the locks on the top of the stack belong to this instance,
|
|
|
|
|
// since any child scopes that might have added locks to the stack must be disposed before we try and dispose this instance.
|
|
|
|
|
var top = _queuedLocks.PeekStack();
|
|
|
|
|
(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top =
|
|
|
|
|
_queuedLocks.PeekStack();
|
|
|
|
|
if (top.instanceId == instanceId)
|
|
|
|
|
{
|
|
|
|
|
_queuedLocks.Pop();
|
|
|
|
|
@@ -938,26 +984,6 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds);
|
|
|
|
|
|
|
|
|
|
public void EagerReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(Database, InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds);
|
|
|
|
|
|
|
|
|
|
public void EagerWriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(Database, InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
public void LazyReadLockInner(Guid instanceId, params int[] lockIds)
|
|
|
|
|
{
|
|
|
|
|
if (ParentScope != null)
|
|
|
|
|
@@ -1014,6 +1040,7 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
{
|
|
|
|
|
_queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var lockId in lockIds)
|
|
|
|
|
{
|
|
|
|
|
_queuedLocks.Enqueue((lockType, TimeSpan.Zero, instanceId, lockId));
|
|
|
|
|
@@ -1029,12 +1056,13 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
{
|
|
|
|
|
_queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_queuedLocks.Enqueue((lockType, timeout, instanceId, lockId));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles acquiring a read lock, will delegate it to the parent if there are any.
|
|
|
|
|
/// 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>
|
|
|
|
|
@@ -1049,12 +1077,13 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// We are the outermost scope, handle the lock request.
|
|
|
|
|
LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds);
|
|
|
|
|
LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock,
|
|
|
|
|
ObtainTimeoutReadLock, timeout, lockIds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any.
|
|
|
|
|
/// 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>
|
|
|
|
|
@@ -1069,12 +1098,13 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// We are the outermost scope, handle the lock request.
|
|
|
|
|
LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds);
|
|
|
|
|
LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock,
|
|
|
|
|
ObtainTimeoutWriteLock, timeout, lockIds);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles acquiring a lock, this should only be called from the outermost scope.
|
|
|
|
|
/// 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>
|
|
|
|
|
@@ -1083,8 +1113,10 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
/// <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(IUmbracoDatabase db, Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>> locks, ref HashSet<int> locksSet,
|
|
|
|
|
Action<IUmbracoDatabase, int> obtainLock, Action<IUmbracoDatabase, int, TimeSpan> obtainLockTimeout, TimeSpan? timeout,
|
|
|
|
|
private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary<Guid, Dictionary<int, int>> locks,
|
|
|
|
|
ref HashSet<int> locksSet,
|
|
|
|
|
Action<IUmbracoDatabase, int> obtainLock, Action<IUmbracoDatabase, int, TimeSpan> obtainLockTimeout,
|
|
|
|
|
TimeSpan? timeout,
|
|
|
|
|
params int[] lockIds)
|
|
|
|
|
{
|
|
|
|
|
lock (_dictionaryLocker)
|
|
|
|
|
@@ -1130,41 +1162,37 @@ namespace Umbraco.Cms.Core.Scoping
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Obtains an ordinary read lock.
|
|
|
|
|
/// Obtains an ordinary read lock.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="lockId">Lock object identifier to lock.</param>
|
|
|
|
|
private void ObtainReadLock(IUmbracoDatabase db, int lockId)
|
|
|
|
|
{
|
|
|
|
|
SqlContext.SqlSyntax.ReadLock(db, lockId);
|
|
|
|
|
}
|
|
|
|
|
private void ObtainReadLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.ReadLock(db, lockId);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Obtains a read lock with a custom timeout.
|
|
|
|
|
/// 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(IUmbracoDatabase db, int lockId, TimeSpan timeout)
|
|
|
|
|
{
|
|
|
|
|
private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) =>
|
|
|
|
|
SqlContext.SqlSyntax.ReadLock(db, timeout, lockId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Obtains an ordinary write lock.
|
|
|
|
|
/// Obtains an ordinary write lock.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="lockId">Lock object identifier to lock.</param>
|
|
|
|
|
private void ObtainWriteLock(IUmbracoDatabase db, int lockId)
|
|
|
|
|
{
|
|
|
|
|
SqlContext.SqlSyntax.WriteLock(db, lockId);
|
|
|
|
|
}
|
|
|
|
|
private void ObtainWriteLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.WriteLock(db, lockId);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Obtains a write lock with a custom timeout.
|
|
|
|
|
/// 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(IUmbracoDatabase db, int lockId, TimeSpan timeout)
|
|
|
|
|
{
|
|
|
|
|
private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) =>
|
|
|
|
|
SqlContext.SqlSyntax.WriteLock(db, timeout, lockId);
|
|
|
|
|
|
|
|
|
|
private enum LockType
|
|
|
|
|
{
|
|
|
|
|
ReadLock,
|
|
|
|
|
WriteLock
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|