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 ( 0289a3...9ef98c )
by Rémi
03:10
created

RedLock::lockAllInstances()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 4
Bugs 2 Features 1
Metric Value
c 4
b 2
f 1
dl 0
loc 20
ccs 12
cts 12
cp 1
rs 9.2
cc 4
eloc 10
nc 5
nop 2
crap 4
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 39
    public function __construct(
32
        array $instances,
33
        TokenGenerator $tokenGenerator,
34
        Stopwatch $stopwatch
35
    ) {
36 39
        self::setInstances($instances);
37
38 39
        $this->instances = $instances;
39 39
        $this->tokenGenerator = $tokenGenerator;
40 39
        $this->stopwatch = $stopwatch;
41 39
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 12
    public function lock($resource, $ttl = null, $retryDelay = 0, $retryCount = 0)
47
    {
48 12
        $lock = new Lock($resource, $this->tokenGenerator->generateToken());
49
50 12
        $tried = 0;
51 12
        while (true) {
52
            try {
53 12
                $this->lockAllInstances($lock, $ttl);
54
55 6
                return $lock;
56 6
            } catch (LockingException $e) {
57 6
                $this->resetLock($lock);
58
            }
59
60 6
            if ($tried++ == $retryCount) {
61 6
                break;
62
            }
63
64 6
            $this->waitBeforeRetrying($retryDelay);
65 4
        }
66
67 6
        throw new LockingException();
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73 9
    public function isResourceLocked($resource)
74
    {
75 9
        foreach ($this->instances as $instance) {
76 9
            if ($this->isInstanceResourceLocked($instance, $resource)) {
77 7
                return true;
78
            }
79 4
        }
80
81 3
        return false;
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 12
    public function unlock(Lock $lock)
88
    {
89 12
        foreach ($this->instances as $instance) {
90 12
            if (!$this->unlockInstance($instance, $lock)) {
91 9
                if ($this->isInstanceResourceLocked($instance, $lock->getResource())) {
92 8
                    throw new UnlockingException(); // Only throw an exception if the lock is still present
93
                }
94 2
            }
95 6
        }
96 6
    }
97
98
    /**
99
     * @param Lock $lock
100
     * @param int  $ttl
101
     *
102
     * @throws LockingException
103
     */
104 12
    private function lockAllInstances($lock, $ttl)
105
    {
106 12
        $timeMeasure = $this->stopwatch->start($lock->getToken());
107
108 12
        foreach ($this->instances as $instance) {
109 12
            if (!$this->lockInstance($instance, $lock, $ttl)) {
110 6
                throw new LockingException();
111
            }
112 8
        }
113
114 9
        $timeMeasure->stop();
115
116 9
        if (!$ttl) {
117 3
            return;
118
        }
119
120 6
        self::checkTtl($timeMeasure->getDuration(), $ttl);
121
122 3
        $lock->setValidityTimeEnd($timeMeasure->getOrigin() + $ttl);
123 3
    }
124
125
    /**
126
     * @param Lock $lock
127
     */
128 6
    private function resetLock($lock)
129
    {
130 6
        foreach ($this->instances as $instance) {
131 6
            $this->unlockInstance($instance, $lock);
132 4
        }
133 6
    }
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 12
    private function lockInstance(\Redis $instance, Lock $lock, $ttl)
143
    {
144 12
        $options = ['NX'];
145
146 12
        if ($ttl) {
147 9
            $options['PX'] = (int) $ttl;
148 6
        }
149
150 12
        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 18
    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 18
        ';
179
180 18
        return (bool) $instance->evaluate(
181 12
            $script,
182 18
            [$lock->getResource(), (string) $lock->getToken()],
183 6
            1
184 12
        );
185
    }
186
187
    /**
188
     * @param \Redis[] $instances
189
     *
190
     * @throws \Exception
191
     */
192 39
    private function setInstances(array $instances)
193
    {
194 39
        if (count($instances) === 0) {
195 3
            throw new \InvalidArgumentException('You must provide at least one Redis instance.');
196
        }
197
198 39
        foreach ($instances as $instance) {
199 39
            if (!$instance->isConnected()) {
200 15
                throw new \InvalidArgumentException('The Redis must be connected.');
201
            }
202 26
        }
203
204 39
        $this->instances = $instances;
205 39
    }
206
207
    /**
208
     * @param $retryDelay
209
     */
210 6
    private function waitBeforeRetrying($retryDelay)
211
    {
212 6
        usleep($retryDelay * 1000);
213 6
    }
214
215
    /**
216
     * @param int $elapsedTime
217
     * @param int $ttl
218
     *
219
     * @throws LockingException
220
     */
221 6
    private static function checkTtl($elapsedTime, $ttl)
222
    {
223 6
        $adjustedElapsedTime = ($elapsedTime + self::getDrift($ttl));
224
225 6
        if ($adjustedElapsedTime >= $ttl) {
226 3
            throw new LockingException();
227
        }
228 3
    }
229
230
    /**
231
     * Get the drift time based on ttl in ms.
232
     *
233
     * @param int $ttl
234
     *
235
     * @return float
236
     */
237 6
    private static function getDrift($ttl)
238
    {
239
        // Add 2 milliseconds to the drift to account for Redis expires
240
        // precision, which is 1 millisecond, plus 2 millisecond min drift
241
        // for small TTLs.
242
243 6
        $redisExpiresPrecision = 2;
244 6
        $minDrift = ($ttl) ? ceil($ttl * self::CLOCK_DRIFT_FACTOR) : 0;
245
246 6
        return $minDrift + $redisExpiresPrecision;
247
    }
248
}
249