GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 5c076b...c6f8b5 )
by Rémi
10s
created

RedLock::checkQuorum()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
crap 2
1
<?php
2
3
namespace RemiSan\Lock\Implementations;
4
5
use RemiSan\Lock\Exceptions\LockingException;
6
use RemiSan\Lock\Exceptions\UnlockingException;
7
use RemiSan\Lock\Lock;
8
use RemiSan\Lock\Locker;
9
use RemiSan\Lock\TokenGenerator;
10
use Symfony\Component\Stopwatch\Stopwatch;
11
12
final class RedLock implements Locker
13
{
14
    /** @var float */
15
    const CLOCK_DRIFT_FACTOR = 0.01;
16
17
    /** @var \Redis[] */
18
    private $instances = [];
19
20
    /** @var TokenGenerator */
21
    private $tokenGenerator;
22
23
    /** @var Stopwatch */
24
    private $stopwatch;
25
26
    /** @var int */
27
    private $quorum;
28
29
    /**
30
     * @param \Redis[]       $instances      Array of pre-connected \Redis objects
31
     * @param TokenGenerator $tokenGenerator The token generator
32
     * @param Stopwatch      $stopwatch      A way to measure time passed
33
     */
34 39
    public function __construct(
35
        array $instances,
36
        TokenGenerator $tokenGenerator,
37
        Stopwatch $stopwatch
38
    ) {
39 39
        $this->setInstances($instances);
40 39
        $this->setQuorum();
41
42 39
        $this->tokenGenerator = $tokenGenerator;
43 39
        $this->stopwatch = $stopwatch;
44 39
    }
45
46
    /**
47
     * {@inheritdoc}
48
     */
49 12
    public function lock($resource, $ttl = null, $retryDelay = 0, $retryCount = 0)
50
    {
51 12
        $lock = new Lock($resource, $this->tokenGenerator->generateToken());
52
53 12
        $tried = 0;
54 12
        while (true) {
55
            try {
56 12
                return $this->monitoredLockingOfAllInstances($lock, $ttl);
57 6
            } catch (LockingException $e) {
58 6
                $this->resetLock($lock);
59
            }
60
61 6
            if ($tried++ === $retryCount) {
62 6
                break;
63
            }
64
65 6
            $this->waitBeforeRetrying($retryDelay);
66 4
        }
67
68 6
        throw new LockingException();
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 9
    public function isResourceLocked($resource)
75
    {
76 9
        foreach ($this->instances as $instance) {
77 9
            if ($this->isInstanceResourceLocked($instance, $resource)) {
78 7
                return true;
79
            }
80 4
        }
81
82 3
        return false;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 12
    public function unlock(Lock $lock)
89
    {
90 12
        foreach ($this->instances as $instance) {
91 12
            if (!$this->unlockInstance($instance, $lock)) {
92 9
                if ($this->isInstanceResourceLocked($instance, $lock->getResource())) {
93 8
                    throw new UnlockingException(); // Only throw an exception if the lock is still present
94
                }
95 2
            }
96 6
        }
97 6
    }
98
99
    /**
100
     * @param Lock $lock
101
     * @param int  $ttl
102
     *
103
     * @throws LockingException
104
     *
105
     * @return Lock
106
     */
107 12
    private function monitoredLockingOfAllInstances(Lock $lock, $ttl)
108
    {
109 12
        $timeMeasure = $this->stopwatch->start($lock->getToken());
110 12
        $instancesLocked = $this->lockInstances($lock, $ttl);
111 12
        $timeMeasure->stop();
112
113 12
        $this->checkQuorum($instancesLocked);
114
115 9
        if ($ttl) {
116 6
            self::checkTtl($timeMeasure->getDuration(), $ttl);
117 3
            $lock->setValidityTimeEnd($timeMeasure->getOrigin() + $ttl);
118 2
        }
119
120 6
        return $lock;
121
    }
122
123
    /**
124
     * @param Lock $lock
125
     * @param int  $ttl
126
     *
127
     * @return int The number of instances locked
128
     */
129 12
    private function lockInstances(Lock $lock, $ttl)
130
    {
131 12
        $instancesLocked = 0;
132
133 12
        foreach ($this->instances as $instance) {
134 12
            if ($this->lockInstance($instance, $lock, $ttl)) {
135 12
                ++$instancesLocked;
136 8
            }
137 8
        }
138
139 12
        return $instancesLocked;
140
    }
141
142
    /**
143
     * @param \Redis $instance Server instance to be locked
144
     * @param Lock   $lock     The lock instance
145
     * @param int    $ttl      Time to live in milliseconds
146
     *
147
     * @return bool
148
     */
149 12
    private function lockInstance(\Redis $instance, Lock $lock, $ttl)
150
    {
151 12
        $options = ['NX'];
152
153 12
        if ($ttl) {
154 9
            $options['PX'] = (int) $ttl;
155 6
        }
156
157 12
        return (bool) $instance->set($lock->getResource(), (string) $lock->getToken(), $options);
158
    }
159
160
    /**
161
     * @param Lock $lock
162
     */
163 6
    private function resetLock($lock)
164
    {
165 6
        foreach ($this->instances as $instance) {
166 6
            $this->unlockInstance($instance, $lock);
167 4
        }
168 6
    }
169
170
    /**
171
     * @param \Redis $instance
172
     * @param string $resource
173
     *
174
     * @return bool
175
     */
176 18
    private function isInstanceResourceLocked(\Redis $instance, $resource)
177
    {
178 18
        return (bool) $instance->exists($resource);
179
    }
180
181
    /**
182
     * @param \Redis $instance Server instance to be unlocked
183
     * @param Lock   $lock     The lock to unlock
184
     *
185
     * @return bool
186
     */
187 18
    private function unlockInstance(\Redis $instance, Lock $lock)
188
    {
189
        $script = '
190
            if redis.call("GET", KEYS[1]) == ARGV[1] then
191
                return redis.call("DEL", KEYS[1])
192
            else
193
                return 0
194
            end
195 18
        ';
196
197 18
        return (bool) $instance->evaluate(
198 12
            $script,
199 18
            [$lock->getResource(), (string) $lock->getToken()],
200 6
            1
201 12
        );
202
    }
203
204
    /**
205
     * @param \Redis[] $instances
206
     *
207
     * @throws \Exception
208
     */
209 39
    private function setInstances(array $instances)
210
    {
211 39
        if (count($instances) === 0) {
212 3
            throw new \InvalidArgumentException('You must provide at least one Redis instance.');
213
        }
214
215 39
        foreach ($instances as $instance) {
216 39
            if (!$instance->isConnected()) {
217 15
                throw new \InvalidArgumentException('The Redis must be connected.');
218
            }
219 26
        }
220
221 39
        $this->instances = $instances;
222 39
    }
223
224 39
    private function setQuorum()
225
    {
226 39
        $numberOfRedisInstances = count($this->instances);
227 39
        $this->quorum = (int) round(min($numberOfRedisInstances, ($numberOfRedisInstances / 2) + 1));
228 39
    }
229
230
    /**
231
     * @param int $instancesLocked
232
     *
233
     * @throws LockingException
234
     */
235 12
    private function checkQuorum($instancesLocked)
236
    {
237 12
        if ($instancesLocked < $this->quorum) {
238 3
            throw new LockingException();
239
        }
240 9
    }
241
242
    /**
243
     * @param int $retryDelay
244
     */
245 6
    private function waitBeforeRetrying($retryDelay)
246
    {
247 6
        usleep($retryDelay * 1000);
248 6
    }
249
250
    /**
251
     * @param int $elapsedTime
252
     * @param int $ttl
253
     *
254
     * @throws LockingException
255
     */
256 6
    private static function checkTtl($elapsedTime, $ttl)
257
    {
258 6
        $adjustedElapsedTime = ($elapsedTime + self::getDrift($ttl));
259
260 6
        if ($adjustedElapsedTime >= $ttl) {
261 3
            throw new LockingException();
262
        }
263 3
    }
264
265
    /**
266
     * Get the drift time based on ttl in ms.
267
     *
268
     * @param int $ttl
269
     *
270
     * @return float
271
     */
272 6
    private static function getDrift($ttl)
273
    {
274
        // Add 2 milliseconds to the drift to account for Redis expires
275
        // precision, which is 1 millisecond, plus 2 millisecond min drift
276
        // for small TTLs.
277
278 6
        $redisExpiresPrecision = 2;
279 6
        $minDrift = ($ttl) ? ceil($ttl * self::CLOCK_DRIFT_FACTOR) : 0;
280
281 6
        return $minDrift + $redisExpiresPrecision;
282
    }
283
}
284