Skip to content
This repository has been archived by the owner on Jun 29, 2022. It is now read-only.

Rate limiter implementation #203

Closed
wants to merge 19 commits into from
Closed

Rate limiter implementation #203

wants to merge 19 commits into from

Conversation

romkatsu
Copy link
Member

@romkatsu romkatsu commented Jan 5, 2020

Q A
Is bugfix?
New feature? ✔️
Breaks BC?
Tests pass? ✔️
Fixed issues #63

@samdark samdark added the status:code review The pull request needs review. label Jan 5, 2020
@samdark samdark requested a review from a team January 5, 2020 21:10
src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
tests/Middleware/RateLimiterTest.php Outdated Show resolved Hide resolved
@samdark samdark added status:under development Someone is working on a pull request. and removed status:code review The pull request needs review. labels Jan 5, 2020
@romkatsu romkatsu requested review from samdark and removed request for a team January 6, 2020 08:37
@samdark samdark added status:code review The pull request needs review. and removed status:under development Someone is working on a pull request. labels Jan 6, 2020
Copy link
Member

@rustamwin rustamwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without a database repository implementation?

src/Middleware/RateLimiter.php Outdated Show resolved Hide resolved
@romkatsu
Copy link
Member Author

romkatsu commented Jan 6, 2020

Without a database repository implementation?

I didn't plan on it, but I can try. What tasks can this be useful for?

@rustamwin
Copy link
Member

@romkatsu
Copy link
Member Author

romkatsu commented Jan 7, 2020

I didn't plan on it, but I can try. What tasks can this be useful for?

See https://github.com/yiisoft/yii2/blob/master/framework/filters/RateLimiter.php and https://github.com/yiisoft/yii2/blob/master/framework/filters/RateLimitInterface.php

added storage implementation, now it will be possible to add storage in the database

@romkatsu romkatsu requested a review from rustamwin January 7, 2020 13:31
@romkatsu romkatsu requested a review from samdark January 7, 2020 23:05
@samdark samdark requested a review from a team January 7, 2020 23:09
- Method prefix removed
- Prefix is added to an interface name instead
$value = $this->getCounterValue();
$value++;

$this->storage->set($this->id, $value, $this->interval);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it makes code simple, this rate limiter implementation (fixed window) results in possibility of traffic spikes on the edge of the interval. The behavior for interval = 360 (6 minutes) and limit = 10000 is that after 10000 requests there will be 504 HTTP response for exactly 6 minutes. Then there could be quick 10000 requests again and another 6 minute pause etc. So you'll still have traffic spikes this way. That's a no go for a good rate limiter.

Good algorithms preventing traffic spikes are:

  1. Leaky bucket https://en.wikipedia.org/wiki/Leaky_bucket. That one is in Yii 2.
  2. Sliding window.

See:

  1. https://blog.cloudflare.com/counting-things-a-lot-of-different-things/
  2. https://habr.com/ru/post/448438/

Copy link
Member

@samdark samdark Jan 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And counts are lost. for example

  1. A request get counter value (X)
  2. B request get counter value (X)
  3. A requrest set counter value (X+1)
  4. B request set counter value (X+1)

In fact, X + 2. In multi server environments, this can mean a lot of lost counting.
I recommend that the storage interface have a incrementAndGet() method. Redis, mysql allow counting in atomic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, increment operation should be atomic.

@samdark samdark added status:under development Someone is working on a pull request. and removed status:code review The pull request needs review. labels Jan 8, 2020
Copy link
Member

@rustamwin rustamwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All setters should be void or with prefixed

public function withLimit(int $limit): self
{
    $new = clone $this
    $new->limit = $limit;
    return $new;
}

/**
* CacheCounterStorage stores counter value in cache
*/
final class CacheCounterStorage implements CounterStorageInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest naming class/interface without Storage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of it is to store counter data. How would you name it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not about storage, it's about counting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. https://github.com/yiisoft/yii-web/pull/203/files#diff-e94ff4fb5bda21fe7b12afa594018f4c is about counting. This one is about storage. If these should be separate is another question.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why storage? There is no need to read it, it is always only increment, if the result is higher than the limit then it is too many requests

CacheCounter implements CounterInterface // or CounterServiceInterface

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counter in current implementation is useless. Currently, a single Counter can be specified for middleware -> can be implemented to middleware.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 layers:

  • LimiterDescriptor
  • CounterInterface
  • LimiterMiddlerware

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Counter class is not needed. Each counter should be individually

we've already discussed this.

What are possible examples?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counter in current implementation is useless. Currently, a single Counter can be specified for middleware -> can be implemented to middleware.

Why useless? You can also use multiple instances of middleware.

$counter1 = (new Counter($storage))->setInterval(60);
$counter2 = (new Counter($storage))->setInterval(86400);

$middleware1 = (new RateLimiter($counter1, $responseFactory))
    ->withLimit(10);

$middleware2 = (new RateLimiter($counter2, $responseFactory))
    ->withLimit(100);

Route::get('/')
    ->prepend($middleware1)
    ->to(new ActionCaller(SiteController::class, 'index', $container))
    ->name('site/index')

Route::get('/')
   ->prepend($middleware2)
   ->to(new ActionCaller(SiteController::class, 'index', $container))
   ->name('site/test')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why useless? You can also use multiple instances of middleware.

the current implementation is the same as:

$counter = new CacheCounter();
$middleware1 = (new RateLimiter($counter, $responseFactory))
    ->withLimit(10)
    ->withInterval(60);
$middleware1 = (new RateLimiter($counter, $responseFactory))
    ->withLimit(100)
    ->withInterval(86400);

/**
* Counter value storage
*/
interface CounterStorageInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest simplify it

interface CounterInterface
{
  public function incrementAndGet(string $id, int $interval): int;
}


private function generateIdFromRequest(ServerRequestInterface $request): string
{
return strtolower('rate-limiter-' . $request->getMethod() . '-' . $request->getUri()->getPath());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate is not public, the strtolower() can be omitted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query request parameters can be anything, I just want the ID to be the same in terms of case.

*/
final class RateLimiter implements MiddlewareInterface
{
private int $limit = 1000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the default value make sense?

{
$this->id = $this->generateId($request);

if (!$this->storage->has($this->id)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already the task of CounterInterface.


private ResponseFactoryInterface $responseFactory;

private bool $autoincrement = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand the point of leaving auto-increment. If autoincrement is disabled, it is more like just a "checker".

@kamarton
Copy link
Contributor

kamarton commented Jan 9, 2020

Current implementation not allow multiple limit descriptor in a single middleware. It is necessary to record more intervals and more limits, eg.

ID                    interval  limit
sms/user/12             60s     2
sms/user/12           3600s     10
sms/users               60s     40
sms/users             3600s     250

@romkatsu
Copy link
Member Author

Created a new pull request with the implementation of the GCRA.

I'm closing this one.

@romkatsu romkatsu closed this Jan 10, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
status:under development Someone is working on a pull request.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants