diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php index 4188cf4ca3..e306c5d3f8 100644 --- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php +++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php @@ -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); } diff --git a/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php b/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php index e0d29bcc12..4a9ace9ec2 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php @@ -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()); + } }