Merge remote-tracking branch 'origin/v12/dev' into v13/dev
# Conflicts: # src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs # src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs # src/Umbraco.Core/CompatibilitySuppressions.xml # src/Umbraco.Web.Common/Configuration/ConfigureApiVersioningOptions.cs # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Tabs/tabs.spec.ts
This commit is contained in:
@@ -24,7 +24,19 @@ public class DictionaryAppCache : IRequestCache
|
||||
public virtual object? Get(string key) => _items.TryGetValue(key, out var value) ? value : null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object? Get(string key, Func<object?> factory) => _items.GetOrAdd(key, _ => factory());
|
||||
public virtual object? Get(string key, Func<object?> factory)
|
||||
{
|
||||
var value = _items.GetOrAdd(key, _ => factory());
|
||||
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// do not cache null values
|
||||
_items.TryRemove(key, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool Set(string key, object? value) => _items.TryAdd(key, value);
|
||||
|
||||
|
||||
@@ -31,6 +31,14 @@ public class FastDictionaryAppCache : IAppCache
|
||||
Lazy<object?>? result = _items.GetOrAdd(cacheKey, k => SafeLazy.GetSafeLazy(getCacheItem));
|
||||
|
||||
var value = result.Value; // will not throw (safe lazy)
|
||||
|
||||
if (value is null)
|
||||
{
|
||||
// do not cache null values
|
||||
_items.TryRemove(cacheKey, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(value is SafeLazy.ExceptionHolder eh))
|
||||
{
|
||||
return value;
|
||||
|
||||
@@ -18,6 +18,7 @@ public interface IAppCache
|
||||
/// <param name="key">The key of the item.</param>
|
||||
/// <param name="factory">A factory function that can create the item.</param>
|
||||
/// <returns>The item.</returns>
|
||||
/// <remarks>Null values returned from the factory function are never cached.</remarks>
|
||||
object? Get(string key, Func<object?> factory);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.ComponentModel;
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for image resize settings.
|
||||
/// Typed configuration options for image resize settings.
|
||||
/// </summary>
|
||||
public class ImagingResizeSettings
|
||||
{
|
||||
@@ -14,13 +14,13 @@ public class ImagingResizeSettings
|
||||
internal const int StaticMaxHeight = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the maximim resize width.
|
||||
/// Gets or sets a value for the maximum resize width.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMaxWidth)]
|
||||
public int MaxWidth { get; set; } = StaticMaxWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the maximim resize height.
|
||||
/// Gets or sets a value for the maximum resize height.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMaxHeight)]
|
||||
public int MaxHeight { get; set; } = StaticMaxHeight;
|
||||
|
||||
@@ -4,18 +4,27 @@
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for imaging settings.
|
||||
/// Typed configuration options for imaging settings.
|
||||
/// </summary>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigImaging)]
|
||||
public class ImagingSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value for imaging cache settings.
|
||||
/// Gets or sets a value for the Hash-based Message Authentication Code (HMAC) secret key for request authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Setting or updating this value will cause all existing generated URLs to become invalid and return a 400 Bad Request response code.
|
||||
/// When set, the maximum resize settings are not used/validated anymore, because you can only request URLs with a valid HMAC token anyway.
|
||||
/// </remarks>
|
||||
public byte[] HMACSecretKey { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for imaging cache settings.
|
||||
/// </summary>
|
||||
public ImagingCacheSettings Cache { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for imaging resize settings.
|
||||
/// Gets or sets a value for imaging resize settings.
|
||||
/// </summary>
|
||||
public ImagingResizeSettings Resize { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
@@ -7,15 +8,18 @@ public sealed class ApiMediaBuilder : IApiMediaBuilder
|
||||
{
|
||||
private readonly IApiContentNameProvider _apiContentNameProvider;
|
||||
private readonly IApiMediaUrlProvider _apiMediaUrlProvider;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
|
||||
|
||||
public ApiMediaBuilder(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
IApiMediaUrlProvider apiMediaUrlProvider,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
{
|
||||
_apiContentNameProvider = apiContentNameProvider;
|
||||
_apiMediaUrlProvider = apiMediaUrlProvider;
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
|
||||
}
|
||||
|
||||
@@ -25,11 +29,27 @@ public sealed class ApiMediaBuilder : IApiMediaBuilder
|
||||
_apiContentNameProvider.GetName(media),
|
||||
media.ContentType.Alias,
|
||||
_apiMediaUrlProvider.GetUrl(media),
|
||||
Extension(media),
|
||||
Width(media),
|
||||
Height(media),
|
||||
Bytes(media),
|
||||
Properties(media));
|
||||
|
||||
// map all media properties except the umbracoFile one, as we've already included the file URL etc. in the output
|
||||
private IDictionary<string, object?> Properties(IPublishedContent media) =>
|
||||
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapProperties(media.Properties.Where(p => p.Alias != Constants.Conventions.Media.File))
|
||||
private string? Extension(IPublishedContent media)
|
||||
=> media.Value<string>(_publishedValueFallback, Constants.Conventions.Media.Extension);
|
||||
|
||||
private int? Width(IPublishedContent media)
|
||||
=> media.Value<int?>(_publishedValueFallback, Constants.Conventions.Media.Width);
|
||||
|
||||
private int? Height(IPublishedContent media)
|
||||
=> media.Value<int?>(_publishedValueFallback, Constants.Conventions.Media.Height);
|
||||
|
||||
private int? Bytes(IPublishedContent media)
|
||||
=> media.Value<int?>(_publishedValueFallback, Constants.Conventions.Media.Bytes);
|
||||
|
||||
// map all media properties except the umbraco ones, as we've already included those in the output
|
||||
private IDictionary<string, object?> Properties(IPublishedContent media)
|
||||
=> _outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapMediaProperties(media)
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
public enum FieldType
|
||||
{
|
||||
String,
|
||||
StringRaw,
|
||||
StringAnalyzed,
|
||||
StringSortable,
|
||||
Number,
|
||||
Date
|
||||
|
||||
@@ -4,7 +4,7 @@ public sealed class FilterOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required string Value { get; set; }
|
||||
public required string[] Values { get; set; }
|
||||
|
||||
public FilterOperation Operator { get; set; }
|
||||
public required FilterOperation Operator { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiRichTextParser
|
||||
{
|
||||
RichTextElement? Parse(string html);
|
||||
IRichTextElement? Parse(string html);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ public interface IContentIndexHandler : IDiscoverable
|
||||
/// Calculates the field values for a given content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="culture">The culture to retrieve the field values for (null if the content does not vary by culture).</param>
|
||||
/// <returns>The values to add to the index.</returns>
|
||||
IEnumerable<IndexFieldValue> GetFieldValues(IContent content);
|
||||
IEnumerable<IndexFieldValue> GetFieldValues(IContent content, string? culture);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the field definitions required to support the field values in the index.
|
||||
|
||||
@@ -6,7 +6,7 @@ public interface IOutputExpansionStrategy
|
||||
{
|
||||
IDictionary<string, object?> MapElementProperties(IPublishedElement element);
|
||||
|
||||
IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties);
|
||||
|
||||
IDictionary<string, object?> MapContentProperties(IPublishedContent content);
|
||||
|
||||
IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
public interface IRequestStartItemProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the requested start item from the "Start-Item" header, if present.
|
||||
/// Gets the requested start item, if present.
|
||||
/// </summary>
|
||||
IPublishedContent? GetStartItem();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the requested start item, if present.
|
||||
/// </summary>
|
||||
string? RequestedStartItem();
|
||||
}
|
||||
|
||||
@@ -5,4 +5,6 @@ public sealed class IndexField
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required FieldType FieldType { get; set; }
|
||||
|
||||
public required bool VariesByCulture { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ public sealed class IndexFieldValue
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required object Value { get; set; }
|
||||
public required IEnumerable<object> Values { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ internal sealed class NoopOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties);
|
||||
|
||||
public IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties)
|
||||
=> properties.ToDictionary(p => p.Alias, p => p.GetDeliveryApiValue(true));
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> MapProperties(content.Properties);
|
||||
|
||||
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
=> MapProperties(media.Properties.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false));
|
||||
|
||||
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties)
|
||||
=> properties.ToDictionary(p => p.Alias, p => p.GetDeliveryApiValue(false));
|
||||
}
|
||||
|
||||
@@ -6,4 +6,7 @@ internal sealed class NoopRequestStartItemProvider : IRequestStartItemProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IPublishedContent? GetStartItem() => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? RequestedStartItem() => null;
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ public sealed class SelectorOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required string Value { get; set; }
|
||||
public required string[] Values { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,5 @@ public sealed class SortOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public Direction Direction { get; set; }
|
||||
|
||||
public FieldType FieldType { get; set; }
|
||||
public required Direction Direction { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
public sealed class ApiMedia : IApiMedia
|
||||
{
|
||||
public ApiMedia(Guid id, string name, string mediaType, string url, IDictionary<string, object?> properties)
|
||||
public ApiMedia(Guid id, string name, string mediaType, string url, string? extension, int? width, int? height, int? bytes, IDictionary<string, object?> properties)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
MediaType = mediaType;
|
||||
Url = url;
|
||||
Extension = extension;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Bytes = bytes;
|
||||
Properties = properties;
|
||||
}
|
||||
|
||||
@@ -19,5 +23,13 @@ public sealed class ApiMedia : IApiMedia
|
||||
|
||||
public string Url { get; }
|
||||
|
||||
public string? Extension { get; }
|
||||
|
||||
public int? Width { get; }
|
||||
|
||||
public int? Height { get; }
|
||||
|
||||
public int? Bytes { get; }
|
||||
|
||||
public IDictionary<string, object?> Properties { get; }
|
||||
}
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
public interface IApiMedia
|
||||
{
|
||||
public Guid Id { get; }
|
||||
Guid Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
string Name { get; }
|
||||
|
||||
public string MediaType { get; }
|
||||
string MediaType { get; }
|
||||
|
||||
public string Url { get; }
|
||||
string Url { get; }
|
||||
|
||||
public IDictionary<string, object?> Properties { get; }
|
||||
string? Extension { get; }
|
||||
|
||||
int? Width { get; }
|
||||
|
||||
int? Height { get; }
|
||||
|
||||
int? Bytes { get; }
|
||||
|
||||
IDictionary<string, object?> Properties { get; }
|
||||
}
|
||||
|
||||
6
src/Umbraco.Core/Models/DeliveryApi/IRichTextElement.cs
Normal file
6
src/Umbraco.Core/Models/DeliveryApi/IRichTextElement.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public interface IRichTextElement
|
||||
{
|
||||
string Tag { get; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public sealed class RichTextElement
|
||||
{
|
||||
public RichTextElement(string tag, string text, Dictionary<string, object> attributes, IEnumerable<RichTextElement> elements)
|
||||
{
|
||||
Tag = tag;
|
||||
Text = text;
|
||||
Attributes = attributes;
|
||||
Elements = elements;
|
||||
}
|
||||
|
||||
public string Tag { get; }
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public Dictionary<string, object> Attributes { get; }
|
||||
|
||||
public IEnumerable<RichTextElement> Elements { get; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public sealed class RichTextGenericElement : IRichTextElement
|
||||
{
|
||||
public RichTextGenericElement(string tag, Dictionary<string, object> attributes, IEnumerable<IRichTextElement> elements)
|
||||
{
|
||||
Tag = tag;
|
||||
Attributes = attributes;
|
||||
Elements = elements;
|
||||
}
|
||||
|
||||
public string Tag { get; }
|
||||
|
||||
public Dictionary<string, object> Attributes { get; }
|
||||
|
||||
public IEnumerable<IRichTextElement> Elements { get; }
|
||||
}
|
||||
11
src/Umbraco.Core/Models/DeliveryApi/RichTextTextElement.cs
Normal file
11
src/Umbraco.Core/Models/DeliveryApi/RichTextTextElement.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public sealed class RichTextTextElement : IRichTextElement
|
||||
{
|
||||
public RichTextTextElement(string text)
|
||||
=> Text = text;
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public string Tag => "#text";
|
||||
}
|
||||
272
src/Umbraco.Core/Scoping/CoreScope.cs
Normal file
272
src/Umbraco.Core/Scoping/CoreScope.cs
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
58
src/Umbraco.Core/Scoping/ILockingMechanism.cs
Normal file
58
src/Umbraco.Core/Scoping/ILockingMechanism.cs
Normal 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();
|
||||
}
|
||||
433
src/Umbraco.Core/Scoping/LockingMechanism.cs
Normal file
433
src/Umbraco.Core/Scoping/LockingMechanism.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="7.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="7.0.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
||||
Reference in New Issue
Block a user