CircuitBreaker::reportFailure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types = 1);
3
4
5
namespace Paymaxi\Component\CircuitBreaker\Core;
6
7
use Paymaxi\Component\CircuitBreaker\CircuitBreakerInterface;
8
use Paymaxi\Component\CircuitBreaker\Storage\StorageInterface;
9
10
/**
11
 * Allows user code to track availability of any service by serviceName.
12
 *
13
 * @see \Paymaxi\Component\CircuitBreaker\CircuitBreakerInterface
14
 * @package Paymaxi\Component\CircuitBreaker\Components
15
 */
16
class CircuitBreaker implements CircuitBreakerInterface
17
{
18
19
    /**
20
     * @var CircuitBreakerInterface used to load/save availability statistics
21
     */
22
    protected $storageAdapter;
23
24
    /**
25
     * @var int default threshold, if service fails this many times will be disabled
26
     */
27
    protected $defaultMaxFailures;
28
29
    /**
30
     * @var int  how many seconds should we wait before retry
31
     */
32
    protected $defaultRetryTimeout;
33
34
    /**
35
     * Array with configuration per service name, format:
36
     *  array(
37
     *      "serviceName1" => array('maxFailures' => X, 'retryTimeout => Y),
38
     *      "serviceName2" => array('maxFailures' => X, 'retryTimeout => Y),
39
     *  )
40
     *
41
     * @var array[] settings per service name
42
     */
43
    protected $settings = array();
44
45
    /**
46
     * Configure instance with storage implementation and default threshold and retry timeout.
47
     *
48
     * @param StorageInterface $storage storage implementation
49
     * @param int $maxFailures default threshold, if service fails this many times will be disabled
50
     * @param int $retryTimeout how many seconds should we wait before retry
51
     */
52 11
    public function __construct(StorageInterface $storage, $maxFailures = 20, $retryTimeout = 60)
53
    {
54 11
        $this->storageAdapter = $storage;
0 ignored issues
show
Documentation Bug introduced by
It seems like $storage of type object<Paymaxi\Component...orage\StorageInterface> is incompatible with the declared type object<Paymaxi\Component...ircuitBreakerInterface> of property $storageAdapter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
55 11
        $this->defaultMaxFailures = $maxFailures;
56 11
        $this->defaultRetryTimeout = $retryTimeout;
57 11
    }
58
59
    /**
60
     * Use this method only if you want to add server specific threshold and retry timeout.
61
     *
62
     * @param $serviceName
63
     * @param int $maxFailures default threshold, if service fails this many times will be disabled
64
     * @param int $retryTimeout how many seconds should we wait before retry
65
     *
66
     * @return CircuitBreaker
67
     * @internal param StorageInterface $storage storage implementation
68
     */
69 11
    public function setServiceSettings($serviceName, $maxFailures, $retryTimeout)
70
    {
71 11
        $this->settings[$serviceName] = array(
72 11
            'maxFailures'  => $maxFailures ? $maxFailures : $this->defaultMaxFailures,
73 11
            'retryTimeout' => $retryTimeout ? $retryTimeout : $this->defaultRetryTimeout,
74
        );
75
76 11
        return $this;
77
    }
78
79
    // ---------------------- HELPERS -----------------
80
81 8
    public function isAvailable(string $serviceName): bool
82
    {
83 8
        $failures = $this->getFailures($serviceName);
84 8
        $maxFailures = $this->getMaxFailures($serviceName);
85 8
        if ($failures < $maxFailures) {
86
            // this is what happens most of the time so we evaluate first
87 5
            return true;
88
        } else {
89 7
            $lastTest = $this->getLastTest($serviceName);
90 7
            $retryTimeout = $this->getRetryTimeout($serviceName);
91 7
            if ($lastTest + $retryTimeout < time()) {
92
                // Once we wait $retryTimeout, we have to allow one
93
                // thread to try to connect again. To prevent all other threads
94
                // from flooding, the potentially dead db, we update the time first
95
                // and then try to connect. If db is dead only one thread will hang
96
                // waiting for the connection. Others will get updated timeout from stats.
97
                //
98
                // 'Race condition' is between first thread getting into this line and
99
                // time it takes to store the settings. In that time other threads will
100
                // also be entering this statement. Even on very busy servers it
101
                // wont allow more than a few requests to get through before stats are updated.
102
                //
103
                // updating lastTest
104 1
                $this->setFailures($serviceName, $failures);
105
                // allowing this thread to try to connect to the resource
106 1
                return true;
107
            } else {
108 7
                return false;
109
            }
110
        }
111
    }
112
113
    // ---------------------- Directly accessed by interface methods -----------------
114
115
    /**
116
     * @param string $serviceName
117
     *
118
     * @return int
119
     */
120 11
    protected function getFailures(string $serviceName)
121
    {
122
        //FIXME - catch exceptions and replace implementation if occur
123 11
        return (int) $this->storageAdapter->loadStatus($serviceName, 'failures');
0 ignored issues
show
Bug introduced by
The method loadStatus() does not seem to exist on object<Paymaxi\Component...ircuitBreakerInterface>.

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...
124
    }
125
126
    /**
127
     * @param string $serviceName
128
     *
129
     * @return int
130
     */
131 11
    protected function getMaxFailures(string $serviceName)
132
    {
133 11
        return $this->getSetting($serviceName, 'maxFailures');
134
    }
135
136
    /**
137
     * Load setting or initialise service name with defaults for faster lookups
138
     *
139
     * @param string $serviceName what service to look for
140
     * @param string $variable what setting to look for
141
     *
142
     * @return int
143
     */
144 11
    private function getSetting(string $serviceName, string $variable)
145
    {
146
        // make sure there are settings for the service
147 11
        if (!isset($this->settings[$serviceName])) {
148 2
            $this->settings[$serviceName] = array(
149 2
                'maxFailures'  => $this->defaultMaxFailures,
150 2
                'retryTimeout' => $this->defaultRetryTimeout,
151
            );
152
        }
153 11
        return $this->settings[$serviceName][$variable];
154
    }
155
156
    /**
157
     * @param string $serviceName
158
     *
159
     * @return int
160
     */
161 7
    protected function getLastTest(string $serviceName)
162
    {
163
        //FIXME - catch exceptions and replace implementation if occur
164 7
        return (int) $this->storageAdapter->loadStatus($serviceName, 'lastTest');
0 ignored issues
show
Bug introduced by
The method loadStatus() does not seem to exist on object<Paymaxi\Component...ircuitBreakerInterface>.

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...
165
    }
166
167
    /**
168
     * @param string $serviceName
169
     *
170
     * @return int
171
     */
172 7
    protected function getRetryTimeout(string $serviceName)
173
    {
174 7
        return $this->getSetting($serviceName, 'retryTimeout');
175
    }
176
177
178
    /**
179
     * @param string $serviceName
180
     * @param integer $newValue
181
     */
182 8
    protected function setFailures(string $serviceName, $newValue)
183
    {
184
        //FIXME - catch exceptions and replace implementation if occur
185 8
        $this->storageAdapter->saveStatus($serviceName, 'failures', $newValue, false);
0 ignored issues
show
Bug introduced by
The method saveStatus() does not seem to exist on object<Paymaxi\Component...ircuitBreakerInterface>.

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...
186
        // make sure storage adapter flushes changes this time
187 8
        $this->storageAdapter->saveStatus($serviceName, 'lastTest', time(), true);
0 ignored issues
show
Bug introduced by
The method saveStatus() does not seem to exist on object<Paymaxi\Component...ircuitBreakerInterface>.

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...
188 8
    }
189
190
191 8
    public function reportFailure(string $serviceName)
192
    {
193
        // there is no science here, we always increase failures count
194 8
        $this->setFailures($serviceName, $this->getFailures($serviceName) + 1);
195 8
    }
196
197
198 5
    public function reportSuccess(string $serviceName)
199
    {
200 5
        $failures = $this->getFailures($serviceName);
201 5
        $maxFailures = $this->getMaxFailures($serviceName);
202 5
        if ($failures > $maxFailures) {
203
            // there were more failures than max failures
204
            // we have to reset failures count to max-1
205 1
            $this->setFailures($serviceName, $maxFailures - 1);
206 5
        } elseif ($failures > 0) {
207
            // if we are between max and 0 we decrease by 1 on each
208
            // success so we will go down to 0 after some time
209
            // but we are still more sensitive to failures
210 3
            $this->setFailures($serviceName, $failures - 1);
211
        } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
212
            // if there are no failures reported we do not
213
            // have to do anything on success (system operational)
214
        }
215 5
    }
216
217
}
218