Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds InMemoryThrottler for mocking throttler functionality.
Provides interface for throttler.
- Loading branch information
1 parent
db69219
commit 1feace8
Showing
10 changed files
with
199 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
<?php | ||
|
||
namespace Maba\GentleForce\InMemory; | ||
|
||
use Maba\GentleForce\Exception\RateLimitReachedException; | ||
use Maba\GentleForce\IncreaseResult; | ||
use Maba\GentleForce\RateLimitProvider; | ||
use Maba\GentleForce\ThrottlerInterface; | ||
|
||
/** | ||
* Only for testing purposes (mocking) - mimics functionality without external dependencies. | ||
* Current time is also mockable. | ||
* | ||
* This allows writing consistent and fast tests for functionality that uses throttler. | ||
* | ||
* @internal | ||
*/ | ||
class InMemoryThrottler implements ThrottlerInterface | ||
{ | ||
private $rateLimitProvider; | ||
private $microtimeProvider; | ||
|
||
/** | ||
* @var array | ||
*/ | ||
private $storage = []; | ||
|
||
public function __construct(RateLimitProvider $rateLimitProvider, MicrotimeProvider $microtimeProvider) | ||
{ | ||
$this->rateLimitProvider = $rateLimitProvider; | ||
$this->microtimeProvider = $microtimeProvider; | ||
} | ||
|
||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
* @return IncreaseResult | ||
* @throws RateLimitReachedException | ||
*/ | ||
public function checkAndIncrease($useCaseKey, $identifier) | ||
{ | ||
$now = $this->microtimeProvider->getMicrotime(); | ||
$rateLimits = $this->rateLimitProvider->getRateLimits($useCaseKey); | ||
$key = $this->buildKey($useCaseKey, $identifier); | ||
|
||
$totals = []; | ||
$usagesAvailable = []; | ||
$validAfter = 0; | ||
foreach ($rateLimits as $rateLimit) { | ||
$tokensPerUsage = $rateLimit->calculateTokensPerUsage(); | ||
$bucketSize = $rateLimit->calculateBucketSize(); | ||
$subKey = $tokensPerUsage . ':' . $bucketSize; | ||
$total = 0; | ||
$emptyAt = isset($this->storage[$key][$subKey]) ? $this->storage[$key][$subKey] : null; | ||
if ($emptyAt !== null) { | ||
$total = max(0, $emptyAt - $now); | ||
} | ||
$total += $tokensPerUsage; | ||
if ($total > $bucketSize) { | ||
$validAfter = max($validAfter, $total - $bucketSize); | ||
} | ||
|
||
$totals[$subKey] = $total; | ||
$usagesAvailable[$subKey] = $total / $tokensPerUsage; | ||
} | ||
|
||
if ($validAfter > 0) { | ||
throw new RateLimitReachedException($validAfter); | ||
} | ||
|
||
$maxUsagesAvailable = 0; | ||
foreach ($totals as $subKey => $total) { | ||
$this->storage[$key][$subKey] = $now + $total; | ||
$maxUsagesAvailable = max($maxUsagesAvailable, $usagesAvailable[$subKey]); | ||
} | ||
|
||
return new IncreaseResult($this, floor($maxUsagesAvailable), $useCaseKey, $identifier); | ||
} | ||
|
||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
*/ | ||
public function decrease($useCaseKey, $identifier) | ||
{ | ||
$rateLimits = $this->rateLimitProvider->getRateLimits($useCaseKey); | ||
$key = $this->buildKey($useCaseKey, $identifier); | ||
|
||
foreach ($rateLimits as $rateLimit) { | ||
$tokensPerUsage = $rateLimit->calculateTokensPerUsage(); | ||
$bucketSize = $rateLimit->calculateBucketSize(); | ||
$subKey = $tokensPerUsage . ':' . $bucketSize; | ||
if (isset($this->storage[$key][$subKey])) { | ||
$this->storage[$key][$subKey] -= $tokensPerUsage; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
*/ | ||
public function reset($useCaseKey, $identifier) | ||
{ | ||
$key = $this->buildKey($useCaseKey, $identifier); | ||
unset($this->storage[$key]); | ||
} | ||
|
||
private function buildKey($useCaseKey, $identifier) | ||
{ | ||
return $useCaseKey . ':' . $identifier; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace Maba\GentleForce\InMemory; | ||
|
||
class MicrotimeProvider | ||
{ | ||
/** | ||
* @var float | ||
*/ | ||
private $mockedMicrotime; | ||
|
||
/** | ||
* @param float $mockedMicrotime | ||
*/ | ||
public function setMockedMicrotime($mockedMicrotime) | ||
{ | ||
$this->mockedMicrotime = $mockedMicrotime; | ||
} | ||
|
||
public function getMicrotime() | ||
{ | ||
return $this->mockedMicrotime !== null ? $this->mockedMicrotime : microtime(true); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
namespace Maba\GentleForce; | ||
|
||
use Maba\GentleForce\Exception\RateLimitReachedException; | ||
|
||
interface ThrottlerInterface | ||
{ | ||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
* @return IncreaseResult | ||
* @throws RateLimitReachedException | ||
*/ | ||
public function checkAndIncrease($useCaseKey, $identifier); | ||
|
||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
*/ | ||
public function decrease($useCaseKey, $identifier); | ||
|
||
/** | ||
* @param string $useCaseKey configured key for this use case, like "credentials_error_ip" | ||
* @param string $identifier rate-limiting group, like IP address or username | ||
*/ | ||
public function reset($useCaseKey, $identifier); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
namespace Maba\GentleForce\Tests\Functional; | ||
|
||
use Maba\GentleForce\InMemory\InMemoryThrottler; | ||
use Maba\GentleForce\InMemory\MicrotimeProvider; | ||
use Maba\GentleForce\RateLimitProvider; | ||
|
||
class FunctionalInMemoryThrottlerTest extends FunctionalTest | ||
{ | ||
|
||
protected function createThrottler(RateLimitProvider $rateLimitProvider) | ||
{ | ||
return new InMemoryThrottler($rateLimitProvider, new MicrotimeProvider()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters