ackintosh /
ganesha
| 1 | <?php |
||||||
| 2 | namespace Ackintosh\Ganesha\Strategy; |
||||||
| 3 | |||||||
| 4 | use Ackintosh\Ganesha; |
||||||
| 5 | use Ackintosh\Ganesha\Configuration; |
||||||
| 6 | use Ackintosh\Ganesha\Exception\StorageException; |
||||||
| 7 | use Ackintosh\Ganesha\Storage; |
||||||
| 8 | use Ackintosh\Ganesha\StrategyInterface; |
||||||
| 9 | use InvalidArgumentException; |
||||||
| 10 | use LogicException; |
||||||
| 11 | |||||||
| 12 | class Rate implements StrategyInterface |
||||||
| 13 | { |
||||||
| 14 | /** |
||||||
| 15 | * @var Configuration |
||||||
| 16 | */ |
||||||
| 17 | private $configuration; |
||||||
| 18 | |||||||
| 19 | /** |
||||||
| 20 | * @var Storage |
||||||
| 21 | */ |
||||||
| 22 | private $storage; |
||||||
| 23 | |||||||
| 24 | /** |
||||||
| 25 | * @var array |
||||||
| 26 | */ |
||||||
| 27 | private static $requirements = [ |
||||||
| 28 | 'adapter', |
||||||
| 29 | 'failureRateThreshold', |
||||||
| 30 | 'intervalToHalfOpen', |
||||||
| 31 | 'minimumRequests', |
||||||
| 32 | 'timeWindow', |
||||||
| 33 | ]; |
||||||
| 34 | |||||||
| 35 | /** |
||||||
| 36 | * @param Configuration $configuration |
||||||
| 37 | */ |
||||||
| 38 | private function __construct(Configuration $configuration, Storage $storage) |
||||||
| 39 | { |
||||||
| 40 | $this->configuration = $configuration; |
||||||
| 41 | $this->storage = $storage; |
||||||
| 42 | } |
||||||
| 43 | |||||||
| 44 | /** |
||||||
| 45 | * @param array $params |
||||||
| 46 | * @throws LogicException |
||||||
| 47 | */ |
||||||
| 48 | public static function validate(array $params): void |
||||||
| 49 | { |
||||||
| 50 | foreach (self::$requirements as $r) { |
||||||
| 51 | if (!isset($params[$r])) { |
||||||
| 52 | throw new LogicException($r . ' is required'); |
||||||
| 53 | } |
||||||
| 54 | } |
||||||
| 55 | |||||||
| 56 | if (!call_user_func([$params['adapter'], 'supportRateStrategy'])) { |
||||||
| 57 | throw new InvalidArgumentException(get_class($params['adapter']) . " doesn't support Rate Strategy."); |
||||||
| 58 | } |
||||||
| 59 | } |
||||||
| 60 | |||||||
| 61 | /** |
||||||
| 62 | * @param Configuration $configuration |
||||||
| 63 | * @return Rate |
||||||
| 64 | */ |
||||||
| 65 | public static function create(Configuration $configuration): StrategyInterface |
||||||
| 66 | { |
||||||
| 67 | $serviceNameDecorator = $configuration['adapter'] instanceof Storage\Adapter\TumblingTimeWindowInterface ? self::serviceNameDecorator($configuration['timeWindow']) : null; |
||||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||||
| 68 | $adapter = $configuration['adapter']; |
||||||
| 69 | $adapter->setConfiguration($configuration); |
||||||
|
0 ignored issues
–
show
The method
setConfiguration() does not exist on null.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 70 | |||||||
| 71 | return new self( |
||||||
| 72 | $configuration, |
||||||
| 73 | new Storage( |
||||||
| 74 | $adapter, |
||||||
|
0 ignored issues
–
show
It seems like
$adapter can also be of type null; however, parameter $adapter of Ackintosh\Ganesha\Storage::__construct() does only seem to accept Ackintosh\Ganesha\Storage\AdapterInterface, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 75 | $configuration['storageKeys'], |
||||||
|
0 ignored issues
–
show
It seems like
$configuration['storageKeys'] can also be of type null; however, parameter $storageKeys of Ackintosh\Ganesha\Storage::__construct() does only seem to accept Ackintosh\Ganesha\Storage\StorageKeysInterface, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 76 | $serviceNameDecorator |
||||||
| 77 | ) |
||||||
| 78 | ); |
||||||
| 79 | } |
||||||
| 80 | |||||||
| 81 | /** |
||||||
| 82 | * @param string $service |
||||||
| 83 | * @return int |
||||||
| 84 | */ |
||||||
| 85 | public function recordFailure(string $service): int |
||||||
| 86 | { |
||||||
| 87 | $this->storage->setLastFailureTime($service, time()); |
||||||
| 88 | $this->storage->incrementFailureCount($service); |
||||||
| 89 | if ( |
||||||
| 90 | $this->storage->getStatus($service) === Ganesha::STATUS_CALMED_DOWN |
||||||
| 91 | && $this->isClosedInCurrentTimeWindow($service) === false |
||||||
| 92 | ) { |
||||||
| 93 | $this->storage->setStatus($service, Ganesha::STATUS_TRIPPED); |
||||||
| 94 | return Ganesha::STATUS_TRIPPED; |
||||||
| 95 | } |
||||||
| 96 | |||||||
| 97 | return Ganesha::STATUS_CALMED_DOWN; |
||||||
| 98 | } |
||||||
| 99 | |||||||
| 100 | /** |
||||||
| 101 | * @param string $service |
||||||
| 102 | * @return int |
||||||
| 103 | */ |
||||||
| 104 | public function recordSuccess(string $service): ?int |
||||||
| 105 | { |
||||||
| 106 | $this->storage->incrementSuccessCount($service); |
||||||
| 107 | $status = $this->storage->getStatus($service); |
||||||
| 108 | if ( |
||||||
| 109 | $status === Ganesha::STATUS_TRIPPED |
||||||
| 110 | && $this->isClosedInPreviousTimeWindow($service) |
||||||
| 111 | ) { |
||||||
| 112 | $this->storage->setStatus($service, Ganesha::STATUS_CALMED_DOWN); |
||||||
| 113 | return Ganesha::STATUS_CALMED_DOWN; |
||||||
| 114 | } |
||||||
| 115 | |||||||
| 116 | return null; |
||||||
| 117 | } |
||||||
| 118 | |||||||
| 119 | /** |
||||||
| 120 | * @return void |
||||||
| 121 | */ |
||||||
| 122 | public function reset(): void |
||||||
| 123 | { |
||||||
| 124 | $this->storage->reset(); |
||||||
| 125 | } |
||||||
| 126 | |||||||
| 127 | /** |
||||||
| 128 | * @param string $service |
||||||
| 129 | * @return bool |
||||||
| 130 | */ |
||||||
| 131 | public function isAvailable(string $service): bool |
||||||
| 132 | { |
||||||
| 133 | if ($this->isClosed($service) || $this->isHalfOpen($service)) { |
||||||
| 134 | return true; |
||||||
| 135 | } |
||||||
| 136 | |||||||
| 137 | $this->storage->incrementRejectionCount($service); |
||||||
| 138 | return false; |
||||||
| 139 | } |
||||||
| 140 | |||||||
| 141 | /** |
||||||
| 142 | * @param string $service |
||||||
| 143 | * @return bool |
||||||
| 144 | * @throws StorageException, \LogicException |
||||||
| 145 | */ |
||||||
| 146 | private function isClosed(string $service): bool |
||||||
| 147 | { |
||||||
| 148 | switch (true) { |
||||||
| 149 | case $this->storage->supportSlidingTimeWindow(): |
||||||
| 150 | return $this->isClosedInCurrentTimeWindow($service); |
||||||
| 151 | break; |
||||||
|
0 ignored issues
–
show
break is not strictly necessary here and could be removed.
The switch ($x) {
case 1:
return 'foo';
break; // This break is not necessary and can be left off.
}
If you would like to keep this construct to be consistent with other Loading history...
|
|||||||
| 152 | case $this->storage->supportTumblingTimeWindow(): |
||||||
| 153 | return $this->isClosedInCurrentTimeWindow($service) && $this->isClosedInPreviousTimeWindow($service); |
||||||
| 154 | break; |
||||||
| 155 | default: |
||||||
| 156 | throw new LogicException(sprintf( |
||||||
| 157 | 'storage adapter should implement %s and/or %s.', |
||||||
| 158 | Storage\Adapter\SlidingTimeWindowInterface::class, |
||||||
| 159 | Storage\Adapter\TumblingTimeWindowInterface::class |
||||||
| 160 | )); |
||||||
| 161 | break; |
||||||
| 162 | } |
||||||
| 163 | } |
||||||
| 164 | |||||||
| 165 | /** |
||||||
| 166 | * @param string $service |
||||||
| 167 | * @return bool |
||||||
| 168 | */ |
||||||
| 169 | private function isClosedInCurrentTimeWindow(string $service): bool |
||||||
| 170 | { |
||||||
| 171 | $failure = $this->storage->getFailureCount($service); |
||||||
| 172 | if ( |
||||||
| 173 | $failure === 0 |
||||||
| 174 | || ($failure / $this->configuration['minimumRequests']) * 100 < $this->configuration['failureRateThreshold'] |
||||||
| 175 | ) { |
||||||
| 176 | return true; |
||||||
| 177 | } |
||||||
| 178 | |||||||
| 179 | $success = $this->storage->getSuccessCount($service); |
||||||
| 180 | $rejection = $this->storage->getRejectionCount($service); |
||||||
| 181 | |||||||
| 182 | return $this->isClosedInTimeWindow($failure, $success, $rejection); |
||||||
| 183 | } |
||||||
| 184 | |||||||
| 185 | /** |
||||||
| 186 | * @param string $service |
||||||
| 187 | * @return bool |
||||||
| 188 | */ |
||||||
| 189 | private function isClosedInPreviousTimeWindow(string $service): bool |
||||||
| 190 | { |
||||||
| 191 | $failure = $this->storage->getFailureCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration['timeWindow'])); |
||||||
|
0 ignored issues
–
show
It seems like
$this->configuration['timeWindow'] can also be of type null; however, parameter $timeWindow of Ackintosh\Ganesha\Strate...ForPreviousTimeWindow() does only seem to accept integer, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 192 | if ( |
||||||
| 193 | $failure === 0 |
||||||
| 194 | || ($failure / $this->configuration['minimumRequests']) * 100 < $this->configuration['failureRateThreshold'] |
||||||
| 195 | ) { |
||||||
| 196 | return true; |
||||||
| 197 | } |
||||||
| 198 | |||||||
| 199 | $success = $this->storage->getSuccessCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration['timeWindow'])); |
||||||
| 200 | $rejection = $this->storage->getRejectionCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration['timeWindow'])); |
||||||
| 201 | |||||||
| 202 | return $this->isClosedInTimeWindow($failure, $success, $rejection); |
||||||
| 203 | } |
||||||
| 204 | |||||||
| 205 | /** |
||||||
| 206 | * @param int $failure |
||||||
| 207 | * @param int $success |
||||||
| 208 | * @param int $rejection |
||||||
| 209 | * @return bool |
||||||
| 210 | */ |
||||||
| 211 | private function isClosedInTimeWindow(int $failure, int $success, int $rejection): bool |
||||||
| 212 | { |
||||||
| 213 | if (($failure + $success + $rejection) < $this->configuration['minimumRequests']) { |
||||||
| 214 | return true; |
||||||
| 215 | } |
||||||
| 216 | |||||||
| 217 | if (($failure / ($failure + $success)) * 100 < $this->configuration['failureRateThreshold']) { |
||||||
| 218 | return true; |
||||||
| 219 | } |
||||||
| 220 | |||||||
| 221 | return false; |
||||||
| 222 | } |
||||||
| 223 | |||||||
| 224 | /** |
||||||
| 225 | * @param string $service |
||||||
| 226 | * @return bool |
||||||
| 227 | * @throws StorageException |
||||||
| 228 | */ |
||||||
| 229 | private function isHalfOpen(string $service): bool |
||||||
| 230 | { |
||||||
| 231 | if (is_null($lastFailureTime = $this->storage->getLastFailureTime($service))) { |
||||||
| 232 | return false; |
||||||
| 233 | } |
||||||
| 234 | |||||||
| 235 | if ((time() - $lastFailureTime) > $this->configuration['intervalToHalfOpen']) { |
||||||
| 236 | $this->storage->setLastFailureTime($service, time()); |
||||||
| 237 | return true; |
||||||
| 238 | } |
||||||
| 239 | |||||||
| 240 | return false; |
||||||
| 241 | } |
||||||
| 242 | |||||||
| 243 | private static function serviceNameDecorator(int $timeWindow, $current = true) |
||||||
| 244 | { |
||||||
| 245 | return function ($service) use ($timeWindow, $current) { |
||||||
| 246 | return sprintf( |
||||||
| 247 | '%s.%d', |
||||||
| 248 | $service, |
||||||
| 249 | $current ? (int)floor(time() / $timeWindow) : (int)floor((time() - $timeWindow) / $timeWindow) |
||||||
| 250 | ); |
||||||
| 251 | }; |
||||||
| 252 | } |
||||||
| 253 | |||||||
| 254 | private static function keyForPreviousTimeWindow(string $service, int $timeWindow) |
||||||
| 255 | { |
||||||
| 256 | $f = self::serviceNameDecorator($timeWindow, false); |
||||||
| 257 | return $f($service); |
||||||
| 258 | } |
||||||
| 259 | } |
||||||
| 260 |