Completed
Pull Request — master (#8)
by
unknown
01:33
created

RedisLockStrategy::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 4

Duplication

Lines 11
Ratio 100 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 11
loc 11
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 1
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 View Code Duplication
        if (\array_key_exists('redis_lock', $GLOBALS['TYPO3_CONF_VARS']['SYS'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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 View Code Duplication
            if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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 timeout 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);
180
181 3
                    $waited += time() - $start;
182
183 3
                    if ($waited > $this->blTo) {
184
                        throw new LockAcquireException('could not acquire lock');
185
                    }
186
187 3
                    $this->isAcquired = $this->lock();
188
189 4
                } while (!$this->isAcquired);
190
            }
191
192
        } else {
193
            throw new LockAcquireException('insufficient capabilities');
194
        }
195
196 4
        return true;
197
    }
198
199
    /**
200
     * @inheritdoc
201
     */
202 2
    public function isAcquired()
203
    {
204 2
        return $this->isAcquired;
205
    }
206
207
    /**
208
     * @inheritdoc
209
     */
210 1
    public function destroy()
211
    {
212 1
        $this->redis->del($this->name);
213 1
        $this->redis->del($this->mutex);
214 1
    }
215
216
    /**
217
     * @inheritdoc
218
     */
219 1
    public function release()
220
    {
221 1
        if (!$this->isAcquired) {
222 1
            return true;
223
        }
224
225
        // discard return code
226
        // we want to release the lock even in error case
227
        // to get a more resilient behaviour
228 1
        $this->unlockAndSignal();
229
230 1
        $this->isAcquired = false;
231
232 1
        return !$this->isAcquired;
233
    }
234
235
    /**
236
     * Initialize the synchronization object, i.e. a simple list with some random element
237
     *
238
     * @return boolean TRUE on sucess, FALSE otherwise
239
     */
240 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...
241
    {
242 5
        $script = '
243
            if redis.call("EXISTS", KEYS[1], KEYS[2]) == 0 then
244
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
245
            else
246
                return 0
247
            end
248
        ';
249 5
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
250
    }
251
252
    /**
253
     * Try to get the lock
254
     * N.B. this a is non-blocking operation
255
     *
256
     * @return boolean TRUE on success, FALSE otherwise
257
     */
258 4 View Code Duplication
    private function lock()
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...
259
    {
260 4
        $this->value = uniqid();
261
262
        // option NX: set value iff key is not present
263 4
        return (bool) $this->redis->set($this->name, $this->value, ['NX', 'PX' => $this->ttl]);
264
    }
265
266
    /**
267
     * Wait for the lock being released
268
     * N.B. this a is blocking operation
269
     *
270
     * @param int $blTo The blocking timeout
271
     * @return string The popped value, FALSE otherwise
272
     */
273 3
    private function wait($blTo)
274
    {
275 3
        $result = $this->redis->blPop([$this->mutex], $blTo);
276
277 3
        return is_array($result) ? $result[1] : false;
278
    }
279
280
    /**
281
     * Try to unlock the mutex and if succeeds, signal the waiting locks
282
     *
283
     * @return boolean TRUE on success, FALSE otherwise
284
     */
285 1 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...
286
    {
287 1
        $script = '
288
            if (redis.call("GET", KEYS[1]) == ARGV[1]) and (redis.call("DEL", KEYS[1]) == 1) then
289
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
290
            else
291
                return 0
292
            end
293
        ';
294 1
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
295
    }
296
297
}
298