Search code examples
c#asp.netconcurrencytoken

Parallel token update


My problem: I'm receiving an expiration token from someone else's API. My task is to request a new token only at the time I set (for example, once every 1 minute). My service receives hundreds of requests simultaneously. And when the time comes to refresh the token, my code requests a hundred new tokens. When I do a test where I first request one token, and then make a request for several tokens in parallel, I am returned the one that has already been created. But if I immediately try to make a hundred simultaneous requests, then everyone will receive a new token. How best to cope with the task? Here's how it works now.

_timeGetToken - time to receive token tokenUpdateTime - time after which the token needs to be renewed(double, minutes(1))

public async Task<string> GetToken()
{
    if(DateTime.Now.Subtract(_timeGetToken) > TimeSpan.FromMinutes(tokenUpdateTime))
    {
        try
        {
            token = await _api.GetToken(new TokenUser());
            _timeGetToken = DateTime.Now;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex.Message);
        }
    }
    if(token != null) 
        return token.Token;
    return string.Empty;
}

My Test:

var t1 = await tokenHandler.GetToken();

List<Task<string>> tokensTasks = new();
for (int i = 0; i < 100; i++)
    tokensTasks.Add(tokenHandler.GetToken());

var tokens = await Task.WhenAll(tokensTasks);

If I do this then I will get the same token, but if I remove the first token request, I will get a hundred different tokens

//var t1 = await tokenHandler.GetToken();

List<Task<string>> tokensTasks = new();
for (int i = 0; i < 100; i++)
    tokensTasks.Add(tokenHandler.GetToken());

var tokens = await Task.WhenAll(tokensTasks);

At first I decided that it was worth using semaphore, but it became clear that in this code they will not help me in any way, I mean that I need to calculate the lifetime of the token in a different way or store it differently. Thanks in advance for any advice


Solution

  • To prevent multiple simultaneous token refreshes in a concurrent environment, you can use a SemaphoreSlim as an asynchronous lock. This ensures that only one thread can execute the token-refreshing code at a time. Here’s how to integrate SemaphoreSlim into your GetToken method along with a double-checked locking pattern:

    private readonly SemaphoreSlim _tokenRefreshLock = new SemaphoreSlim(1, 1);
    
    public async Task<string> GetToken()
    {
        // First check to see if the token needs refreshing
        if (DateTime.Now.Subtract(_timeGetToken) > TimeSpan.FromMinutes(tokenUpdateTime))
        {
            bool acquiredLock = false;
            try
            {
                // Acquire the lock
                await _tokenRefreshLock.WaitAsync();
                acquiredLock = true;
    
                // Double-check to see if another task has already refreshed the token
                if (DateTime.Now.Subtract(_timeGetToken) > TimeSpan.FromMinutes(tokenUpdateTime))
                {
                    token = await _api.GetToken(new TokenUser());
                    _timeGetToken = DateTime.Now;
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex.Message);
            }
            finally
            {
                // Ensure the lock is released
                if (acquiredLock)
                {
                    _tokenRefreshLock.Release();
                }
            }
        }
    
        return token != null ? token.Token : string.Empty;
    }
    

    This approach significantly reduces the likelihood of multiple threads entering the token refresh logic simultaneously, as the first check may prevent most unnecessary lock acquisitions, and the second check, done after acquiring the lock, ensures that only one refresh happens even if multiple threads pass the first check.