Completed
Pull Request — master (#8)
by
unknown
06:25 queued 05:09
created

RedisLockStrategy   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 252
Duplicated Lines 8.73 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 80.56%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 27
c 4
b 2
f 0
lcom 1
cbo 3
dl 22
loc 252
ccs 58
cts 72
cp 0.8056
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 49 9
A getCapabilities() 0 4 1
A getPriority() 0 4 1
A isAcquired() 0 4 1
A destroy() 0 5 1
A release() 0 15 2
A init() 11 11 1
A lock() 0 7 1
A wait() 0 6 2
A unlockAndSignal() 11 11 1
C acquire() 0 39 7

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
namespace Tourstream\RedisLockStrategy;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2017 Alexander Miehe ([email protected])
9
 *  All rights reserved
10
 *
11
 *  You may not remove or change the name of the author above. See:
12
 *  http://www.gnu.org/licenses/gpl-faq.html#IWantCredit
13
 *
14
 *  This script is part of the Typo3 project. The Typo3 project is
15
 *  free software; you can redistribute it and/or modify
16
 *  it under the terms of the GNU General Public License as published by
17
 *  the Free Software Foundation; either version 3 of the License, or
18
 *  (at your option) any later version.
19
 *
20
 *  The GNU General Public License can be found at
21
 *  http://www.gnu.org/copyleft/gpl.html.
22
 *  A copy is found in the LICENSE and distributed with these scripts.
23
 *
24
 *
25
 *  This script is distributed in the hope that it will be useful,
26
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28
 *  GNU General Public License for more details.
29
 *
30
 *  This copyright notice MUST APPEAR in all copies of the script!
31
 ***************************************************************/
32
33
use TYPO3\CMS\Core\Locking\Exception\LockAcquireException;
34
use TYPO3\CMS\Core\Locking\Exception\LockCreateException;
35
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
36
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
37
38
/**
39
 * @author Alexander Miehe <[email protected]>
40
 */
41
class RedisLockStrategy implements LockingStrategyInterface
42
{
43
    /**
44
     * @var \Redis A key-value data store
45
     */
46
    private $redis;
47
48
    /**
49
     * @var string The locking subject, i.e. a string to discriminate the lock
50
     */
51
    private $subject;
52
53
    /**
54
     * @var string The key used for the lock itself
55
     */
56
    private $name;
57
58
    /**
59
     * @var string The key used for the mutex, i.e. a list
60
     */
61
    private $mutex;
62
63
    /**
64
     * @var string The value used for the lock
65
     */
66
    private $value;
67
68
    /**
69
     * @var boolean TRUE if lock is acquired
70
     */
71
    private $isAcquired = false;
72
73
    /**
74
     * @var int Seconds the lock remains persistent
75
     */
76
    private $ttl = 3600;
77
78
    /**
79
     * @var int Seconds to wait for a lock
80
     */
81
    private $blTo = 60;
82
83
    /**
84
     * @inheritdoc
85
     */
86 9
    public function __construct($subject)
87
    {
88 9
        $config = null;
89
90 9
        if (\array_key_exists('redis_lock', $GLOBALS['TYPO3_CONF_VARS']['SYS'])) {
91 8
            $config = $GLOBALS['TYPO3_CONF_VARS']['SYS']['redis_lock'];
92
        }
93
94 9
        if (!\is_array($config)) {
95 2
            throw new LockCreateException('no configuration for redis lock strategy found');
96
        }
97
98 7
        if (!\array_key_exists('host', $config)) {
99 1
            throw new LockCreateException('no host for redis lock strategy found');
100
        }
101 6
        $port = 6379;
102
103 6
        if (\array_key_exists('port', $config)) {
104 5
            $port = (int) $config['port'];
105
        }
106
107 6
        if (!\array_key_exists('database', $config)) {
108 1
            throw new LockCreateException('no database for redis lock strategy found');
109
        }
110
111 5
        if (\array_key_exists('ttl', $config)) {
112
            $this->ttl = (int) $config['ttl'];
113
        }
114
115 5
        if (\array_key_exists('blTo', $config)) {
116
            $this->blTo = (int) $config['blTo'];
117
        }
118
119 5
        $this->redis   = new \Redis();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 3 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
120 5
        $this->redis->connect($config['host'], $port);
121
122 5
        if (\array_key_exists('auth', $config)) {
123
            $this->redis->auth($config['auth']);
124
        }
125
126 5
        $this->redis->select((int) $config['database']);
127
128 5
        $this->subject = $subject;
129 5
        $this->name = sprintf('lock:name:%s', $subject);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
130 5
        $this->mutex = sprintf('lock:mutex:%s', $subject);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
131 5
        $this->value = uniqid();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
132
133 5
        $this->init();
134 5
    }
135
136
    /**
137
     * @inheritdoc
138
     */
139 9
    public static function getCapabilities()
140
    {
141 9
        return self::LOCK_CAPABILITY_EXCLUSIVE | self::LOCK_CAPABILITY_NOBLOCK;
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147 9
    public static function getPriority()
148
    {
149 9
        return 100;
150
    }
151
152
    /**
153
     * @inheritdoc
154
     */
155 4
    public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE)
156
    {
157 4
        if ($this->isAcquired) {
158
            return true;
159
        }
160
161 4
        if ($mode & self::LOCK_CAPABILITY_EXCLUSIVE) {
162
163 4
            if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
164
165
                // this does not block
166 1
                $this->isAcquired = $this->lock();
167
168 1
                if (!$this->isAcquired) {
169 1
                    throw new LockAcquireWouldBlockException('could not acquire lock');
170
                }
171
            } else {
172
173
                // try to acquire the lock till $blTo is reached
174 3
                $waited = 0;
175
                do {
176 3
                    $start = time();
177
178
                    // this blocks till the lock gets released
179 3
                    $this->wait($this->blTo - $waited);
0 ignored issues
show
Unused Code introduced by
The call to RedisLockStrategy::wait() has too many arguments starting with $this->blTo - $waited.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
180
181 3
                    $waited += time() - $start;
182
183 3
                    $this->isAcquired = $this->lock();
184
185 4
                } while (!$this->isAcquired && $waited < $this->blTo);
186
            }
187
188
        } else {
189
            throw new LockAcquireException('insufficient capabilities');
190
        }
191
192 4
        return true;
193
    }
194
195
    /**
196
     * @inheritdoc
197
     */
198 1
    public function isAcquired()
199
    {
200 1
        return $this->isAcquired;
201
    }
202
203
    /**
204
     * @inheritdoc
205
     */
206 1
    public function destroy()
207
    {
208 1
        $this->redis->del($this->name);
209 1
        $this->redis->del($this->mutex);
210 1
    }
211
212
    /**
213
     * @inheritdoc
214
     */
215
    public function release()
216
    {
217
        if (!$this->isAcquired) {
218
            return true;
219
        }
220
221
        // discard return code
222
        // we want to release the lock even in error case
223
        // to get a more resilient behaviour
224
        $this->unlockAndSignal();
225
226
        $this->isAcquired = false;
227
228
        return !$this->isAcquired;
229
    }
230
231
    /**
232
     * Initialize the synchronization object, i.e. a simple list with some random element
233
     *
234
     * @return boolean TRUE on sucess, FALSE otherwise
235
     */
236 5 View Code Duplication
    private function init()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
237
    {
238 5
        $script = '
239
            if redis.call("EXISTS", KEYS[1], KEYS[2]) == 0 then
240
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
241
            else
242
                return 0
243
            end
244
        ';
245 5
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
246
    }
247
248
    /**
249
     * Try to get the lock
250
     * N.B. this a is non-blocking operation
251
     *
252
     * @return boolean TRUE on success, FALSE otherwise
253
     */
254 4
    private function lock()
255
    {
256 4
        $this->value = uniqid();
257
258
        // option NX: set value iff key is not present
259 4
        return (bool) $this->redis->set($this->name, $this->value, ['NX', 'PX' => $this->ttl]);
260
    }
261
262
    /**
263
     * Wait for the lock being released
264
     * N.B. this a is blocking operation
265
     *
266
     * @return string The popped value, FALSE otherwise
267
     */
268 3
    private function wait()
269
    {
270 3
        $result = $this->redis->blPop([$this->mutex], $this->blTo);
271
272 3
        return $result ? $result[1] : false;
273
    }
274
275
    /**
276
     * Try to unlock the mutex and if succeeds, signal the waiting locks
277
     *
278
     * @return boolean TRUE on success, FALSE otherwise
279
     */
280 View Code Duplication
    private function unlockAndSignal()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281
    {
282
        $script = '
283
            if (redis.call("GET", KEYS[1]) == ARGV[1]) and (redis.call("DEL", KEYS[1]) == 1) then
284
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
285
            else
286
                return 0
287
            end
288
        ';
289
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
290
    }
291
292
}
293