1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of ninja-mutex. |
4
|
|
|
* |
5
|
|
|
* (C) Kamil Dziedzic <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
namespace NinjaMutex\Lock; |
11
|
|
|
|
12
|
|
|
use Predis; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Lock implementor using Predis (client library for Redis) |
16
|
|
|
* |
17
|
|
|
* @author Kamil Dziedzic <[email protected]> |
18
|
|
|
*/ |
19
|
|
|
class PredisRedisLock extends LockAbstract implements LockExpirationInterface |
20
|
|
|
{ |
21
|
|
|
/** |
22
|
|
|
* Predis connection |
23
|
|
|
* |
24
|
|
|
* @var Predis\Client |
25
|
|
|
*/ |
26
|
|
|
protected $client; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var int Expiration time of the lock in seconds |
30
|
|
|
*/ |
31
|
|
|
protected $expiration = 0; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param $client Predis\Client |
35
|
|
|
*/ |
36
|
|
|
public function __construct(Predis\Client $client) |
37
|
|
|
{ |
38
|
|
|
parent::__construct(); |
39
|
|
|
|
40
|
|
|
$this->client = $client; |
41
|
|
|
} |
42
|
|
|
|
43
|
33 |
|
/** |
44
|
|
|
* @param int $expiration Expiration time of the lock in seconds |
45
|
33 |
|
*/ |
46
|
4 |
|
public function setExpiration($expiration) |
47
|
|
|
{ |
48
|
|
|
$this->expiration = $expiration; |
49
|
33 |
|
|
50
|
|
|
// Regenerate the lock information |
51
|
|
|
$this->lockInformation = $this->generateLockInformation(); |
|
|
|
|
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @inheritDoc |
56
|
|
|
*/ |
57
|
|
|
protected function generateLockInformation() |
58
|
33 |
|
{ |
59
|
|
|
$params = parent::generateLockInformation(); |
|
|
|
|
60
|
33 |
|
|
61
|
33 |
|
if ($this->expiration) { |
62
|
|
|
$params[] = time() + $this->expiration; |
63
|
33 |
|
} |
64
|
|
|
|
65
|
|
|
return $params; |
66
|
5 |
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* @param string $name |
70
|
|
|
* @param bool $blocking |
71
|
|
|
* @return bool |
72
|
|
|
*/ |
73
|
|
|
protected function getLock($name, $blocking) |
74
|
|
|
{ |
75
|
15 |
|
/** |
76
|
|
|
* Perform the process recommended by Redis for acquiring a lock, from here: https://redis.io/commands/setnx |
77
|
15 |
|
* We are "C4" in this example... |
78
|
|
|
* |
79
|
|
|
* 1. C4 sends SETNX lock.foo in order to acquire the lock (sets the value if it does not already exist). |
80
|
|
|
* 2. The crashed client C3 still holds it, so Redis will reply with 0 to C4. |
81
|
|
|
* 3. C4 sends GET lock.foo to check if the lock expired. |
82
|
|
|
* If it is not, it will sleep for some time and retry from the start. |
83
|
|
|
* 4. Instead, if the lock is expired because the Unix time at lock.foo is older than the current Unix time, |
84
|
|
|
* C4 tries to perform: |
85
|
|
|
* GETSET lock.foo <current Unix timestamp + lock timeout + 1> |
86
|
|
|
* Because of the GETSET semantic, C4 can check if the old value stored at key is still an expired timestamp |
87
|
|
|
* If it is, the lock was acquired. |
88
|
|
|
* 5. If another client, for instance C5, was faster than C4 and acquired the lock with the GETSET operation, |
89
|
|
|
* the C4 GETSET operation will return a non expired timestamp. |
90
|
|
|
* C4 will simply restart from the first step. Note that even if C4 wrote they key and set the expiry time |
91
|
|
|
* a few seconds in the future this is not a problem. C5's timeout will just be a few seconds later. |
92
|
|
|
*/ |
93
|
|
|
|
94
|
|
|
$lockValue = serialize($this->getLockInformation()); |
95
|
|
|
|
96
|
|
|
if ($this->client->setnx($name, $lockValue)) { |
97
|
|
|
return true; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
// Check if the existing lock has an expiry time. If it does and it has expired, delete the lock. |
101
|
|
|
if ($existingValue = $this->client->get($name)) { |
102
|
|
|
$existingValue = unserialize($existingValue); |
103
|
|
|
if (!empty($existingValue[3]) && $existingValue[3] <= time()) { |
104
|
|
|
// The existing lock has expired. We can delete it and take over. |
105
|
|
|
$newExistingValue = unserialize($this->client->getset($name, $lockValue)); |
106
|
|
|
|
107
|
|
|
// GETSET atomically sets key to value and returns the old value that was stored at key. |
108
|
|
|
// If the old value from getset does not still contain an expired timestamp |
109
|
|
|
// another probably acquired the lock in the meantime. |
110
|
|
|
if ($newExistingValue[3] > time()) { |
111
|
|
|
return false; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
// Got him. |
115
|
|
|
return true; |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return false; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Release lock |
124
|
|
|
* |
125
|
|
|
* @param string $name name of lock |
126
|
|
|
* @return bool |
127
|
|
|
*/ |
128
|
|
|
public function releaseLock($name) |
129
|
|
|
{ |
130
|
|
|
if (isset($this->locks[$name]) && $this->client->del([$name])) { |
131
|
|
|
unset($this->locks[$name]); |
132
|
|
|
|
133
|
|
|
return true; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
return false; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Check if lock is locked |
141
|
|
|
* |
142
|
|
|
* @param string $name name of lock |
143
|
|
|
* @return bool |
144
|
|
|
*/ |
145
|
|
|
public function isLocked($name) |
146
|
|
|
{ |
147
|
|
|
return null !== $this->client->get($name); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Clear lock without releasing it |
152
|
|
|
* Do not use this method unless you know what you do |
153
|
|
|
* |
154
|
|
|
* @param string $name name of lock |
155
|
|
|
* @return bool |
156
|
|
|
*/ |
157
|
|
|
public function clearLock($name) |
158
|
|
|
{ |
159
|
|
|
if (!isset($this->locks[$name])) { |
160
|
|
|
return false; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
unset($this->locks[$name]); |
164
|
|
|
return true; |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.