[RateLimiter] Fix sliding_window misbehaving with stale records

This commit is contained in:
Xesxen 2021-02-09 23:05:56 +01:00 committed by Fabien Potencier
parent ec38bab34a
commit 57033164c6
2 changed files with 43 additions and 3 deletions

View File

@ -63,8 +63,12 @@ final class SlidingWindow implements LimiterStateInterface
public static function createFromPreviousWindow(self $window, int $intervalInSeconds): self
{
$new = new self($window->id, $intervalInSeconds);
$new->hitCountForLastWindow = $window->hitCount;
$new->windowEndAt = $window->windowEndAt + $intervalInSeconds;
$windowEndAt = $window->windowEndAt + $intervalInSeconds;
if (time() < $windowEndAt) {
$new->hitCountForLastWindow = $window->hitCount;
$new->windowEndAt = $windowEndAt;
}
return $new;
}
@ -112,7 +116,7 @@ final class SlidingWindow implements LimiterStateInterface
public function getHitCount(): int
{
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
$percentOfCurrentTimeFrame = (time() - $startOfWindow) / $this->intervalInSeconds;
$percentOfCurrentTimeFrame = min((time() - $startOfWindow) / $this->intervalInSeconds, 1);
return (int) floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame) + $this->hitCount);
}

View File

@ -12,9 +12,13 @@
namespace Symfony\Component\RateLimiter\Tests\Policy;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;
use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
use Symfony\Component\RateLimiter\Policy\SlidingWindow;
/**
* @group time-sensitive
*/
class SlidingWindowTest extends TestCase
{
public function testGetExpirationTime()
@ -36,4 +40,36 @@ class SlidingWindowTest extends TestCase
$this->expectException(InvalidIntervalException::class);
new SlidingWindow('foo', 0);
}
public function testLongInterval()
{
ClockMock::register(SlidingWindow::class);
$window = new SlidingWindow('foo', 60);
$this->assertSame(0, $window->getHitCount());
$window->add(20);
$this->assertSame(20, $window->getHitCount());
sleep(60);
$new = SlidingWindow::createFromPreviousWindow($window, 60);
$this->assertSame(20, $new->getHitCount());
sleep(30);
$this->assertSame(10, $new->getHitCount());
sleep(30);
$this->assertSame(0, $new->getHitCount());
sleep(30);
$this->assertSame(0, $new->getHitCount());
}
public function testLongIntervalCreate()
{
ClockMock::register(SlidingWindow::class);
$window = new SlidingWindow('foo', 60);
sleep(300);
$new = SlidingWindow::createFromPreviousWindow($window, 60);
$this->assertFalse($new->isExpired());
}
}