CircuitBreaker::halfOpen()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 11
c 2
b 0
f 0
dl 0
loc 19
rs 9.6111
cc 5
nc 4
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Tleckie\CircuitBreaker;
6
7
use Exception;
8
use Psr\SimpleCache\CacheInterface;
9
use Psr\SimpleCache\InvalidArgumentException;
10
use ReflectionException;
11
use Tleckie\CircuitBreaker\Exception\CircuitBreakerException;
12
use Tleckie\CircuitBreaker\Exception\ExceptionFactory;
13
use Tleckie\CircuitBreaker\Exception\ExceptionFactoryInterface;
14
use Tleckie\CircuitBreaker\Exception\Serialized;
15
use function serialize;
16
use function sprintf;
17
use function unserialize;
18
19
/**
20
 * Class CircuitBreaker
21
 * @package Tleckie\CircuitBreaker
22
 */
23
class CircuitBreaker implements CircuitBreakerInterface
24
{
25
    /** @var string */
26
    protected const RETRY_KEY_FORMAT = '%s.retry';
27
28
    /** @var string */
29
    protected const EXCEPTION_KEY_FORMAT = '%s.serialized';
30
31
    /** @var CacheInterface */
32
    protected CacheInterface $cache;
33
34
    /** @var int */
35
    protected int $maxFailures;
36
37
    /** @var int */
38
    protected int $retryTimeout;
39
40
    /** @var ExceptionFactoryInterface */
41
    protected ExceptionFactoryInterface $factory;
42
43
    /**
44
     * CircuitBreaker constructor.
45
     * @param CacheInterface $cache
46
     * @param int $maxFailures
47
     * @param int $retryTimeout
48
     * @param ExceptionFactoryInterface|null $factory
49
     */
50
    public function __construct(
51
        CacheInterface $cache,
52
        int $maxFailures,
53
        int $retryTimeout,
54
        ExceptionFactoryInterface $factory = null
55
    ) {
56
        $this->cache = $cache;
57
        $this->maxFailures = $maxFailures;
58
        $this->retryTimeout = $retryTimeout;
59
        $this->factory = $factory ?? new ExceptionFactory();
60
    }
61
62
    /**
63
     * @inheritdoc
64
     *
65
     * @throws InvalidArgumentException
66
     * @throws Exception
67
     */
68
    public function callService(callable $callable, string $serviceName): mixed
69
    {
70
        $this->checkClosed($serviceName);
71
72
        try {
73
            return $callable();
74
        } catch (CircuitBreakerException $exception) {
75
            $this->halfOpen($exception->getException(), $serviceName);
76
        }
77
    }
78
79
    /**
80
     * @param string $serviceName
81
     *
82
     * @throws InvalidArgumentException
83
     * @throws ReflectionException
84
     * @throws Exception
85
     */
86
    protected function checkClosed(string $serviceName): void
87
    {
88
        $exceptionKey = $this->exception($serviceName);
89
90
        if ($this->cache->has($exceptionKey) &&
91
            ($serialized = unserialize($this->cache->get($exceptionKey))) &&
92
            $serialized instanceof Serialized) {
93
            throw $this->factory->create($serialized);
94
        }
95
    }
96
97
    /**
98
     * @param string $serviceName
99
     * @return string
100
     */
101
    protected function exception(string $serviceName): string
102
    {
103
        return sprintf(static::EXCEPTION_KEY_FORMAT, strtolower($serviceName));
104
    }
105
106
    /**
107
     * @param Exception $exception
108
     * @param string $serviceName
109
     *
110
     * @throws InvalidArgumentException
111
     * @throws Exception
112
     */
113
    protected function halfOpen(Exception $exception, string $serviceName)
114
    {
115
        $counter = 1;
116
        $retryKey = $this->retry($serviceName);
117
118
        if ($this->cache->has($retryKey) && ($counter = $this->cache->get($retryKey)) && $counter) {
119
            $counter++;
120
        }
121
122
        $this->cache->set($retryKey, $counter, 60);
123
124
        if ($counter === $this->maxFailures) {
125
            $this->cache->delete($retryKey);
126
            $value = serialize(new Serialized($exception));
127
            $exceptionKey = $this->exception($serviceName);
128
            $this->cache->set($exceptionKey, $value, $this->retryTimeout);
129
        }
130
131
        throw $exception;
132
    }
133
134
    /**
135
     * @param string $serviceName
136
     *
137
     * @return string
138
     */
139
    protected function retry(string $serviceName): string
140
    {
141
        return sprintf(static::RETRY_KEY_FORMAT, strtolower($serviceName));
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147
    public function setExceptionFactory(ExceptionFactoryInterface $exceptionFactory): CircuitBreakerInterface
148
    {
149
        $this->factory = $exceptionFactory;
150
151
        return $this;
152
    }
153
154
    /**
155
     * @inheritdoc
156
     */
157
    public function getExceptionFactory(): ExceptionFactoryInterface
158
    {
159
        return $this->factory;
160
    }
161
162
    /**
163
     * @inheritdoc
164
     */
165
    public function getCache(): CacheInterface
166
    {
167
        return $this->cache;
168
    }
169
170
    /**
171
     * @inheritdoc
172
     */
173
    public function setCache(CacheInterface $cache): CircuitBreaker
174
    {
175
        $this->cache = $cache;
176
177
        return $this;
178
    }
179
}
180