Обеспечение того, чтобы поле обновлялось только одним потоком после выхода семафора C # - PullRequest
0 голосов
/ 04 февраля 2019

У меня есть DelegatingHandler, который передается в конструкторе синглтона HttpClient.

Этот обработчик отвечает за выполнение базовой аутентификации для получения токена-носителя, который используется для последующих запросов досрок действия токена истекает.Когда срок действия токена истекает, базовая аутентификация запускается снова, чтобы обновить токен и т. Д.

public class MyMessageHandler : DelegatingHandler
{
    private readonly string baseAddress;
    private readonly string user;
    private readonly string pass;

    private readonly SemaphoreSlim sem;
    private Token token;

    public MyMessageHandler() : base()
    {
        // validation/assignment of baseAddress, userName, password
        // ..omitted for brevity

        sem = new SemaphoreSlim(1);
        // this is the first time, so get the token
        token = GetTokenAsync().GetAwaiter().GetResult();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("Authorization"))
        {
            request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
        }

        var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

        // if a token refresh is needed
        if (response.StatusCode == HttpStatusCode.Unauthorized && whateverOtherCheckToTriggerTokenRefresh
        {
            try
            {
                // don't want multiple requests refreshing the token
                await sem.WaitAsync().ConfigureAwait(false);
                token = await GetTokenAsync().ConfigureAwait(false);

                // we have the token, now set the headers to the new values
                request.Headers.Remove("Authorization");
                request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");

                // replay the request with the new token
                response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
            }
            finally
            {
                sem.Release();
            }
        }
        return response;
    }

    private async Task<Token> GetTokenAsync()
    {
        var authBytes = Encoding.UTF8.GetBytes($"{user}:{pass}");
        var basicAuthToken = Convert.ToBase64String(authBytes);

        var pairs = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials")
        };

        // get ourselves a token using basic auth
        var message = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(baseAddress), "/token"))
        {
            Content = new FormUrlEncodedContent(pairs)
        };

        message.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuthToken);

        var response = await base.SendAsync(message, new CancellationToken()).ConfigureAwait(false);

        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

       // return our token
       return JsonConvert.DeserializeObject<Token>(result);
    }
}

Я использовал Semaphore на этапе обновления / обновления токена, так как это одновременный WCFприложение, и я не хочу, чтобы несколько запросов все запрашивали новый токен.После завершения этапа обновления в поле token для MyMessageHandler устанавливается новый объект Token, возвращаемый GetTokenAsync(), и семафор освобождается, поэтому другие ожидающие запросы будут входить в блок кода.

1) Как теперь запретить зависшим запросам, ожидающим в строке await sem.WaitAsync().ConfigureAwait(false);, выполнить шаг обновления токена?

2) Должен ли я беспокоиться о входящих запросах, пытающихся получить значение поля token во время его обновления в семафоре?Если да, должен ли я сделать что-то вроде Interlocked.Exchange(ref token, newlyFetchedToken);, как только будет получен новый токен?

Обновление :

После ответа Damien_The_Unbeliever, вот мой подход к реализацииего ответ.Мне все еще трудно понять, как реализовать ответ.

public class MyMessageHandler : DelegatingHandler
{
    private readonly string baseAddress;
    private readonly string user;
    private readonly string pass;

    private TaskToken> tokenTask;

    public MyMessageHandler() : base()
    {
        // validation/assignment of baseAddress, userName, password
        // ..omitted for brevity
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var localTokenTask = tokenTask;
        Token localToken;

        if (!request.Headers.Contains("Authorization"))
        {
            request.Headers.Add("Authorization", $"Bearer {localToken.AccessToken}");
        }

        var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

        // if a token refresh is needed
        if (whateverOtherCheckToTriggerTokenRefresh)
        {
            var tcs = new TaskCompletionSource<Token>();

            if (localTokenTask != Interlocked.CompareExchange(ref tokenTask, tcs.Task, localTokenTask))
            {
                // get latest value of Task<Token> field locally
                localTokenTask = tokenTask;
                localToken = await localTokenTask;
                request.Headers.Authorization = new AuthenticationHeaderValue($"{localToken.TokenType}", $"{localToken.AccessToken}");
            }
            else
            {
                var newToken = await GetTokenAsync();
                tcs.SetResult(newToken);
                request.Headers.Authorization = new AuthenticationHeaderValue($"{newToken.TokenType}", $"{newToken.AccessToken}");
            }
        }
        response = await base.SendAsync(request, cancellationToken);

        return response;
    }

    private async Task<Token> GetTokenAsync()
    {
        var authBytes = Encoding.UTF8.GetBytes($"{user}:{pass}");
        var basicAuthToken = Convert.ToBase64String(authBytes);

        var pairs = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials")
        };

        // get ourselves a token using basic auth
        var message = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(baseAddress), "/token"))
        {
            Content = new FormUrlEncodedContent(pairs)
        };

        message.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuthToken);

        var response = await base.SendAsync(message, new CancellationToken()).ConfigureAwait(false);

        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

       // return our token
       return JsonConvert.DeserializeObject<Token>(result);
    }
}

1 Ответ

0 голосов
/ 05 февраля 2019

Оставляя в стороне некоторые другие проблемы (пустые блоки catch и предположение, что один экземпляр HttpRequestMessage действительно может быть отправлен более одного раза), то, что я обычно имел бы в качестве поля, было бы Task<Token>.

В исходящих запросах скопируйте копию этого поля в локальную переменную, а затем await фактический токен.Если вы получите неавторизованный ответ, создайте новый TaskCompletionSource и выполните InterlockedCompareExchange, чтобы поменять Task в поле.Если обмен был успешным, теперь «ваша» ответственность за обновление токена и завершение Task.

Однако, если InterlockedCompareExchange не удалось, это означает, что кто-то еще имеет или находится в процессео замене токена.Вернитесь к началу вашего метода и вместо этого await этот новый Task<Token>.

Никаких семафоров, достаточно простое поведение для размышления.Вполне возможно, что даже новый Token также истечет к тому времени, когда вы попытаетесь его использовать - так что будьте готовы к многократному зацикливанию и выработайте некоторую стратегию, чтобы вы не зацикливались вечно, если в игре что-то еще иС токенами проблем нет.

И удалите пустые catch.

...