diff --git a/composer.json b/composer.json index aa29144..5457476 100755 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "ext-curl": "*", "ext-redis": "*", "utopia-php/database": "^6.0.0", + "utopia-php/circuit-breaker": "0.3.*", "utopia-php/pools": "1.*", "appwrite/appwrite": "^26.0" }, diff --git a/composer.lock b/composer.lock index 1c988fb..ad541f7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bb890a101e6cacd0317970c692b16083", + "content-hash": "081f7ab870b7ac0f095032dc2c42a78e", "packages": [ { "name": "appwrite/appwrite", diff --git a/src/Abuse/Adapters/TimeLimit/RedisPool.php b/src/Abuse/Adapters/TimeLimit/RedisPool.php index a7a4cc8..7058594 100644 --- a/src/Abuse/Adapters/TimeLimit/RedisPool.php +++ b/src/Abuse/Adapters/TimeLimit/RedisPool.php @@ -5,6 +5,7 @@ use RuntimeException; use Throwable; use Utopia\Abuse\Adapters\TimeLimit; +use Utopia\CircuitBreaker\CircuitBreaker; use Utopia\Pools\Pool as UtopiaPool; class RedisPool extends TimeLimit @@ -21,7 +22,8 @@ public function __construct( string $key, int $limit, int $seconds, - protected UtopiaPool $pool + protected UtopiaPool $pool, + protected ?CircuitBreaker $breaker = null ) { $this->key = $key; $this->ttl = $seconds; @@ -30,6 +32,24 @@ public function __construct( $this->limit = $limit; } + /** + * @template T + * @param callable(\Redis|\RedisCluster): T $operation + * @param T $fallback + * @return T + */ + private function guard(callable $operation, mixed $fallback): mixed + { + if ($this->breaker === null) { + return $this->pool->use($operation); + } + + return $this->breaker->call( + open: fn (): mixed => $fallback, + close: fn (): mixed => $this->pool->use($operation), + ); + } + protected function count(string $key, int $timestamp): int { if (0 == $this->limit) { @@ -41,11 +61,11 @@ protected function count(string $key, int $timestamp): int } /** @var int $count */ - $count = $this->pool->use(function (\Redis|\RedisCluster $redis) use ($key, $timestamp): int { + $count = $this->guard(function (\Redis|\RedisCluster $redis) use ($key, $timestamp): int { $count = $redis->get(Redis::NAMESPACE . '__' . $key . '__' . $timestamp); return \is_numeric($count) ? (int) $count : 0; - }); + }, 0); $this->count = $count; @@ -61,7 +81,7 @@ protected function hit(string $key, int $timestamp): void $ttl = $this->ttl; $key = Redis::NAMESPACE . '__' . $key . '__' . $timestamp; - $this->pool->use(function (\Redis|\RedisCluster $redis) use ($key, $ttl): void { + $this->guard(function (\Redis|\RedisCluster $redis) use ($key, $ttl): void { $redis->multi(); try { $redis->incr($key); @@ -76,7 +96,7 @@ protected function hit(string $key, int $timestamp): void $this->discard($redis); throw new RuntimeException('Redis transaction failed.'); } - }); + }, null); $this->count = ($this->count ?? 0) + 1; } @@ -86,7 +106,7 @@ protected function set(string $key, int $timestamp, int $value): void $ttl = $this->ttl; $key = Redis::NAMESPACE . '__' . $key . '__' . $timestamp; - $this->pool->use(function (\Redis|\RedisCluster $redis) use ($key, $ttl, $value): void { + $this->guard(function (\Redis|\RedisCluster $redis) use ($key, $ttl, $value): void { $redis->multi(); try { $redis->set($key, (string) $value); @@ -101,7 +121,7 @@ protected function set(string $key, int $timestamp, int $value): void $this->discard($redis); throw new RuntimeException('Redis transaction failed.'); } - }); + }, null); $this->count = $value; } @@ -121,7 +141,7 @@ public function getLogs(?int $offset = null, ?int $limit = 25): array $limit = $limit ?? 25; /** @var array $result */ - $result = $this->pool->use(function (\Redis|\RedisCluster $redis) use ($offset, $limit): array { + $result = $this->guard(function (\Redis|\RedisCluster $redis) use ($offset, $limit): array { if ($redis instanceof \RedisCluster) { return $this->getRedisClusterLogs($redis, $offset, $limit); } @@ -150,7 +170,7 @@ public function getLogs(?int $offset = null, ?int $limit = 25): array } return $logs; - }); + }, []); return $result; } diff --git a/tests/Abuse/RedisPoolCircuitBreakerTest.php b/tests/Abuse/RedisPoolCircuitBreakerTest.php new file mode 100644 index 0000000..31c16e6 --- /dev/null +++ b/tests/Abuse/RedisPoolCircuitBreakerTest.php @@ -0,0 +1,37 @@ +setReconnectAttempts(1) + ->setRetryAttempts(1); + + /** @var Pool<\Redis> $pool */ + $adapter = new RedisPool( + 'redis-pool-unavailable', + 1, + 60, + $pool, + new CircuitBreaker() + ); + + $abuse = new Abuse($adapter); + + $this->assertFalse($abuse->check()); + $this->assertSame([], $adapter->getLogs()); + } +}