Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dan.Common/Dan.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AsyncKeyedLock" Version="5.1.2" />
<PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.11.0-preview2" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.0.0-preview2" />
Expand Down
78 changes: 10 additions & 68 deletions Dan.Common/Util/KeyedLock.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
using System.Diagnostics;
using AsyncKeyedLock;

namespace Dan.Common.Util;

// Based on https://stackoverflow.com/questions/31138179/asynchronous-locking-based-on-a-key/65256155#65256155
[Obsolete("Please use AsyncKeyedLocker<T> instead.")]
public class KeyedLock<TKey> where TKey : notnull
{
private readonly Dictionary<TKey, (SemaphoreSlim, int)> _perKey;
private readonly Stack<SemaphoreSlim> _pool;
private readonly int _poolCapacity;
private readonly AsyncKeyedLocker<TKey> _asyncKeyedLocker;

public KeyedLock(IEqualityComparer<TKey>? keyComparer = null, int poolCapacity = 10)
{
_perKey = new Dictionary<TKey, (SemaphoreSlim, int)>(keyComparer);
_pool = new Stack<SemaphoreSlim>(poolCapacity);
_poolCapacity = poolCapacity;
_asyncKeyedLocker = (keyComparer == null) ? new(new AsyncKeyedLockOptions(poolSize: poolCapacity)) : new(new AsyncKeyedLockOptions(poolSize: poolCapacity), keyComparer);
}

public async Task<bool> WaitAsync(TKey key, int millisecondsTimeout,
CancellationToken cancellationToken = default)
{
var semaphore = GetSemaphore(key);
var releaser = _asyncKeyedLocker.GetOrAdd(key);
bool entered = false;
try
{
entered = await semaphore.WaitAsync(millisecondsTimeout,
entered = await releaser.SemaphoreSlim.WaitAsync(millisecondsTimeout,
cancellationToken).ConfigureAwait(false);
}
finally
{
if (!entered) ReleaseSemaphore(key, entered: false);
if (!entered) releaser.Dispose();
}

return entered;
Expand All @@ -40,74 +36,20 @@ public Task WaitAsync(TKey key, CancellationToken cancellationToken = default)
public bool Wait(TKey key, int millisecondsTimeout,
CancellationToken cancellationToken = default)
{
var semaphore = GetSemaphore(key);
var releaser = _asyncKeyedLocker.GetOrAdd(key);
bool entered = false;
try
{
entered = semaphore.Wait(millisecondsTimeout, cancellationToken);
entered = releaser.SemaphoreSlim.Wait(millisecondsTimeout, cancellationToken);
}
finally
{
if (!entered) ReleaseSemaphore(key, entered: false);
if (!entered) releaser.Dispose();
}

return entered;
}

public void Wait(TKey key, CancellationToken cancellationToken = default)
=> Wait(key, Timeout.Infinite, cancellationToken);

public void Release(TKey key) => ReleaseSemaphore(key, entered: true);

private SemaphoreSlim GetSemaphore(TKey key)
{
SemaphoreSlim? semaphore;
lock (_perKey)
{
if (_perKey.TryGetValue(key, out var entry))
{
(semaphore, var counter) = entry;
_perKey[key] = (semaphore, ++counter);
}
else
{
lock (_pool) semaphore = _pool.Count > 0 ? _pool.Pop() : null;
semaphore ??= new SemaphoreSlim(1, 1);
_perKey[key] = (semaphore, 1);
}
}

return semaphore;
}

private void ReleaseSemaphore(TKey key, bool entered)
{
SemaphoreSlim semaphore;
int counter;
lock (_perKey)
{
if (_perKey.TryGetValue(key, out var entry))
{
(semaphore, counter) = entry;
counter--;
if (counter == 0)
_perKey.Remove(key);
else
_perKey[key] = (semaphore, counter);
}
else
{
throw new InvalidOperationException("Key not found.");
}
}

if (entered) semaphore.Release();
if (counter == 0)
{
Debug.Assert(semaphore.CurrentCount == 1);
lock (_pool)
if (_pool.Count < _poolCapacity)
_pool.Push(semaphore);
}
}
}
17 changes: 5 additions & 12 deletions Dan.Core/Services/AltinnServiceOwnerAPIService.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using Dan.Common.Models;
using AsyncKeyedLock;
using Dan.Common.Models;
using Dan.Core.Config;
using Dan.Core.Exceptions;
using Dan.Core.Extensions;
using Dan.Core.Models;
using Dan.Core.Services.Interfaces;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net;
using System.Text;
using Dan.Common.Util;
using Dan.Core.Models;

namespace Dan.Core.Services;

Expand All @@ -28,7 +28,7 @@ public class AltinnServiceOwnerApiService : IAltinnServiceOwnerApiService
private const string LanguageEn = "1033";
private readonly string _baseUrl;
private readonly HttpClient _client;
private readonly KeyedLock<string> _keyedLock = new();
private readonly AsyncKeyedLocker<string> _asyncKeyedLock = new(new AsyncKeyedLockOptions { PoolSize = 10 });

/// <summary>
/// Create a new AltinnServiceOwnerHelper instance
Expand Down Expand Up @@ -158,10 +158,8 @@ private async Task UpdateSrrRights(SrrRight singleRight)
{
var key = $"{singleRight.Reportee}_{singleRight.ServiceCode}_{singleRight.ServiceEditionCode}";

try
using (await _asyncKeyedLock.LockAsync(key))
{
await _keyedLock.WaitAsync(key);

var result = await MakeRequest(string.Format(GetSrrUrl, _baseUrl, singleRight.Reportee, singleRight.ServiceCode, singleRight.ServiceEditionCode), HttpMethod.Get);
var rights = JsonConvert.DeserializeObject<List<SrrRight>>(result);

Expand All @@ -184,11 +182,6 @@ private async Task UpdateSrrRights(SrrRight singleRight)
await UpdateSrrRight(right);
}
}

}
finally
{
_keyedLock.Release(key);
}
}

Expand Down
18 changes: 6 additions & 12 deletions Dan.Core/Services/CachingEntityRegistryApiClientService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Net.Http.Json;
using AsyncKeyedLock;
using Dan.Common.Interfaces;
using Dan.Common.Models;
using Dan.Common.Util;
using Dan.Core.Config;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
Expand All @@ -16,7 +15,7 @@ public class CachingEntityRegistryApiClientService : IEntityRegistryApiClientSer
private readonly ILogger<CachingEntityRegistryApiClientService> _logger;
private readonly IHttpClientFactory _clientFactory;
private readonly IPolicyRegistry<string> _policyRegistry;
private readonly KeyedLock<string> _keyedLock = new();
private readonly AsyncKeyedLocker<string> _asyncKeyedLock = new(new AsyncKeyedLockOptions { PoolSize = 10 });

public CachingEntityRegistryApiClientService(ILoggerFactory loggerFactory, IHttpClientFactory clientFactory, IPolicyRegistry<string> policyRegistry)
{
Expand All @@ -40,20 +39,15 @@ public CachingEntityRegistryApiClientService(ILoggerFactory loggerFactory, IHttp
}

var cacheKey = GetCacheKeyFromUri(registryApiUri);
try
{
// There may be several parallel requests due to authorization requirements being validated in parallel.
// To avoid unecessary requests for the same unit, use a keyed lock to make sure we hit the cache.
await _keyedLock.WaitAsync(cacheKey);

// There may be several parallel requests due to authorization requirements being validated in parallel.
// To avoid unecessary requests for the same unit, use a keyed lock to make sure we hit the cache.
using (await _asyncKeyedLock.LockAsync(cacheKey))
{
var cachePolicy = _policyRegistry.Get<AsyncPolicy<EntityRegistryUnit?>>(EntityRegistryCachePolicy);
return await cachePolicy.ExecuteAsync(
async _ => await InternalGetUpstreamEntityRegistryUnitAsync(registryApiUri), new Context(cacheKey));
}
finally
{
_keyedLock.Release(cacheKey);
}
}

private static string GetCacheKeyFromUri(Uri registryApiUri)
Expand Down