sunspikes / php-ratelimiter

A framework independent rate limiter for PHP
MIT License
63 stars 12 forks source link

Rate limiting multiple limits for the AWS SES API #8

Closed geoff-ellison closed 7 years ago

geoff-ellison commented 7 years ago

Hi, and thank you for PHP Ratelimiter, which looks like a really comprehensive implementation! I would like to try use it to limit my application's consumption of the AWS Simple Email Service (SES) API to send out emails through the service.

With this service, there are two limits:

We have to stay within both limits. I understand that the daily limit is applied in a moving 24-hour window, ending at the current time of the request. The per-second limit is presumably applied as a measure of throughput. Unfortunately, SES will simply fail messages that are over quota, which means that pro-active rate limiting on the PHP side becomes essential.

My application sends emails by chucking them in a queue, then triggering an asynchronous background worker process (if needed) to deliver what is in the queue, and returning without waiting for a response. This means there could be multiple delivery threads running at once. That's why I think PHP Ratelimiter could be ideal: it doesn't matter how many disparate processes are trying to send, as long as they all go through the limiter it will all work out ok.

But I have questions about implementing PHP Ratelimiter for this:

  1. I'm not sure how I would implement 2 different rate limits in PHP Ratelimiter. Do you perhaps have an example illustrating this?
  2. I'm not clear on which precise method to use to handle these two limits. For the daily limit, would it make sense to do this: $settings = new MovingWindowSettings(50000, 86400);? And for the per-second limit, would a leaky bucket work: $settings = new LeakyBucketSettings(14, 1000, 8);?
  3. AWS will increase our send limits from time to time, and we'd like to take advantage of it. But once a rate limit has been set in PHP Ratelimiter, how do we update it so that the new limits are instantly taken up by all the running workers calling $throttler->access() several time per second?
  4. I'd be implementing this using Redis. I see the memcached example, do you have one for Redis?

I bet there are a number of devs struggling to incorporate Amazon's rate limits into their applications. A comprehensive example covering this use case would be terrific.

Many thanks for your time, and for building this very useful library!

Cheers

Geoff.

Feijs commented 7 years ago

Hi Geoff,

I'll jump in and try to answer your questions:

  1. Simple: use two! Both ratelimits need to be enforced, so just fetch two throttlers and evaluate both. Probably best to put the one which is most likely to hit the limit first. This will "save" the most checks. Something like:
    
    $secondThrottler = $this->ratelimiter->get('aws-second', LeakyBucketSettings(14, 1000, 8));
    $dayThrottler = $this->ratelimiter->get('aws-daily', MovingWindowSettings(5000, 86400));

if (!minuteThrottler->access() || !$dayThrottler->access()) { return; }

// Send mail


2. Seems a good choice. It depends partially on the way the ratelimit is enforced remotely, since that is the limit you want to avoid hitting.
> I understand that the daily limit is applied in a moving 24-hour window, ending at the current time of the request.

Based on this, the MovingWindowThrottler is the correct match. For the second limit a LeakyBucketThrottler seems a good choice, it works well for small (time)windows. 

3. Simply put: you can't. The throttlers hold the setting parameters in runtime. The throttlers themselves are also cached on key by the `Ratelimiter` class. So you'll need to recreate those class instances. 

Do you have long running worker processes? If it is a requirement, some form of updateSettings method would have to be added to the ThrottlerInterface. I'll leave that open for discussion 

4. For details of the cache drivers, I refer to https://github.com/desarrolla2/Cache#predis. The first option named is available through the config, like so:

$cacheFactory = new DesarrollaCacheFactory( null, [ 'driver' => 'redis`, 'redis' => [], } );



~~The second option named, injecting a configured client is not possible in the current code. You might extend `DesarrollaCacheFactory` and override `createRedisDriver()` for that~~ 
For the second option you can't use the DesarrollaCacheFactory, but you can instantiate your own adapter and pass it as a constructor argument to a DesarrollaCacheAdapter