Completed
Pull Request — master (#8)
by
unknown
08:17
created

RedisLockStrategy::unlockAndSignal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
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
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
        if (\array_key_exists('redis_lock', $GLOBALS['TYPO3_CONF_VARS']['SYS'])) {
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
    private function lock()
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
    private function unlockAndSignal()
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