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'])) { |
|
|
|
|
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(); |
|
|
|
|
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); |
|
|
|
|
127
|
5 |
|
$this->mutex = sprintf('lock:mutex:%s', $subject); |
|
|
|
|
128
|
5 |
|
$this->value = uniqid(); |
|
|
|
|
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 get the 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() |
|
|
|
|
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 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 the mutex and if succeeds, signal the waiting locks |
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() |
|
|
|
|
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
|
|
|
|
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.