5

I have access an API call that accepts a maximum rate of calls per second. If the rate is exceeded, an exception is thrown.

I would like to wrap this call into an abstraction that does the necessary to keep the call rate under the limit. It would act like a network router: handling multiple calls and returning the results to the correct caller caring about the call rate. The goal is to make the calling code as unaware as possible about that limitation. Otherwise, every part in the code having this call would have to be wrapped into a try-catch!

For example: Imagine that you can call a method from an external API that can add 2 numbers. This API can be called 5 times per second. Anything higher than this will result in an exception.

To illustrate the problem, the external service that limits the call rate is like the one in the answer to this question: How to build a rate-limiting API with Observables?

Additional info

Since you don't want the worry about that limit every time you call this method from any part of your code, you think about designing a wrapper method that you could call without worrying about the rate limit. On the inside you care about the limit, but on the outside you expose a simple async method.

It's similar to a web server. How does it return the correct pack of results to the correct customer?

Multiple callers will call this method, and they will get the results as they come. This abstraction should act like a proxy.

How could I do it?

I'm sure the firm of the wrapper method should be like

public async Task<Results> MyMethod()

And inside the method it will perform the logic, maybe using Reactive Extensions (Buffer). I don't know.

But how? I mean, multiple calls to this method should return the results to the correct caller. Is this even possible?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • 1
    The easiest method that comes in mind is using an FiFo (`Queue`) combined with some kind of asynchronus implementation of handling the incomming calls. – lokusking Jul 30 '16 at 16:14
  • 1
    Have you got a strategy for when you constantly have more incoming calls than can be processed? i.e if you get 1000 calls/min for 2 days, should you drop some messages? Should you just fill an unbounded buffer (that may throw OOM ex)? Or have a fixed size buffer that when full, will block any further calls to the API? – Lee Campbell Aug 03 '16 at 01:41
  • I think in my scenario it's quite difficult to keep a high call rate for so long to produce a OOM, so for the moment I would take the 1st strategy. – SuperJMN Aug 03 '16 at 06:09
  • Maybe something based on this? http://wp.sjkp.dk/rate-limiting-with-reactive-extensions-or-linq/ – SuperJMN Aug 06 '16 at 19:05

3 Answers3

8

There are rate limiting libraries available (see Esendex's TokenBucket Github or Nuget).

Usage is very simple, this example would limit polling to 1 a second

// 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();
}

I have also needed to make it async for a project of mine, I simply made an extension method:

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

Using this you wouldn't need to throw/catch exceptions and writing a wrapper becomes fairly trivial

Aaron
  • 188
  • 1
  • 7
  • 1
    FYI - I have forked and republished the TokenBucket to now support .NET Standard 2.0 & .NET 5.0 [here](https://www.nuget.org/packages/CasCap.Apis.TokenBucket). – alv Nov 16 '20 at 06:11
2

What exactly you should depends on your goals and limitations. My assumptions:

  • you want to avoid making requests while the rate limiter is in effect
  • you can't predict whether a specific request would be denied or how exactly will it take for another request to be allowed again
  • you don't need to make multiple request concurrently, and when multiple requests are waiting, it does not matter in which order are they completed

If these assumptions are valid, you could use AsyncAutoResetEvent from AsyncEx: wait for it to be set before making the request, set it after successfully making a request and set it after a delay when it's rate limited.

The code can look like this:

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());
            }
        }
    }
}

Usage:

public static Task<int> Add(int a, int b)
{
    return rateLimitedWrapper.Execute(() => rateLimitingCalculator.Add(a, b));
}
svick
  • 236,525
  • 50
  • 385
  • 514
0

A variant to implement this is to ensure a minimum time between calls, something like the following:

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 */ ...;
}
Andrew Sklyarevsky
  • 2,095
  • 14
  • 17