2016-07-30 76 views
4

我可以访问一个API调用,接受每秒最大调用速率。如果超过速率,则会抛出异常环绕速率限制API调用

我想把这个调用包装成一个抽象,这个抽象把通话费率保持在限制之下。它将像网络路由器一样工作:处理多个呼叫并将结果返回给关注呼叫率的正确呼叫者。目标是使呼叫代码尽可能不知道该限制。否则,具有此调用的代码中的每个部分都必须包装到try-catch中!

例如:想象一下,您可以从可以添加2个数字的extern API调用方法。这个API可以称为每秒5次。任何高于此的值都会导致异常。

为了说明问题,限制呼叫速率的外部服务就像是一个在回答这个问题

How to build a rate-limiting API with Observables?

附加信息:

既然你不每次从代码的任何部分调用此方法时都不需要担心该限制,因此您考虑设计一种可以调用的包装器方法,而不必担心速率限制。在内部你关心的限制,但在外面你公开一个简单的异步方法。

它类似于网络服务器。它如何将正确的结果包返回给正确的客户?

多个来电者会调用这个方法,他们会在他们来的时候得到结果。这种抽象应该像一个代理。

我该怎么办?

我敢肯定,包装方法的公司应该像

public async Task<Results> MyMethod() 

而且里面的方法也将执行逻辑,也许使用无扩展(缓冲)。我不知道。

但是如何?我的意思是,对这个方法的多次调用应该将结果返回给正确的调用者。这甚至有可能吗?

非常感谢!

+1

想到最简单的方法是使用FiFo('Queue')和某种异步我们实施处理呼叫。 – lokusking

+1

当你有更多的来电比可以处理的时候,你有没有策略?即如果您在两天内获得1000次通话/分钟,您是否应该放弃一些消息?如果你只是填充一个无界缓冲区(可能会抛出OOM)?或者有一个固定大小的缓冲区,当它满的时候,会阻止对API的进一步调用? –

+0

我认为在我的情况下,要长时间保持高通话率才能生成OOM是相当困难的,所以目前我会采取第一种策略。 – SuperJMN

回答

4

有可用的速率限制库(见Esendex的TokenBucket GithubNuget)。

用法很简单,这个例子将限制轮询1第二

// Create a token bucket with a capacity of 1 token that refills at a fixed interval of 1 token/sec. 
ITokenBucket bucket = TokenBuckets.Construct() 
    .WithCapacity(1) 
    .WithFixedIntervalRefillStrategy(1, TimeSpan.FromSeconds(1)) 
    .Build(); 

// ... 

while (true) 
{ 
    // Consume a token from the token bucket. If a token is not available this method will block until 
    // the refill strategy adds one to the bucket. 
    bucket.Consume(1); 

    Poll(); 
} 

我还需要使异步我的一个项目,我只是做了扩展方法:

public static class TokenBucketExtensions 
{ 
    public static Task ConsumeAsync(this ITokenBucket tokenBucket) 
    { 
     return Task.Factory.StartNew(tokenBucket.Consume); 
    } 
} 

使用这个你不需要抛出/捕获异常并且编写一个包装器变得相当简单

0

实现这个变体是保证通话之间的最小时间,类似如下:

private readonly Object syncLock = new Object(); 
private readonly TimeSpan minTimeout = TimeSpan.FromSeconds(5); 
private volatile DateTime nextCallDate = DateTime.MinValue; 

public async Task<Result> RequestData(...) { 
    DateTime possibleCallDate = DateTime.Now; 
    lock(syncLock) { 
     // When is it possible to make the next call? 
     if (nextCallDate > possibleCallDate) { 
      possibleCallDate = nextCallDate; 
     } 
     nextCallDate = possibleCallDate + minTimeout; 
    } 

    TimeSpan waitingTime = possibleCallDate - DateTime.Now; 
    if (waitingTime > TimeSpan.Zero) { 
     await Task.Delay(waitingTime); 
    } 

    return await ... /* the actual call to API */ ...; 
} 
+2

你不能在'lock'中使用'await'。 – svick

+0

哎呀,谢谢你,@svick,我编辑了答案,以避免'lock'里面的'await'。然而,思想保持不变 - 计算下一个可能呼叫的日期,等待它,然后进行呼叫。 –

0

究竟你应该取决于你的目标和限制。我的假设:

  • 你希望避免的请求,而速率限制实际上是
  • 你无法预测一个特定的请求是否会被拒绝,否则将究竟如何承担再次被允许另一个请求
  • 你不需要让多个请求同时,当有多个请求正在等待,不要紧的次序是他们完成

如果这些假设是正确的,你可以使用AsyncAutoResetEvent from AsyncEx:等待它将在maki之前设置在请求成功之后设置它,并且在速率受限时延迟设置它。

的代码可以是这样的:

class RateLimitedWrapper<TException> where TException : Exception 
{ 
    private readonly AsyncAutoResetEvent autoResetEvent = new AsyncAutoResetEvent(set: true); 

    public async Task<T> Execute<T>(Func<Task<T>> func) 
    { 
     while (true) 
     { 
      try 
      { 
       await autoResetEvent.WaitAsync(); 

       var result = await func(); 

       autoResetEvent.Set(); 

       return result; 
      } 
      catch (TException) 
      { 
       var ignored = Task.Delay(500).ContinueWith(_ => autoResetEvent.Set()); 
      } 
     } 
    } 
} 

用法:

public static Task<int> Add(int a, int b) 
{ 
    return rateLimitedWrapper.Execute(() => rateLimitingCalculator.Add(a, b)); 
}