Completed
Push — master ( 74f59f...025add )
by Alexander
12s
created

RedisLockStrategy   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 82.26%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 25
c 3
b 1
f 0
lcom 1
cbo 3
dl 0
loc 214
ccs 51
cts 62
cp 0.8226
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A isAcquired() 0 4 1
A destroy() 0 4 1
C __construct() 0 40 8
A getCapabilities() 0 4 1
A getPriority() 0 4 1
C acquire() 0 32 7
A release() 0 14 2
A lock() 0 7 1
A wait() 0 7 2
A unlockAndSignal() 0 11 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
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 by this locker
70
     */
71
    private $isAcquired = false;
72
73
    /**
74
     * @var int Seconds the lock remains persistent
75
     */
76
    private $ttl = 60;
77
78
    /**
79
     * @inheritdoc
80
     */
81 9
    public function __construct($subject)
82
    {
83 9
        $config = null;
84 9
        if (\array_key_exists('redis_lock', $GLOBALS['TYPO3_CONF_VARS']['SYS'])) {
85 8
            $config = $GLOBALS['TYPO3_CONF_VARS']['SYS']['redis_lock'];
86
        }
87
88 9
        if (!\is_array($config)) {
89 2
            throw new LockCreateException('no configuration for redis lock strategy found');
90
        }
91
92 7
        if (!\array_key_exists('host', $config)) {
93 1
            throw new LockCreateException('no host for redis lock strategy found');
94
        }
95
96 6
        $port = 6379;
97 6
        if (\array_key_exists('port', $config)) {
98
            $port = (int) $config['port'];
99
        }
100
101 6
        if (!\array_key_exists('database', $config)) {
102 1
            throw new LockCreateException('no database for redis lock strategy found');
103
        }
104
105 5
        if (\array_key_exists('ttl', $config)) {
106
            $this->ttl = (int) $config['ttl'];
107
        }
108
109 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...
110 5
        $this->redis->connect($config['host'], $port);
111 5
        if (\array_key_exists('auth', $config)) {
112
            $this->redis->auth($config['auth']);
113
        }
114 5
        $this->redis->select((int) $config['database']);
115
116 5
        $this->subject = $subject;
117 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...
118 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...
119 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...
120 5
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125 9
    public static function getCapabilities()
126
    {
127 9
        return self::LOCK_CAPABILITY_EXCLUSIVE | self::LOCK_CAPABILITY_NOBLOCK;
128
    }
129
130
    /**
131
     * @inheritdoc
132
     */
133 9
    public static function getPriority()
134
    {
135 9
        return 100;
136
    }
137
138
    /**
139
     * @inheritdoc
140
     */
141 4
    public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE)
142
    {
143 4
        if ($this->isAcquired) {
144
            return true;
145
        }
146
147 4
        if ($mode & self::LOCK_CAPABILITY_EXCLUSIVE) {
148 4
            if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
149
150
                // try to acquire the lock - non-blocking
151 1
                if (!$this->isAcquired = $this->lock()) {
152 1
                    throw new LockAcquireWouldBlockException('could not acquire lock');
153
                }
154
            } else {
155
156
                // try to acquire the lock - blocking
157
                // N.B. we do this in a loop because between
158
                // wait() and lock() another process may acquire the lock
159 4
                while (!$this->isAcquired = $this->lock()) {
160
161
                    // this blocks till the lock gets released or timeout is reached
162
                    if (!$this->wait()) {
163
                        throw new LockAcquireException('could not acquire lock');
164
                    }
165
                }
166
            }
167
        } else {
168
            throw new LockAcquireException('insufficient capabilities');
169
        }
170
171 4
        return $this->isAcquired;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 2
    public function isAcquired()
178
    {
179 2
        return $this->isAcquired;
180
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185 1
    public function destroy()
186
    {
187 1
        $this->release();
188 1
    }
189
190
    /**
191
     * @inheritdoc
192
     */
193 2
    public function release()
194
    {
195 2
        if (!$this->isAcquired) {
196 2
            return true;
197
        }
198
199
        // discard return code
200
        // N.B. we want to release the lock even in error case
201
        // to get a more resilient behaviour
202 1
        $this->unlockAndSignal();
203 1
        $this->isAcquired = false;
204
205 1
        return !$this->isAcquired;
206
    }
207
208
    /**
209
     * Try to lock
210
     * N.B. this a is non-blocking operation
211
     *
212
     * @return boolean TRUE on success, FALSE otherwise
213
     */
214 4
    private function lock()
215
    {
216 4
        $this->value = uniqid();
217
218
        // option NX: set value iff key is not present
219 4
        return (bool) $this->redis->set($this->name, $this->value, ['NX', 'EX' => $this->ttl]);
220
    }
221
222
    /**
223
     * Wait on the mutex for the lock being released
224
     * N.B. this a is blocking operation
225
     *
226
     * @return string The popped value, FALSE on timeout
227
     */
228
    private function wait()
229
    {
230
        $blTo = max(1, $this->redis->ttl($this->name));
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...
231
        $result = $this->redis->blPop([$this->mutex], $blTo);
232
233
        return is_array($result) ? $result[1] : false;
234
    }
235
236
    /**
237
     * Try to unlock and if succeeds, signal the mutex
238
     * N.B. by using EVAL we enforce transactional behaviour
239
     *
240
     * @return boolean TRUE on success, FALSE otherwise
241
     */
242 1
    private function unlockAndSignal()
243
    {
244 1
        $script = '
245
            if (redis.call("GET", KEYS[1]) == ARGV[1]) and (redis.call("DEL", KEYS[1]) == 1) then
246
                return redis.call("RPUSH", KEYS[2], ARGV[1]) and redis.call("EXPIRE", KEYS[2], ARGV[2])
247
            else
248
                return 0
249
            end
250
        ';
251 1
        return (bool) $this->redis->eval($script, [$this->name, $this->mutex, $this->value, $this->ttl], 2);
252
    }
253
254
}
255