diff --git a/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj b/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj index 9b757d4..ac45516 100644 --- a/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj +++ b/src/abstractions/DarkLoop.Azure.Functions.Authorization.Abstractions.csproj @@ -21,6 +21,7 @@ + diff --git a/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs b/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs index 12b2ea8..a2fda38 100644 --- a/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs +++ b/src/abstractions/FunctionsAuthorizationCoreServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Copyright (c) DarkLoop. All rights reserved. // +using AsyncKeyedLock; using DarkLoop.Azure.Functions.Authorization.Cache; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -20,7 +21,8 @@ public static IServiceCollection AddFunctionsAuthorizationCore(this IServiceColl return services .AddSingleton() - .AddSingleton(typeof(IFunctionsAuthorizationFilterCache<>), typeof(FunctionsAuthorizationFilterCache<>)); + .AddSingleton(typeof(IFunctionsAuthorizationFilterCache<>), typeof(FunctionsAuthorizationFilterCache<>)) + .AddSingleton(new AsyncKeyedLocker(o => { o.PoolSize = 20; o.PoolInitialFill = 1; })); } } } diff --git a/src/abstractions/FunctionsAuthorizationProvider.cs b/src/abstractions/FunctionsAuthorizationProvider.cs index 1b380d2..b09ff32 100644 --- a/src/abstractions/FunctionsAuthorizationProvider.cs +++ b/src/abstractions/FunctionsAuthorizationProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AsyncKeyedLock; using DarkLoop.Azure.Functions.Authorization.Cache; using DarkLoop.Azure.Functions.Authorization.Internal; using DarkLoop.Azure.Functions.Authorization.Properties; @@ -26,6 +27,7 @@ internal class FunctionsAuthorizationProvider : IFunctionsAuthorizationProvider private readonly IFunctionsAuthorizationFilterCache _filterCache; private readonly FunctionAuthorizationMetadataCollection _metadataStore; private readonly IOptionsMonitor _options; + private readonly AsyncKeyedLocker _asyncKeyedLocker; private readonly ILogger _logger; /// @@ -41,6 +43,7 @@ public FunctionsAuthorizationProvider( IFunctionsAuthorizationFilterCache cache, IOptions options, IOptionsMonitor configOptions, + AsyncKeyedLocker asyncKeyedLocker, ILogger logger) { Check.NotNull(schemeProvider, nameof(schemeProvider)); @@ -53,6 +56,7 @@ public FunctionsAuthorizationProvider( _filterCache = cache; _metadataStore = options.Value.AuthorizationMetadata; _options = configOptions; + _asyncKeyedLocker = asyncKeyedLocker; _logger = logger; } @@ -71,9 +75,7 @@ public async Task GetAuthorizationAsync(string func var asyncKey = $"fap:{functionName}"; - await KeyedMonitor.EnterAsync(asyncKey, unblockOnFirstExit: true); - - try + using (await _asyncKeyedLocker.LockAsync(asyncKey).ConfigureAwait(false)) { if (_filterCache.TryGetFilter(key, out filter)) { @@ -110,10 +112,6 @@ public async Task GetAuthorizationAsync(string func return filter; } - finally - { - KeyedMonitor.Exit(asyncKey); - } } private async Task GetPolicy(IAuthorizationPolicyProvider policyProvider, IEnumerable authData) diff --git a/src/abstractions/Internal/KeyedMonitor.cs b/src/abstractions/Internal/KeyedMonitor.cs deleted file mode 100644 index e6b9bbb..0000000 --- a/src/abstractions/Internal/KeyedMonitor.cs +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright (c) DarkLoop. All rights reserved. -// - -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace DarkLoop.Azure.Functions.Authorization.Internal -{ - /// - /// Provides a way to monitor a key and block other threads from entering the same key. - /// - internal static class KeyedMonitor - { - private static readonly ConcurrentDictionary __locks = new(); - private static readonly Timer __cleanupTimer = new(_ => OnCleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - - static KeyedMonitor() - { - AppDomain.CurrentDomain.ProcessExit += (s, e) => __cleanupTimer.Dispose(); - } - - /// - /// Enters the monitor for the specified key. - /// - /// - /// Try to use very unique keys per block of code you want to protect. - /// If the same ey might be used in different parts of the code, create a string and prefix with another identifier. - /// - /// The key to monitor. - /// - /// A value indicating the protected logic shouldn't need the lock any longer. - /// The lock should not be monitored and should be cleaned-up internally. - /// - public static async Task EnterAsync(object key, bool unblockOnFirstExit = false) - { - var @lock = __locks.GetOrAdd(key, _ => new KeyedLock(unblockOnFirstExit)); - - if (!@lock.Terminated) - { - await @lock.LockAsync(); - } - } - - /// - /// Exits the monitor for the specified key. - /// - /// The key blocked on. - public static void Exit(object key) - { - if (__locks.TryGetValue(key, out var @lock)) - { - @lock.Unlock(); - } - } - - private static void OnCleanup() - { - var keysToDelete = __locks.Where(x => x.Value.Terminated).ToList(); - - foreach (var key in keysToDelete) - { - __locks.TryRemove(key.Key, out _); - } - } - - private class KeyedLock - { - private readonly SemaphoreSlim _monitor; - private readonly bool _disposeOnFirstExit; - private bool _terminated; - - public KeyedLock(bool disposeOnFirstExit) - { - _monitor = new SemaphoreSlim(1); - _disposeOnFirstExit = disposeOnFirstExit; - } - - public bool Terminated => _terminated; - - public async Task LockAsync() - { - await _monitor.WaitAsync(); - } - - public void Unlock() - { - if (_terminated) - { - return; - } - - if (_disposeOnFirstExit) - { - while (_monitor.CurrentCount == 0) - { - _monitor.Release(); - Task.Delay(1).Wait(); - } - - _terminated = true; - _monitor.Dispose(); - } - else - { - if (_monitor.CurrentCount == 0) - { - _monitor.Release(); - } - } - } - } - } -} diff --git a/test/Abstractions.Tests/FunctionsAuthorizationProviderTests.cs b/test/Abstractions.Tests/FunctionsAuthorizationProviderTests.cs index b5303d0..4512948 100644 --- a/test/Abstractions.Tests/FunctionsAuthorizationProviderTests.cs +++ b/test/Abstractions.Tests/FunctionsAuthorizationProviderTests.cs @@ -3,6 +3,7 @@ // using Abstractions.Tests.Fakes; +using AsyncKeyedLock; using Common.Tests; using DarkLoop.Azure.Functions.Authorization; using DarkLoop.Azure.Functions.Authorization.Cache; @@ -63,7 +64,7 @@ public async Task AuthorizationProviderShouldReturnFilterWithNoPolicyWhenMetadat // Arrange var provider = new FunctionsAuthorizationProvider( - _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, _logger!); + _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, new AsyncKeyedLocker(), _logger!); _onLog = (level, eventId, state, exception, formatter) => { @@ -91,7 +92,7 @@ public async Task AuthorizationProviderShouldReturnFilterWithNoPolicyWhenMetadat // Arrange var provider = new FunctionsAuthorizationProvider( - _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, _logger!); + _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, new AsyncKeyedLocker(), _logger!); _onLog = (level, eventId, state, exception, formatter) => { @@ -123,7 +124,7 @@ public async Task AuthorizationProviderShouldReturnFilterWithPolicyWhenMetadataP // Arrange var provider = new FunctionsAuthorizationProvider( - _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, _logger!); + _schemeProvider!, new FunctionsAuthorizationFilterCache(), _options!, _configOptions!, new AsyncKeyedLocker(), _logger!); _onLog = (level, eventId, state, exception, formatter) => { diff --git a/test/Abstractions.Tests/Internal/KeyedMonitorTests.cs b/test/Abstractions.Tests/Internal/KeyedMonitorTests.cs index aa597b6..dfaba3e 100644 --- a/test/Abstractions.Tests/Internal/KeyedMonitorTests.cs +++ b/test/Abstractions.Tests/Internal/KeyedMonitorTests.cs @@ -2,12 +2,7 @@ // Copyright (c) DarkLoop. All rights reserved. // -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DarkLoop.Azure.Functions.Authorization.Internal; +using AsyncKeyedLock; namespace Abstractions.Tests.Internal { @@ -19,6 +14,7 @@ public class KeyedMonitorTests private List? _unmonitored; private List? _monitored; private List? _winner; + private AsyncKeyedLocker? _asyncKeyedLocker; [TestInitialize] public void Initialize() @@ -28,6 +24,11 @@ public void Initialize() _unmonitored = new List(); _monitored = new List(); _winner = new List(); + _asyncKeyedLocker = new(o => + { + o.PoolSize = 20; + o.PoolInitialFill = 1; + }); } [TestMethod("KeyedMonitor: should allow for other threads to unblock after first exit")] @@ -75,9 +76,7 @@ private async Task MonitoredLogicAsync(string name, int millisecondsToBlock) return; } - await KeyedMonitor.EnterAsync("x", unblockOnFirstExit: true); - - try + using (await _asyncKeyedLocker.LockAsync("x")) { await Task.Delay(millisecondsToBlock); _monitored!.Add(name); @@ -90,10 +89,6 @@ private async Task MonitoredLogicAsync(string name, int millisecondsToBlock) _flag = true; _winner!.Add(name); } - finally - { - KeyedMonitor.Exit("x"); - } } } }