-
-
Notifications
You must be signed in to change notification settings - Fork 46
Conversation
romkatsu
commented
Jan 5, 2020
Q | A |
---|---|
Is bugfix? | ❌ |
New feature? | ✔️ |
Breaks BC? | ❌ |
Tests pass? | ✔️ |
Fixed issues | #63 |
merge base repository
Update fork
Update fork
update fork
update fork
This reverts commit 1b22224.
There was a problem hiding this 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?
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 |
- Method prefix removed - Prefix is added to an interface name instead
$value = $this->getCounterValue(); | ||
$value++; | ||
|
||
$this->storage->set($this->id, $value, $this->interval); |
There was a problem hiding this comment.
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:
- Leaky bucket https://en.wikipedia.org/wiki/Leaky_bucket. That one is in Yii 2.
- Sliding window.
See:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
- A request get counter value (X)
- B request get counter value (X)
- A requrest set counter value (X+1)
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 singleCounter
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')
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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".
Current implementation not allow multiple limit descriptor in a single middleware. It is necessary to record more intervals and more limits, eg.
|
Created a new pull request with the implementation of the GCRA. I'm closing this one. |