-
Notifications
You must be signed in to change notification settings - Fork 20
sliding window + token bucket adapters #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
d275ce3
feat(sliding-window): add storage-agnostic SlidingWindow adapter cont…
ArnabChatterjee20k 0dbc1f0
feat(sliding-window): add RedisBase with atomic Lua check-and-increment
ArnabChatterjee20k f281091
feat(sliding-window): add Redis adapter
ArnabChatterjee20k d68199f
feat(sliding-window): add RedisCluster adapter with hash-tagged keys
ArnabChatterjee20k 2d15f05
feat(sliding-window): add RedisPool adapter
ArnabChatterjee20k 5daae51
test(sliding-window): add shared adapter test base
ArnabChatterjee20k 4d8c86b
test(sliding-window): add Redis, RedisCluster and RedisPool test cases
ArnabChatterjee20k 47a5459
Merge branch 'main' into strategy/sliding-window
ArnabChatterjee20k 5c016a8
refactor(sliding-window): reduce adapter seams to eval/get/delete pri…
ArnabChatterjee20k bf8336d
fix(sliding-window): validate window/ttl bounds and cache weighted es…
ArnabChatterjee20k 25016c0
refactor(sliding-window): improve window computation and state manage…
ArnabChatterjee20k 252a10d
feat(token-bucket): add agnostic adapter and Redis-family base
ArnabChatterjee20k 1830b61
feat(token-bucket): add Redis, RedisCluster and RedisPool adapters
ArnabChatterjee20k 84f4424
test(token-bucket): add Redis, RedisCluster and RedisPool test cases
ArnabChatterjee20k 70e1e4c
fix(token-bucket): floor available balance before deriving consumed c…
ArnabChatterjee20k 6c152d3
fix(token-bucket): always read a fresh refill estimate in count()
ArnabChatterjee20k 6a011fe
fix(token-bucket): scan every cluster master fully before paginating …
ArnabChatterjee20k e5000d3
feat(sliding-window): add None adapter
ArnabChatterjee20k 4afb6a2
feat(token-bucket): add None adapter
ArnabChatterjee20k 1eecb5e
Merge branch 'strategy/sliding-window' into strategy/token-bucket
ArnabChatterjee20k fba428f
fix(sliding-window): guard None against zero-sized window
ArnabChatterjee20k 49ac83a
fix(sliding-window): drop stale count cache so estimate keeps decaying
ArnabChatterjee20k d75bb51
Merge pull request #122 from utopia-php/strategy/token-bucket
ArnabChatterjee20k File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,100 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Abuse\Adapters; | ||
|
|
||
| use Throwable; | ||
| use Utopia\Abuse\Adapter; | ||
|
|
||
| // counter based sliding window | ||
| abstract class SlidingWindow extends Adapter | ||
| { | ||
| /** | ||
| * @var int | ||
| */ | ||
| protected int $limit = 0; | ||
|
|
||
| /** | ||
| * @var int | ||
| */ | ||
| protected int $timestamp; | ||
|
|
||
| /** | ||
| * Count | ||
| * | ||
| * Read-only weighted estimate of hits in the current sliding window. | ||
| * | ||
| * @param string $key | ||
| * @param int $timestamp | ||
| * @return int | ||
| * | ||
| * @throws \Exception | ||
| */ | ||
| abstract protected function count(string $key, int $timestamp): int; | ||
|
|
||
| /** | ||
| * Check | ||
| * | ||
| * Atomically evaluates the sliding-window estimate and records the request | ||
| * if it is under the limit. Storage backends MUST implement this as a single | ||
| * atomic operation so the read-decide-increment sequence cannot race between | ||
| * concurrent requests. Returns true when the request is abuse (limit reached). | ||
| * limit 0 is equal to unlimited. | ||
| * | ||
| * @return bool | ||
| * | ||
| * @throws \Exception|Throwable | ||
| */ | ||
| abstract public function check(): bool; | ||
|
|
||
| /** | ||
| * Remaining | ||
| * | ||
| * Returns the number of current remaining counts | ||
| * | ||
| * @return int | ||
| * | ||
| * @throws \Exception | ||
| */ | ||
| public function remaining(): int | ||
| { | ||
| $left = $this->limit - ($this->count($this->parseKey(), $this->timestamp) + 1); | ||
|
|
||
| return (0 > $left) ? 0 : $left; | ||
| } | ||
|
|
||
| /** | ||
| * Limit | ||
| * | ||
| * Return the limit integer | ||
| * | ||
| * @return int | ||
| */ | ||
| public function limit(): int | ||
| { | ||
| return $this->limit; | ||
| } | ||
|
|
||
| /** | ||
| * Time | ||
| * | ||
| * Return the timestamp | ||
| * | ||
| * @return int | ||
| */ | ||
| public function time(): int | ||
| { | ||
| return $this->timestamp; | ||
| } | ||
|
|
||
| /** | ||
| * Reset | ||
| * | ||
| * Clear the counters for the current key so the limit starts fresh. | ||
| * Implementations must clear both the current and previous window buckets. | ||
| * | ||
| * @return void | ||
| * | ||
| * @throws \Exception | ||
| */ | ||
| abstract public function reset(): void; | ||
| } |
This file contains hidden or 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,62 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Abuse\Adapters\SlidingWindow; | ||
|
|
||
| use Utopia\Abuse\Adapters\SlidingWindow; | ||
|
|
||
| class None extends SlidingWindow | ||
| { | ||
| /** | ||
| * @param int $ttl Accepted for parity with the storage adapters; unused here | ||
| */ | ||
| public function __construct(string $key, int $limit, int $windowSize, int $ttl) // @phpstan-ignore constructor.unusedParameter | ||
| { | ||
| if ($windowSize <= 0) { | ||
| throw new \InvalidArgumentException('windowSize must be greater than 0'); | ||
| } | ||
|
|
||
| $this->key = $key; | ||
| $this->limit = $limit; | ||
| $now = \time(); | ||
| $this->timestamp = (int) ($now - ($now % $windowSize)); | ||
| } | ||
|
|
||
| protected function count(string $key, int $timestamp): int | ||
| { | ||
| return 0; | ||
| } | ||
|
|
||
| public function check(): bool | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| public function reset(): void | ||
| { | ||
| } | ||
|
|
||
| /** | ||
| * Get abuse logs | ||
| * | ||
| * Return logs with an offset and limit | ||
| * | ||
| * @param int|null $offset | ||
| * @param int|null $limit | ||
| * @return array<string, mixed> | ||
| */ | ||
| public function getLogs(?int $offset = null, ?int $limit = 25): array | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| /** | ||
| * Delete all logs older than $timestamp | ||
| * | ||
| * @param int $timestamp | ||
| * @return bool | ||
| */ | ||
| public function cleanup(int $timestamp): bool | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
This file contains hidden or 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,94 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Abuse\Adapters\SlidingWindow; | ||
|
|
||
| class Redis extends RedisBase | ||
| { | ||
| /** | ||
| * @param string $key Abuse key pattern, e.g. "ip:{ip}"; params are substituted via setParam() | ||
| * @param int $limit Max allowed hits per window; 0 means unlimited | ||
| * @param int $windowSize Length of the rate-limit window in seconds | ||
| * @param int $ttl Lifetime of a bucket key in seconds; must be >= $windowSize so the | ||
| * previous window's bucket survives long enough to be weighted | ||
| * @param \Redis $redis Redis connection used for storage | ||
| */ | ||
| public function __construct(protected string $key, protected int $limit, int $windowSize, int $ttl, protected \Redis $redis) | ||
| { | ||
| $this->initWindow($windowSize, $ttl); | ||
| } | ||
|
|
||
| /** | ||
| * @param string $script | ||
| * @param list<string> $keys | ||
| * @param list<int|float> $argv | ||
| * @return mixed | ||
| * | ||
| * @throws \RedisException | ||
| */ | ||
| protected function eval(string $script, array $keys, array $argv): mixed | ||
| { | ||
| return $this->redis->eval($script, [...$keys, ...$argv], \count($keys)); | ||
| } | ||
|
|
||
| /** | ||
| * @param string $key | ||
| * @return mixed | ||
| * | ||
| * @throws \RedisException | ||
| */ | ||
| protected function get(string $key): mixed | ||
| { | ||
| return $this->redis->get($key); | ||
| } | ||
|
|
||
| /** | ||
| * @param string ...$keys | ||
| * @return void | ||
| * | ||
| * @throws \RedisException | ||
| */ | ||
| protected function delete(string ...$keys): void | ||
| { | ||
| $this->redis->del(...$keys); | ||
| } | ||
|
|
||
| /** | ||
| * Get abuse logs | ||
| * | ||
| * Return logs with an offset and limit | ||
| * | ||
| * @param int|null $offset | ||
| * @param int|null $limit | ||
| * @return array<string, mixed> | ||
| */ | ||
| public function getLogs(?int $offset = null, ?int $limit = 25): array | ||
| { | ||
| $offset = $offset ?? 0; | ||
| $limit = $limit ?? 25; | ||
|
|
||
| $cursor = null; | ||
| $matches = []; | ||
| $pattern = self::NAMESPACE . '__*'; | ||
|
|
||
| do { | ||
| $keys = $this->redis->scan($cursor, $pattern, 100); | ||
| if ($keys !== false) { | ||
| \array_push($matches, ...$keys); | ||
| } | ||
| } while ($cursor > 0); | ||
|
|
||
| \sort($matches); | ||
| $matches = \array_slice($matches, $offset, $limit); | ||
|
|
||
| if (empty($matches)) { | ||
| return []; | ||
| } | ||
|
|
||
| $logs = []; | ||
| foreach ($matches as $key) { | ||
| $logs[$key] = $this->redis->get($key); | ||
| } | ||
|
|
||
| return $logs; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.