【问题标题】:How to update ALL requests when a request property has changed, when using Async/Await?使用 Async/Await 时,如何在请求属性发生更改时更新所有请求?
【发布时间】:2014-08-12 05:27:28
【问题描述】:

我正在使用 async/await 每秒向使用其 API 的服务发出多个请求。我遇到的问题是当我需要刷新令牌时(它每小时过期)。令牌过期后,我从服务返回 401 未经授权的错误。那是我刷新令牌并再次重试失败的请求的时候。令牌刷新得很好,但我发现即使在刷新令牌之后,仍然会使用旧令牌发送许多后续请求。以下是此功能中使用的方法。想知道是否有什么突出的东西是造成这种意外行为的罪魁祸首。

public void Process(id)
{
    var tasks = items.Select(async item =>
    {
        var response = await SendRequestAsync(() => CreateRequest(item.Url));
        //do something with response
        await Process(item.subId); //recursive call to process sub items.
    }).ToList();

    if (tasks.Any())
        await Task.WhenAll(tasks);      

}

public HttpRequestMessage CreateRequest(string url)
{
    var request = new HttpRequestMessage(HttpMethod.Get, url);
    request.Headers.Add("Authorization", "Bearer " + AppSettings.AccessToken); 
    return request;
}

public async Task<HttpResponseMessage> SendRequestAsync(Func<HttpRequestMessage> funcReq)
{   
    var response = await ExecuteRequestAsync(funcReq());

    while (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        await RefreshTokenAsync();
        return await ExecuteRequestAsync(funcReq());  //assuming func ensures that CreateRequest is called each time, so I'll always have a new request with the updated token.
    }  

    return response;            
}

private async Task<HttpResponseMessage> ExecuteRequestAsync(HttpRequestMessage request)
{
    var client = new HttpClient();
    var response = await client.SendAsync(request);
    return response;    
}

public async Task RefreshTokenAsync()
{
    await semaphoreSlim.WaitAsync();
    try
    {      
        if ((DateTime.Now - refreshTime).TotalMinutes < 60) //tokens last for an hour, so after the refresh is made by the first request that failed, subsequent requests should have the latest token.
            return; 

        Token newToken = GetNewToken();
        AppSettings.AccessToken = newToken.AccessToken //AppSettings is a singleton wrapper class for app.cofig app settings
        refreshTime = DateTime.Now
    }
    finally
    {
        semaphoreSlim.Release();
    }
}

【问题讨论】:

  • 使用相同令牌执行请求的时间有多长?您是否消除了这些请求在刷新令牌之前正在进行/启动的可能性?
  • @MarcelN。起初我收到一堆 401 错误——这是意料之中的。但是我的信号量应该确保只有第一个失败的请求才会刷新令牌。其余的应该在它们被发送时具有更新的令牌。尽管有些请求确实使用新令牌发出,但仍有许多请求使用旧令牌发出。刷新后,一些重试请求使用旧令牌持续了一个多小时。
  • 您的请求需要多长时间?假设@MarcelN 关于先前请求未完成执行的说法听起来很合理。另外,你是如何初始化你的SemaphoreSlim的?
  • 我认为这并不完全正确。某些请求可能已经在此时var response = await ExecuteRequestAsync(funcReq());,在评估funcReq 之后,当令牌被刷新时。到这个时候,他们获得新令牌为时已晚。他们将与旧的一起被处决。
  • @Prabhu:我不确定更好,但您可以不等待 401 来刷新令牌。相反,每 60 分钟刷新一次并仅在重新生成时序列化访问。这意味着在 CreateRequest 中,您将不再使用 AppSettings.AccessToken,而是使用 getter 来完成这项工作。这样你就可以肯定你总是拥有最新的令牌。缺点是如果 API 更改了您必须调整的时间间隔。

标签: c# .net asynchronous async-await task


【解决方案1】:

这不是答案。只是我没有在哪里发布其中一个 cmets 中讨论的代码。

Prabhu,我认为这样的事情应该可以在获得 401 之前更新令牌。这只有在您可以假设令牌过期的频率时才有效。

public HttpRequestMessage CreateRequest(string url)
{
    var request = new HttpRequestMessage(HttpMethod.Get, url);
    request.Headers.Add("Authorization", "Bearer " + GetUpToDateAccessToken()); 
    return request;
}

private Token GetUpToDateAccessToken()
{
    _readWriteLockSlim.EnterReadLock();
    try
    {      
        return _latestToken;
    }
    finally
    {
        _readWriteLockSlim.ExitReadLock();
    }  
}

可以使用计时器每 60 分钟更新一次令牌。同步是通过读写锁完成的。这将是 Timer 的刻度处理程序(您可以使用 System.Timers.Timer)。

private void UpdateToken()
{  
  _readWriteLockSlim.EnterWriteLock();
  try 
  {
     if ((DateTime.Now - refreshTime).TotalMinutes >= 60) 
     {
         Token newToken = GetNewToken();
         _latestToken = newToken.AccessToken;
          refreshTime = DateTime.Now;
     }
  }
  finally
  {
    _readWriteLockSlim.ExitWriteLock();
  }
}

正如您所提到的,如果不能保证 60 分钟的有效期,那么这将无法按预期工作。也许您可以每 5 分钟左右重新生成一次令牌,以确保您不会使用无效令牌发出请求。

最后,为了处理 401,因为它们仍然可能发生,您可以将 SendRequestAsync 中的 while 循环修改为:

if (response.StatusCode == HttpStatusCode.Unauthorized)
{
   UpdateToken();
   return await ExecuteRequestAsync(funcReq());
}

【讨论】:

  • @YuvalItzchakov:感谢您的编辑,但我只是在空白行所在的位置添加了_readWriteLockSlim.EnterWriteLock(); :)。
  • 大声笑,很抱歉:)
  • @Marcel 谢谢。如果我得到它,我可以将它与处理 401 一起使用,以确保我不会丢失任何请求吗?
  • @Prabhu:如果你确实得到了 401,那么我想你可以调用 UpdateToken 无论如何都会进入临界区。但是,如果在一个 401 之前启动了两个或多个请求,那么奇怪的“竞争条件”仍然会发生,但频率会降低。
  • @Prabhu:我现在唯一想到的就是这种情况if ((DateTime.Now - refreshTime).TotalMinutes &lt; 60)。如果服务器拒绝请求 AND 时差小于 60 分钟,则请求仍将使用旧令牌。这是唯一的方法。
【解决方案2】:

我将建议以下工作流程(添加到 Marcel N 目前建议的)(伪代码):

// Manage the expiration token yourself in the Application or Db
var token = GetTokenFromDbOrApplicationWithExpirationDateTime()
// Expiration on your application can be a little bit less than real, so instead of 60 can be 50 minutes.
if (token.isExpired)
    token = RequestNewToken()
    Db.SaveChanges(token);
}

CallMethod1Async(token)
CallMethod2Async(token)
CallMethod3Async(token)

您可能还想检查 CallMethodAsync 是否返回带有无效令牌的响应,如 Marcel N 提供的那样。

【讨论】:

    猜你喜欢
    • 2021-04-03
    • 2022-01-11
    • 2013-08-03
    • 2012-03-15
    • 1970-01-01
    • 2019-03-12
    • 1970-01-01
    • 1970-01-01
    • 2018-01-28
    相关资源
    最近更新 更多