For one of our csharp projects one of our developers accidentally created a deadlock in a class using a SemaphoreSlim object where the SemaphoreSlim was not released in all execution paths.
Below is a stripped version of the class including the deadlock issue on line 34:
public abstract class BaseTokenPolicy
{
private readonly SemaphoreSlim _semaphore = new(1);
private readonly IMemoryCache _cache;
protected BaseTokenPolicy(IMemoryCache memoryCache)
{
_cache = memoryCache;
}
public async Task ProcessAsync(string scope)
{
var token = await GetTokenAsync(scope);
}
public abstract Task<AccessToken> GetAccessTokenAsync(string scope);
private async Task<AccessToken> GetTokenAsync(string scope)
{
if (GetValidToken(scope, out var cachedToken))
{
return cachedToken;
}
await _semaphore.WaitAsync();
if (GetValidToken(scope, out var recentlyCachedToken))
{
return recentlyCachedToken;
}
try
{
var newToken = await GetAccessTokenAsync(scope);
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(newToken.Expiry - DateTimeOffset.UtcNow);
_cache.Set(scope, newToken, cacheEntryOptions);
return newToken;
}
finally
{
_semaphore.Release();
}
}
private bool GetValidToken(string scope, [NotNullWhen(true)] out AccessToken token)
{
if (_cache.TryGetValue(scope, out AccessToken accessToken) && accessToken.Expiry > DateTimeOffset.UtcNow)
{
token = accessToken;
return true;
}
else
{
token = new AccessToken("", DateTimeOffset.MinValue);
return false;
}
}
public class AccessToken
{
public AccessToken(string token, DateTimeOffset expiry, string type = "Bearer")
{
Token = token ?? throw new ArgumentNullException(nameof(token));
Expiry = expiry;
Type = type;
}
public string Token { get; set; }
public DateTimeOffset Expiry { get; set; }
public string Type { get; set; }
public override string ToString() => $"{Type} {Token}";
}
}