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

RedisLockStrategy::acquire()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.6393

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 34
ccs 9
cts 14
cp 0.6429
rs 8.439
cc 6
eloc 16
nc 6
nop 1
crap 7.6393
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
        $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...
116 5
        $this->redis->connect($config['host'], $port);
117
118 5
        if (\array_key_exists('auth', $config)) {
119
            $this->redis->auth($config['auth']);
120
        }
121
122 5
        $this->redis->select((int) $config['database']);
123
124 5
        $this->subject = $subject;
125 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...
126 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...
127 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...
128
129 5
        $this->init();
130 5
    }
131
132
    /**
133
     * @inheritdoc
134
     */
135 9
    public static function getCapabilities()
136
    {
137 9
        return self::LOCK_CAPABILITY_EXCLUSIVE | self::LOCK_CAPABILITY_NOBLOCK;
138
    }
139
140
    /**
141
     * @inheritdoc
142
     */
143 9
    public static function getPriority()
144
    {
145 9
        return 100;
146
    }
147
148
    /**
149
     * @inheritdoc
150
     */
151 2
    public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE)
152
    {
153 2
        if ($this->isAcquired) {
154
            return true;
155
        }
156
157 2
        if ($mode & self::LOCK_CAPABILITY_EXCLUSIVE) {
158
159 2
            if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
160
161
                // this does not block
162
                $this->isAcquired = $this->lock();
163
164
                if (!$this->isAcquired) {
165
                    throw new LockAcquireWouldBlockException('could not acquire lock');
166
                }
167
            } else {
168
169
                // this blocks till the lock gets released
170 2
                $this->wait();
171
172 1
                $this->isAcquired = $this->lock();
173
174 1
                if (!$this->isAcquired) {
175 1
                    throw new LockAcquireException('could not acquire lock');
176
                }
177
            }
178
179
        } else {
180
            throw new LockAcquireException('insufficient capabilities');
181
        }
182
183 1
        return true;
184
    }
185
186
    /**
187
     * @inheritdoc
188
     */
189 1
    public function isAcquired()
190
    {
191 1
        return $this->isAcquired;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197 1
    public function destroy()
198
    {
199 1
        $this->redis->del($this->name);
200 1
        $this->redis->del($this->mutex);
201 1
    }
202
203
    /**
204
     * @inheritdoc
205
     */
206
    public function release()
207
    {
208
        if (!$this->isAcquired) {
209
            return true;
210
        }
211
212
        // discard return code
213
        // we want to release the lock even in error case
214
        // to get a more resilient behaviour
215
        $this->unlockAndSignal();
216
217
        $this->isAcquired = false;
218
219
        return !$this->isAcquired;
220
    }
221
222
    /**
223
     * Initialize the synchronization object, i.e. a simple list with some random element
224
     *
225
     * @return boolean TRUE on sucess, FALSE otherwise
226
     */
227 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...
228
    {
229 5
        $script = '
230
            if not redis.call("EXISTS", KEYS[1], KEYS[2]) then
231
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
232
            else
233
                return 0
234
            fi
235
        ';
236 5
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
237
    }
238
239
    /**
240
     * Try to get the lock
241
     * N.B. this a is non-blocking operation
242
     *
243
     * @return boolean TRUE on success, FALSE otherwise
244
     */
245 1
    private function lock()
246
    {
247 1
        $this->value = uniqid();
248
249
        // option NX: set value iff key is not present
250 1
        return $this->redis->set($this->name, $this->value, ['NX', 'PX' => $this->ttl]);
251
    }
252
253
    /**
254
     * Wait for the lock being released
255
     * N.B. this a is blocking operation
256
     *
257
     * @return string The value, FALSE otherwise
258
     */
259 2
    private function wait()
260
    {
261 2
        return $this->redis->blPop([$this->mutex], $this->blTo)[1];
262
    }
263
264
    /**
265
     * Try to unlock the mutex and if succeeds, signal the waiting locks
266
     *
267
     * @return boolean TRUE on success, FALSE otherwise
268
     */
269 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...
270
    {
271
        $script = '
272
            if redis.call("GET", KEYS[1]) == ARGV[1] and redis.call("DEL", KEYS[1]) then
273
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
274
            else
275
                return 0
276
            end
277
        ';
278
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
279
    }
280
281
}
282