使用async / await时处理限制/速率限制(429错误)

时间:2014-07-30 04:03:12

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

我有以下异步代码,可以从我项目中的很多地方调用:

public async Task<HttpResponseMessage> MakeRequestAsync(HttpRequestMessage request)
{            
    var client = new HttpClient();
    return await client.SendAsync(request).ConfigureAwait(false);                
}

如何调用上述方法的示例:

 var tasks = items.Select(async i =>
            {                    
                var response = await MakeRequestAsync(i.Url);           
                //do something with response    
            });

我正在击中的ZenDesk API每分钟允许大约200个请求,之后我收到了429错误。如果我遇到429错误,我需要做一些Thread.sleep,但是使用async / await,并行线程中可能会有很多请求等待处理,我不知道怎么能让所有人都睡不着持续5秒左右,然后再次恢复。

解决此问题的正确方法是什么?我想听听快速解决方案以及良好的设计解决方案。

2 个答案:

答案 0 :(得分:5)

我不认为这是重复的,如最近标记的那样。另一个SO海报不需要基于时间的滑动窗口(或基于时间的限制),并且那里的答案不包括这种情况。只有当您想要对传出请求设置硬限制时,这才有效。

无论如何,一种准快速解决方案是使用MakeRequestAsync方法进行限制。像这样:

public async Task<HttpResponseMessage> MakeRequestAsync(HttpRequestMessage request)
{            
    //Wait while the limit has been reached. 
    while(!_throttlingHelper.RequestAllowed) 
    {
      await Task.Delay(1000);
    }

    var client = new HttpClient();

    _throttlingHelper.StartRequest();
    var result = await client.SendAsync(request).ConfigureAwait(false);
    _throttlingHelper.EndRequest();

    return result; 
}

ThrottlingHelper只是我现在制作的东西,所以你可能需要稍微调试一下(读取 - 可能无法开箱即用)。 它试图成为一个时间戳滑动窗口。

public class ThrottlingHelper : IDisposable
{
    //Holds time stamps for all started requests
    private readonly List<long> _requestsTx;
    private readonly ReaderWriterLockSlim _lock;

    private readonly int _maxLimit;
    private TimeSpan _interval;

    public ThrottlingHelper(int maxLimit, TimeSpan interval)
    {
        _requestsTx = new List<long>();
        _maxLimit = maxLimit;
        _interval = interval;
        _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
    }

    public bool RequestAllowed
    {
        get
        {
            _lock.EnterReadLock();
            try
            {
                var nowTx = DateTime.Now.Ticks;
                return _requestsTx.Count(tx => nowTx - tx < _interval.Ticks) < _maxLimit;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public void StartRequest()
    {
        _lock.EnterWriteLock();
        try
        {
            _requestsTx.Add(DateTime.Now.Ticks);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void EndRequest()
    {
        _lock.EnterWriteLock();
        try
        {
            var nowTx = DateTime.Now.Ticks;
            _requestsTx.RemoveAll(tx => nowTx - tx >= _interval.Ticks);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {
        _lock.Dispose();
    }
}

您可以将它用作发出请求的类中的成员,并将其实例化为:

_throttlingHelper = new ThrottlingHelper(200, TimeSpan.FromMinutes(1));

当你完成它时,不要忘记处理它。

关于ThrottlingHelper的一些文档:

  1. 构造函数参数是您希望在特定时间间隔内能够执行的最大请求,以及时间间隔本身作为时间跨度。因此,200分钟和1分钟意味着您希望每分钟不超过200个请求。
  2. 属性RequestAllowed可让您知道是否能够使用当前限制设置执行请求。
  3. 方法StartRequest&amp; EndRequest使用当前日期/时间注册/取消注册请求。
  4. 修改/陷阱

    如@PhilipABarnes所示,EndRequest可能会删除仍在进行中的请求。据我所知,这可能发生在两种情况:

    1. 间隔很小,因此请求无法及时完成。
    2. 请求实际上需要的时间超过执行间隔。
    3. 建议的解决方案涉及通过GUID或类似方式实际匹配EndRequestStartRequest次来电。

答案 1 :(得分:0)

如果有多个请求在while循环中等待RequestAllowed,其中一些请求可能同时启动。如何简单的StartRequestIfAllowed?

public class ThrottlingHelper : DisposeBase
{
    //Holds time stamps for all started requests
    private readonly List<long> _requestsTx;
    private readonly Mutex _mutex = new Mutex();

    private readonly int _maxLimit;
    private readonly TimeSpan _interval;

    public ThrottlingHelper(int maxLimit, TimeSpan interval)
    {
        _requestsTx = new List<long>();
        _maxLimit = maxLimit;
        _interval = interval;
    }


    public bool StartRequestIfAllowed
    {
        get
        {
            _mutex.WaitOne();
            try
            {
                var nowTx = DateTime.Now.Ticks;
                if (_requestsTx.Count(tx => nowTx - tx < _interval.Ticks) < _maxLimit)
                {
                    _requestsTx.Add(DateTime.Now.Ticks);
                    return true;
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                _mutex.ReleaseMutex();
            }
        }
    }

    public void EndRequest()
    {
        _mutex.WaitOne();
        try
        {
            var nowTx = DateTime.Now.Ticks;
            _requestsTx.RemoveAll(tx => nowTx - tx >= _interval.Ticks);
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }

    protected override void DisposeResources()
    {
        _mutex.Dispose();
    }
}