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 ( c1bf2f...cd7da5 )
by Rémi
02:41
created

RedLock::isResourceLocked()   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 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
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
    /**
27
     * @param \Redis[]       $instances      Array of pre-connected \Redis objects
28
     * @param TokenGenerator $tokenGenerator The token generator
29
     * @param Stopwatch      $stopwatch      A way to measure time passed
30
     */
31 33
    public function __construct(
32
        array $instances,
33
        TokenGenerator $tokenGenerator,
34
        Stopwatch $stopwatch
35
    ) {
36 33
        self::setInstances($instances);
37
38 33
        $this->instances = $instances;
39 33
        $this->tokenGenerator = $tokenGenerator;
40 33
        $this->stopwatch = $stopwatch;
41 33
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 6
    public function lock($resource, $ttl = null, $retryDelay = 0, $retryCount = 0)
47
    {
48 6
        $lock = new Lock($resource, $this->tokenGenerator->generateToken());
49
50 6
        $tried = 0;
51 6
        while (true) {
52
            try {
53 6
                $this->lockAllInstances($lock, $ttl);
54
55 3
                return $lock;
56 3
            } catch (LockingException $e) {
57 3
                $this->resetLock($lock);
58
            }
59
60 3
            if ($tried++ == $retryCount) {
61 3
                throw new LockingException();
62
            }
63
64 3
            $this->waitBeforeRetrying($retryDelay);
65 2
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 9
    public function isResourceLocked($resource)
72
    {
73 9
        foreach ($this->instances as $instance) {
74 9
            if ($this->isInstanceResourceLocked($instance, $resource)) {
75 7
                return true;
76
            }
77 4
        }
78
79 3
        return false;
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 12
    public function unlock(Lock $lock)
86
    {
87 12
        foreach ($this->instances as $instance) {
88 12
            if (!$this->unlockInstance($instance, $lock)) {
89 9
                if ($this->isInstanceResourceLocked($instance, $lock->getResource())) {
90 8
                    throw new UnlockingException(); // Only throw an exception if the lock is still present
91
                }
92 2
            }
93 6
        }
94 6
    }
95
96
    /**
97
     * @param Lock $lock
98
     * @param int  $ttl
99
     *
100
     * @throws LockingException
101
     */
102 6
    private function lockAllInstances($lock, $ttl)
103
    {
104 6
        $timeMeasure = $this->stopwatch->start($lock->getToken());
105
106 6
        foreach ($this->instances as $instance) {
107 6
            if (!$this->lockInstance($instance, $lock, $ttl)) {
108 2
                throw new LockingException();
109
            }
110 4
        }
111
112 6
        $timeMeasure->stop();
113
114 6
        if (!$ttl) {
115
            return;
116
        }
117
118 6
        if (($ttl - ($timeMeasure->getDuration() + self::getDrift($ttl))) <= 0) {
119 3
            throw new LockingException();
120
        }
121
122 3
        $lock->setValidityTimeEnd($timeMeasure->getOrigin() + $ttl);
123 3
    }
124
125
    /**
126
     * @param Lock $lock
127
     */
128 3
    private function resetLock($lock)
129
    {
130 3
        foreach ($this->instances as $instance) {
131 3
            $this->unlockInstance($instance, $lock);
132 2
        }
133 3
    }
134
135
    /**
136
     * @param \Redis $instance Server instance to be locked
137
     * @param Lock   $lock     The lock instance
138
     * @param int    $ttl      Time to live in milliseconds
139
     *
140
     * @return bool
141
     */
142 6
    private function lockInstance(\Redis $instance, Lock $lock, $ttl)
143
    {
144 6
        $options = ['NX'];
145
146 6
        if ($ttl) {
147 6
            $options['PX'] = (int) $ttl;
148 4
        }
149
150 6
        return (bool) $instance->set($lock->getResource(), (string) $lock->getToken(), $options);
151
    }
152
153
    /**
154
     * @param \Redis $instance
155
     * @param string $resource
156
     *
157
     * @return bool
158
     */
159 18
    private function isInstanceResourceLocked(\Redis $instance, $resource)
160
    {
161 18
        return (bool) $instance->exists($resource);
162
    }
163
164
    /**
165
     * @param \Redis $instance Server instance to be unlocked
166
     * @param Lock   $lock     The lock to unlock
167
     *
168
     * @return bool
169
     */
170 15
    private function unlockInstance(\Redis $instance, Lock $lock)
171
    {
172
        $script = '
173
            if redis.call("GET", KEYS[1]) == ARGV[1] then
174
                return redis.call("DEL", KEYS[1])
175
            else
176
                return 0
177
            end
178 15
        ';
179
180 15
        return (bool) $instance->evaluate(
181 10
            $script,
182 15
            [$lock->getResource(), (string) $lock->getToken()],
183 5
            1
184 10
        );
185
    }
186
187
    /**
188
     * @param \Redis[] $instances
189
     *
190
     * @throws \Exception
191
     */
192 33
    private function setInstances(array $instances)
193
    {
194 33
        if (count($instances) === 0) {
195 3
            throw new \InvalidArgumentException('You must provide at least one Redis instance.');
196
        }
197
198 33
        foreach ($instances as $instance) {
199 33
            if (!$instance->isConnected()) {
200 13
                throw new \InvalidArgumentException('The Redis must be connected.');
201
            }
202 22
        }
203
204 33
        $this->instances = $instances;
205 33
    }
206
207
    /**
208
     * @param $retryDelay
209
     */
210 3
    private function waitBeforeRetrying($retryDelay)
211
    {
212 3
        usleep($retryDelay * 1000);
213 3
    }
214
215
    /**
216
     * Get the drift time based on ttl in ms.
217
     *
218
     * @param int $ttl
219
     *
220
     * @return float
221
     */
222 6
    private static function getDrift($ttl)
223
    {
224
        // Add 2 milliseconds to the drift to account for Redis expires
225
        // precision, which is 1 millisecond, plus 2 millisecond min drift
226
        // for small TTLs.
227
228 6
        $redisExpiresPrecision = 2;
229 6
        $minDrift = ($ttl) ? ceil($ttl * self::CLOCK_DRIFT_FACTOR) : 0;
230
231 6
        return $minDrift + $redisExpiresPrecision;
232
    }
233
}
234