Lines
76.92%
20 / 26
Methods
53.84%
7 / 13
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| read | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| write | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| remove | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| exists | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| increment | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| keysWithPrefix | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| removeByPrefix | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| probe | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| getHealth | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| runGuarded | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 4 | ||
| pauseAfter | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 1 | ||
| isPaused | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| 15 | final class GuardedStoreBackend implements StoreBackend | |
| 16 | { | |
| 17 | private const array PAUSING_FAILURES = [StoreError::Unreachable, StoreError::TimedOut]; | |
| 18 | ||
| 19 | private ?MonotonicTime $pausedUntil = null; | |
| 20 | ||
| 21 | private bool $lastOperationAnswered = true; | |
| 22 | ||
| 23 | private ?string $lastFailure = null; | |
| 24 | ||
| 25 | private ?Instant $lastFailureAt = null; | |
| 26 | ||
| 27 | public function __construct( | |
| 28 | private readonly StoreBackend $backend, | |
| 29 | private readonly StoreDsn $dsn, | |
| 30 | private readonly StoreTiming $timing, | |
| 31 | private readonly Clock $clock, | |
| 32 | ) {} | |
| 33 | ||
| 34 | public function read(string $key): ?string | |
| 35 | { | |
| 36 | return $this->runGuarded(fn(): ?string => $this->backend->read($key)); | |
| 37 | } | |
| 38 | ||
| 39 | public function write(string $key, string $value, ?Duration $ttl): void | |
| 40 | { | |
| 41 | $this->runGuarded(fn() => $this->backend->write($key, $value, $ttl)); | |
| 42 | } | |
| 43 | ||
| 44 | public function remove(string $key): void | |
| 45 | { | |
| 46 | $this->runGuarded(fn() => $this->backend->remove($key)); | |
| 47 | } | |
| 48 | ||
| 49 | public function exists(string $key): bool | |
| 50 | { | |
| 51 | return $this->runGuarded(fn(): bool => $this->backend->exists($key)); | |
| 52 | } | |
| 53 | ||
| 54 | public function increment(string $key, int $by): int | |
| 55 | { | |
| 56 | return $this->runGuarded(fn(): int => $this->backend->increment($key, $by)); | |
| 57 | } | |
| 58 | ||
| 59 | public function keysWithPrefix(string $prefix): array | |
| 60 | { | |
| 61 | return $this->runGuarded(fn(): array => $this->backend->keysWithPrefix($prefix)); | |
| 62 | } | |
| 63 | ||
| 64 | public function removeByPrefix(string $prefix): void | |
| 65 | { | |
| 66 | $this->runGuarded(fn() => $this->backend->removeByPrefix($prefix)); | |
| 67 | } | |
| 68 | ||
| 69 | public function probe(): void | |
| 70 | { | |
| 71 | $this->runGuarded($this->backend->probe(...)); | |
| 72 | } | |
| 73 | ||
| 74 | public function getHealth(): StoreHealth | |
| 75 | { | |
| 76 | return new StoreHealth($this->lastOperationAnswered, $this->lastFailure, $this->lastFailureAt); | |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * @template T | |
| 81 | * @param Closure(): T $operation | |
| 82 | * @return T | |
| 83 | * @throws StoreException | |
| 84 | */ | |
| 85 | private function runGuarded(Closure $operation): mixed | |
| 86 | { | |
| 87 | if ($this->isPaused()) { | |
| 88 | throw StoreException::createForReason(StoreError::RecentlyFailed, target: (string) $this->dsn, detail: $this->lastFailure ?? ''); | |
| 89 | } | |
| 90 | ||
| 91 | try { | |
| 92 | $result = $operation(); | |
| 93 | } catch (StoreException $e) { | |
| 94 | if (\in_array($e->reason, self::PAUSING_FAILURES, true)) { | |
| 95 | $this->pauseAfter($e); | |
| 96 | } else { | |
| 97 | $this->lastOperationAnswered = true; | |
| 98 | } | |
| 99 | ||
| 100 | throw $e; | |
| 101 | } | |
| 102 | ||
| 103 | $this->pausedUntil = null; | |
| 104 | $this->lastOperationAnswered = true; | |
| 105 | ||
| 106 | return $result; | |
| 107 | } | |
| 108 | ||
| 109 | private function pauseAfter(StoreException $failure): void | |
| 110 | { | |
| 111 | $this->pausedUntil = $this->clock->getMonotonicTime()->plus($this->timing->recoveryInterval); | |
| 112 | $this->lastOperationAnswered = false; | |
| 113 | $this->lastFailure = $failure->getMessage(); | |
| 114 | $this->lastFailureAt = $this->clock->getNow(); | |
| 115 | } | |
| 116 | ||
| 117 | private function isPaused(): bool | |
| 118 | { | |
| 119 | return $this->pausedUntil?->isAfter($this->clock->getMonotonicTime()) === true; | |
| 120 | } | |
| 121 | } |