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 ( c6f8b5...a6df00 )
by Rémi
14:52
created

RedLock::isLocked()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 5
nc 3
nop 1
crap 3
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
     * RedLock constructor.
31
     *
32
     * @param \Redis[]       $instances      Array of pre-connected \Redis objects
33
     * @param TokenGenerator $tokenGenerator The token generator
34
     * @param Stopwatch      $stopwatch      A way to measure time passed
35
     */
36 39
    public function __construct(
37
        array $instances,
38
        TokenGenerator $tokenGenerator,
39
        Stopwatch $stopwatch
40
    ) {
41 39
        $this->setInstances($instances);
42 39
        $this->setQuorum();
43
44 39
        $this->tokenGenerator = $tokenGenerator;
45 39
        $this->stopwatch = $stopwatch;
46 39
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 12
    public function lock($resource, $ttl = null, $retryDelay = 0, $retryCount = 0)
52
    {
53 12
        $lock = new Lock((string) $resource, $this->tokenGenerator->generateToken());
54
55 12
        $tried = 0;
56 12
        while (true) {
57
            try {
58 12
                return $this->monitoredLockingOfAllInstances($lock, $ttl);
59 6
            } catch (LockingException $e) {
60 6
                $this->resetLock($lock);
61
            }
62
63 6
            if ($tried++ === $retryCount) {
64 6
                break;
65
            }
66
67 6
            $this->waitBeforeRetrying($retryDelay);
68 4
        }
69
70 6
        throw new LockingException('Failed locking the resource.');
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 9
    public function isLocked($resource)
77
    {
78 9
        foreach ($this->instances as $instance) {
79 9
            if ($this->isInstanceResourceLocked($instance, (string) $resource)) {
80 7
                return true;
81
            }
82 4
        }
83
84 3
        return false;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 12
    public function unlock(Lock $lock)
91
    {
92 12
        foreach ($this->instances as $instance) {
93 12
            if (!$this->unlockInstance($instance, $lock)) {
94 9
                if ($this->isInstanceResourceLocked($instance, $lock->getResource())) {
95
                    // Only throw an exception if the lock is still present
96 8
                    throw new UnlockingException('Failed releasing the lock.');
97
                }
98 2
            }
99 6
        }
100 6
    }
101
102
    /**
103
     * Try locking all Redis instances.
104
     *
105
     * Measure the time to do it and reject if not enough Redis instance have successfully
106
     * locked the resource or if time to lock all instances have exceeded the ttl.
107
     *
108
     * @param Lock $lock The lock instance
109
     * @param int  $ttl  Time to live in milliseconds
110
     *
111
     * @throws LockingException
112
     *
113
     * @return Lock
114
     */
115 12
    private function monitoredLockingOfAllInstances(Lock $lock, $ttl)
116
    {
117 12
        $timeMeasure = $this->stopwatch->start($lock->getToken());
118 12
        $instancesLocked = $this->lockInstances($lock, $ttl);
119 12
        $timeMeasure->stop();
120
121 12
        $this->checkQuorum($instancesLocked);
122
123 9
        if ($ttl) {
124 6
            self::checkTtl($timeMeasure->getDuration(), $ttl);
125 3
            $lock->setValidityEndTime($timeMeasure->getOrigin() + $ttl);
126 2
        }
127
128 6
        return $lock;
129
    }
130
131
    /**
132
     * Lock resource in Redis instances and count how many instance did it with success.
133
     *
134
     * @param Lock $lock The lock instance
135
     * @param int  $ttl  Time to live in milliseconds
136
     *
137
     * @return int The number of instances locked
138
     */
139 12
    private function lockInstances(Lock $lock, $ttl)
140
    {
141 12
        $instancesLocked = 0;
142
143 12
        foreach ($this->instances as $instance) {
144 12
            if ($this->lockInstance($instance, $lock, $ttl)) {
145 12
                ++$instancesLocked;
146 8
            }
147 8
        }
148
149 12
        return $instancesLocked;
150
    }
151
152
    /**
153
     * Lock the resource on a given Redis instance.
154
     *
155
     * @param \Redis $instance Server instance to be locked
156
     * @param Lock   $lock     The lock instance
157
     * @param int    $ttl      Time to live in milliseconds
158
     *
159
     * @return bool
160
     */
161 12
    private function lockInstance(\Redis $instance, Lock $lock, $ttl)
162
    {
163 12
        $options = ['NX'];
164
165 12
        if ($ttl) {
166 9
            $options['PX'] = (int) $ttl;
167 6
        }
168
169 12
        return (bool) $instance->set($lock->getResource(), (string) $lock->getToken(), $options);
170
    }
171
172
    /**
173
     * Unlock the resource on all Redis instances.
174
     *
175
     * @param Lock $lock The lock to release
176
     */
177 6
    private function resetLock($lock)
178
    {
179 6
        foreach ($this->instances as $instance) {
180 6
            $this->unlockInstance($instance, $lock);
181 4
        }
182 6
    }
183
184
    /**
185
     * Checks if the resource exists on a given Redis instance.
186
     *
187
     * @param \Redis $instance The Redis instance
188
     * @param string $resource The name of the resource
189
     *
190
     * @return bool
191
     */
192 18
    private function isInstanceResourceLocked(\Redis $instance, $resource)
193
    {
194 18
        return (bool) $instance->exists($resource);
195
    }
196
197
    /**
198
     * Unlock the resource on a given Redis instance.
199
     *
200
     * If the lock is successfully released, it will return true.
201
     * If the token provided by the lock doesn't correspond to the token stored, it will return false.
202
     * If the lock is not found, it will return false.
203
     *
204
     * @param \Redis $instance Server instance to be unlocked
205
     * @param Lock   $lock     The lock to unlock
206
     *
207
     * @return bool
208
     */
209 18
    private function unlockInstance(\Redis $instance, Lock $lock)
210
    {
211
        $script = '
212
            if redis.call("GET", KEYS[1]) == ARGV[1] then
213
                return redis.call("DEL", KEYS[1])
214
            else
215
                return 0
216
            end
217 18
        ';
218
219 18
        return (bool) $instance->evaluate(
220 12
            $script,
221 18
            [$lock->getResource(), (string) $lock->getToken()],
222 6
            1
223 12
        );
224
    }
225
226
    /**
227
     * Init all Redis instances passed to the constructor.
228
     *
229
     * If no Redis instance is given, it will return a InvalidArgumentException.
230
     * If one or more Redis instance is not connected, it will return a InvalidArgumentException.
231
     *
232
     * @param \Redis[] $instances The connected Redis instances
233
     *
234
     * @throws \InvalidArgumentException
235
     */
236 39
    private function setInstances(array $instances)
237
    {
238 39
        if (count($instances) === 0) {
239 3
            throw new \InvalidArgumentException('You must provide at least one Redis instance.');
240
        }
241
242 39
        foreach ($instances as $instance) {
243 39
            if (!$instance->isConnected()) {
244 15
                throw new \InvalidArgumentException('The Redis must be connected.');
245
            }
246 26
        }
247
248 39
        $this->instances = $instances;
249 39
    }
250
251
    /**
252
     * Set the quorum based on the number of instances passed to the constructor.
253
     */
254 39
    private function setQuorum()
255
    {
256 39
        $numberOfRedisInstances = count($this->instances);
257 39
        $this->quorum = (int) round(min($numberOfRedisInstances, ($numberOfRedisInstances / 2) + 1));
258 39
    }
259
260
    /**
261
     * Check if the number of instances that have been locked reach the quorum.
262
     *
263
     * @param int $instancesLocked The number of instances that have been locked
264
     *
265
     * @throws LockingException
266
     */
267 12
    private function checkQuorum($instancesLocked)
268
    {
269 12
        if ($instancesLocked < $this->quorum) {
270 3
            throw new LockingException('Quorum has not been met.');
271
        }
272 9
    }
273
274
    /**
275
     * Make the script wait before retrying to lock.
276
     *
277
     * @param int $retryDelay The retry delay in milliseconds
278
     */
279 6
    private function waitBeforeRetrying($retryDelay)
280
    {
281 6
        usleep($retryDelay * 1000);
282 6
    }
283
284
    /**
285
     * Checks if the elapsed time is inferior to the ttl.
286
     *
287
     * To the elapsed time is added a drift time to have a margin of error.
288
     * If this adjusted time is greater than the ttl, it will throw a LockingException.
289
     *
290
     * @param int $elapsedTime The time elapsed in milliseconds
291
     * @param int $ttl         The time to live in milliseconds
292
     *
293
     * @throws LockingException
294
     */
295 6
    private static function checkTtl($elapsedTime, $ttl)
296
    {
297 6
        $adjustedElapsedTime = ($elapsedTime + self::getDrift($ttl));
298
299 6
        if ($adjustedElapsedTime >= $ttl) {
300 3
            throw new LockingException('Time to lock the resource has exceeded the ttl.');
301
        }
302 3
    }
303
304
    /**
305
     * Get the drift time based on ttl in ms.
306
     *
307
     * @param int $ttl The time to live in milliseconds
308
     *
309
     * @return float
310
     */
311 6
    private static function getDrift($ttl)
312
    {
313
        // Add 2 milliseconds to the drift to account for Redis expires
314
        // precision, which is 1 millisecond, plus 2 millisecond min drift
315
        // for small TTLs.
316
317 6
        $redisExpiresPrecision = 2;
318 6
        $minDrift = ($ttl) ? ceil($ttl * self::CLOCK_DRIFT_FACTOR) : 0;
319
320 6
        return $minDrift + $redisExpiresPrecision;
321
    }
322
}
323