configuration = $configuration; $this->storage = $storage; } /** * @throws LogicException */ public static function validate(array $params): void { foreach (self::$requirements as $r) { if (!isset($params[$r])) { throw new LogicException($r . ' is required'); } } if (!call_user_func([$params['adapter'], 'supportRateStrategy'])) { throw new InvalidArgumentException(get_class($params['adapter']) . " doesn't support Rate Strategy."); } } public static function create(Storage\AdapterInterface $adapter, Configuration $configuration): StrategyInterface { $serviceNameDecorator = $adapter instanceof Storage\Adapter\TumblingTimeWindowInterface ? self::serviceNameDecorator($configuration->timeWindow()) : null; return new self( $configuration, new Storage( $adapter, $configuration->storageKeys(), $serviceNameDecorator ) ); } public function recordFailure(string $service): int { $this->storage->setLastFailureTime($service, time()); $this->storage->incrementFailureCount($service); if ( $this->storage->getStatus($service) === Ganesha::STATUS_CALMED_DOWN && $this->isClosedInCurrentTimeWindow($service) === false ) { $this->storage->setStatus($service, Ganesha::STATUS_TRIPPED); return Ganesha::STATUS_TRIPPED; } return Ganesha::STATUS_CALMED_DOWN; } public function recordSuccess(string $service): ?int { $this->storage->incrementSuccessCount($service); $status = $this->storage->getStatus($service); if ( $status === Ganesha::STATUS_TRIPPED && $this->isClosedInPreviousTimeWindow($service) ) { $this->storage->setStatus($service, Ganesha::STATUS_CALMED_DOWN); return Ganesha::STATUS_CALMED_DOWN; } return null; } public function reset(): void { $this->storage->reset(); } public function isAvailable(string $service): bool { if ($this->isClosed($service) || $this->isHalfOpen($service)) { return true; } $this->storage->incrementRejectionCount($service); return false; } /** * @throws StorageException * @throws \LogicException */ private function isClosed(string $service): bool { switch (true) { case $this->storage->supportSlidingTimeWindow(): return $this->isClosedInCurrentTimeWindow($service); break; case $this->storage->supportTumblingTimeWindow(): return $this->isClosedInCurrentTimeWindow($service) && $this->isClosedInPreviousTimeWindow($service); break; default: throw new LogicException(sprintf( 'storage adapter should implement %s and/or %s.', Storage\Adapter\SlidingTimeWindowInterface::class, Storage\Adapter\TumblingTimeWindowInterface::class )); break; } } private function isClosedInCurrentTimeWindow(string $service): bool { $failure = $this->storage->getFailureCount($service); if ( $failure === 0 || ($failure / $this->configuration->minimumRequests()) * 100 < $this->configuration->failureRateThreshold() ) { return true; } $success = $this->storage->getSuccessCount($service); $rejection = $this->storage->getRejectionCount($service); return $this->isClosedInTimeWindow($failure, $success, $rejection); } private function isClosedInPreviousTimeWindow(string $service): bool { $failure = $this->storage->getFailureCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow())); if ( $failure === 0 || ($failure / $this->configuration->minimumRequests()) * 100 < $this->configuration->failureRateThreshold() ) { return true; } $success = $this->storage->getSuccessCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow())); $rejection = $this->storage->getRejectionCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow())); return $this->isClosedInTimeWindow($failure, $success, $rejection); } private function isClosedInTimeWindow(int $failure, int $success, int $rejection): bool { if (($failure + $success + $rejection) < $this->configuration->minimumRequests()) { return true; } if (($failure / ($failure + $success)) * 100 < $this->configuration->failureRateThreshold()) { return true; } return false; } /** * @throws StorageException */ private function isHalfOpen(string $service): bool { if (is_null($lastFailureTime = $this->storage->getLastFailureTime($service))) { return false; } if ((time() - $lastFailureTime) > $this->configuration->intervalToHalfOpen()) { $this->storage->setLastFailureTime($service, time()); return true; } return false; } private static function serviceNameDecorator(int $timeWindow, $current = true) { return function ($service) use ($timeWindow, $current) { return sprintf( '%s.%d', $service, $current ? (int)floor(time() / $timeWindow) : (int)floor((time() - $timeWindow) / $timeWindow) ); }; } private static function keyForPreviousTimeWindow(string $service, int $timeWindow) { $f = self::serviceNameDecorator($timeWindow, false); return $f($service); } }