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

RedisLockStrategy::getPriority()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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