Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 75 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
RedisConnRef | |
0.00% |
0 / 75 |
|
0.00% |
0 / 16 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getServer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
clearLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__call | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
tryCall | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
scan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
sScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
zScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkAuthentication | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
postCallCleanup | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
luaEval | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
42 | |||
isConnIdentical | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__destruct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | use Psr\Log\LoggerAwareInterface; |
21 | use Psr\Log\LoggerInterface; |
22 | |
23 | /** |
24 | * Helper class to handle automatically marking connections as reusable (via RAII pattern) |
25 | * |
26 | * This class simply wraps the Redis class and can be used the same way |
27 | * |
28 | * @ingroup Redis |
29 | * @since 1.21 |
30 | */ |
31 | class RedisConnRef implements LoggerAwareInterface { |
32 | /** @var RedisConnectionPool */ |
33 | protected $pool; |
34 | /** @var Redis */ |
35 | protected $conn; |
36 | /** @var string */ |
37 | protected $server; |
38 | /** @var string|null */ |
39 | protected $lastError; |
40 | |
41 | /** |
42 | * @var LoggerInterface |
43 | */ |
44 | protected $logger; |
45 | |
46 | /** |
47 | * No authentication errors. |
48 | */ |
49 | private const AUTH_NO_ERROR = 200; |
50 | |
51 | /** |
52 | * Temporary authentication error; recovered by reauthenticating. |
53 | */ |
54 | private const AUTH_ERROR_TEMPORARY = 201; |
55 | |
56 | /** |
57 | * Authentication error was permanent and could not be recovered. |
58 | */ |
59 | private const AUTH_ERROR_PERMANENT = 202; |
60 | |
61 | /** |
62 | * @param RedisConnectionPool $pool |
63 | * @param string $server |
64 | * @param Redis $conn |
65 | * @param LoggerInterface $logger |
66 | */ |
67 | public function __construct( |
68 | RedisConnectionPool $pool, $server, Redis $conn, LoggerInterface $logger |
69 | ) { |
70 | $this->pool = $pool; |
71 | $this->server = $server; |
72 | $this->conn = $conn; |
73 | $this->logger = $logger; |
74 | } |
75 | |
76 | public function setLogger( LoggerInterface $logger ) { |
77 | $this->logger = $logger; |
78 | } |
79 | |
80 | /** |
81 | * @return string |
82 | * @since 1.23 |
83 | */ |
84 | public function getServer() { |
85 | return $this->server; |
86 | } |
87 | |
88 | public function getLastError() { |
89 | return $this->lastError; |
90 | } |
91 | |
92 | public function clearLastError() { |
93 | $this->lastError = null; |
94 | } |
95 | |
96 | /** |
97 | * Magic __call handler for most Redis functions. |
98 | * |
99 | * @param string $name |
100 | * @param array $arguments |
101 | * @return mixed |
102 | * @throws RedisException |
103 | */ |
104 | public function __call( $name, $arguments ) { |
105 | // Work around https://github.com/nicolasff/phpredis/issues/70 |
106 | $lname = strtolower( $name ); |
107 | if ( |
108 | ( $lname === 'blpop' || $lname === 'brpop' || $lname === 'brpoplpush' ) |
109 | && count( $arguments ) > 1 |
110 | ) { |
111 | // Get timeout off the end since it is always required and argument length can vary |
112 | $timeout = end( $arguments ); |
113 | // Only give the additional one second buffer if not requesting an infinite timeout |
114 | $this->pool->resetTimeout( $this->conn, ( $timeout > 0 ? $timeout + 1 : $timeout ) ); |
115 | } |
116 | |
117 | return $this->tryCall( $name, $arguments ); |
118 | } |
119 | |
120 | /** |
121 | * Do the method call in the common try catch handler. |
122 | * |
123 | * @param string $method |
124 | * @param array $arguments |
125 | * @return mixed |
126 | * @throws RedisException |
127 | */ |
128 | private function tryCall( $method, $arguments ) { |
129 | $this->conn->clearLastError(); |
130 | try { |
131 | $res = $this->conn->$method( ...$arguments ); |
132 | $authError = $this->checkAuthentication(); |
133 | if ( $authError === self::AUTH_ERROR_TEMPORARY ) { |
134 | $res = $this->conn->$method( ...$arguments ); |
135 | } |
136 | if ( $authError === self::AUTH_ERROR_PERMANENT ) { |
137 | throw new RedisException( "Failure reauthenticating to Redis." ); |
138 | } |
139 | return $res; |
140 | } finally { |
141 | $this->postCallCleanup(); |
142 | } |
143 | } |
144 | |
145 | /** |
146 | * Key Scan |
147 | * Handle this explicitly due to needing the iterator passed by reference. |
148 | * See: https://github.com/phpredis/phpredis#scan |
149 | * |
150 | * @param int &$iterator |
151 | * @param string|null $pattern |
152 | * @param int|null $count |
153 | * @return array |
154 | */ |
155 | public function scan( &$iterator, $pattern = null, $count = null ) { |
156 | return $this->tryCall( 'scan', [ &$iterator, $pattern, $count ] ); |
157 | } |
158 | |
159 | /** |
160 | * Set Scan |
161 | * Handle this explicitly due to needing the iterator passed by reference. |
162 | * See: https://github.com/phpredis/phpredis#sScan |
163 | * |
164 | * @param string $key |
165 | * @param int &$iterator |
166 | * @param string|null $pattern |
167 | * @param int|null $count |
168 | * @return array |
169 | */ |
170 | public function sScan( $key, &$iterator, $pattern = null, $count = null ) { |
171 | return $this->tryCall( 'sScan', [ $key, &$iterator, $pattern, $count ] ); |
172 | } |
173 | |
174 | /** |
175 | * Hash Scan |
176 | * Handle this explicitly due to needing the iterator passed by reference. |
177 | * See: https://github.com/phpredis/phpredis#hScan |
178 | * |
179 | * @param string $key |
180 | * @param int &$iterator |
181 | * @param string|null $pattern |
182 | * @param int|null $count |
183 | * @return array |
184 | */ |
185 | public function hScan( $key, &$iterator, $pattern = null, $count = null ) { |
186 | return $this->tryCall( 'hScan', [ $key, &$iterator, $pattern, $count ] ); |
187 | } |
188 | |
189 | /** |
190 | * Sorted Set Scan |
191 | * Handle this explicitly due to needing the iterator passed by reference. |
192 | * See: https://github.com/phpredis/phpredis#hScan |
193 | * |
194 | * @param string $key |
195 | * @param int &$iterator |
196 | * @param string|null $pattern |
197 | * @param int|null $count |
198 | * @return array |
199 | */ |
200 | public function zScan( $key, &$iterator, $pattern = null, $count = null ) { |
201 | return $this->tryCall( 'zScan', [ $key, &$iterator, $pattern, $count ] ); |
202 | } |
203 | |
204 | /** |
205 | * Handle authentication errors and automatically reauthenticate. |
206 | * |
207 | * @return int self::AUTH_NO_ERROR, self::AUTH_ERROR_TEMPORARY, or self::AUTH_ERROR_PERMANENT |
208 | */ |
209 | private function checkAuthentication() { |
210 | $lastError = $this->conn->getLastError(); |
211 | if ( $lastError && preg_match( '/^ERR operation not permitted\b/', $lastError ) ) { |
212 | if ( !$this->pool->reauthenticateConnection( $this->server, $this->conn ) ) { |
213 | return self::AUTH_ERROR_PERMANENT; |
214 | } |
215 | $this->conn->clearLastError(); |
216 | $this->logger->info( |
217 | "Used automatic re-authentication for Redis.", |
218 | [ 'redis_server' => $this->server ] |
219 | ); |
220 | return self::AUTH_ERROR_TEMPORARY; |
221 | } |
222 | return self::AUTH_NO_ERROR; |
223 | } |
224 | |
225 | /** |
226 | * Post Redis call cleanup. |
227 | * |
228 | * @return void |
229 | */ |
230 | private function postCallCleanup() { |
231 | $this->lastError = $this->conn->getLastError() ?: $this->lastError; |
232 | |
233 | // Restore original timeout in the case of blocking calls. |
234 | $this->pool->resetTimeout( $this->conn ); |
235 | } |
236 | |
237 | /** |
238 | * @param string $script |
239 | * @param array $params |
240 | * @param int $numKeys |
241 | * @return mixed |
242 | * @throws RedisException |
243 | */ |
244 | public function luaEval( $script, array $params, $numKeys ) { |
245 | $sha1 = sha1( $script ); // 40 char hex |
246 | $conn = $this->conn; // convenience |
247 | $server = $this->server; // convenience |
248 | |
249 | // Try to run the server-side cached copy of the script |
250 | $conn->clearLastError(); |
251 | $res = $conn->evalSha( $sha1, $params, $numKeys ); |
252 | // If we got a permission error reply that means that (a) we are not in |
253 | // multi()/pipeline() and (b) some connection problem likely occurred. If |
254 | // the password the client gave was just wrong, an exception should have |
255 | // been thrown back in getConnection() previously. |
256 | $lastError = $conn->getLastError(); |
257 | if ( $lastError && preg_match( '/^ERR operation not permitted\b/', $lastError ) ) { |
258 | $this->pool->reauthenticateConnection( $server, $conn ); |
259 | $conn->clearLastError(); |
260 | $res = $conn->eval( $script, $params, $numKeys ); |
261 | $this->logger->info( |
262 | "Used automatic re-authentication for Lua script '$sha1'.", |
263 | [ 'redis_server' => $server ] |
264 | ); |
265 | } |
266 | // If the script is not in cache, use eval() to retry and cache it |
267 | $lastError = $conn->getLastError(); |
268 | if ( $lastError && preg_match( '/^NOSCRIPT/', $lastError ) ) { |
269 | $conn->clearLastError(); |
270 | $res = $conn->eval( $script, $params, $numKeys ); |
271 | $this->logger->info( |
272 | "Used eval() for Lua script '$sha1'.", |
273 | [ 'redis_server' => $server ] |
274 | ); |
275 | } |
276 | |
277 | $lastError = $conn->getLastError(); |
278 | if ( $lastError ) { // script bug? |
279 | $this->logger->error( |
280 | 'Lua script error on server "{redis_server}": {lua_error}', |
281 | [ |
282 | 'redis_server' => $server, |
283 | 'lua_error' => $lastError |
284 | ] |
285 | ); |
286 | $this->lastError = $lastError; |
287 | } |
288 | |
289 | return $res; |
290 | } |
291 | |
292 | /** |
293 | * @param Redis $conn |
294 | * @return bool |
295 | */ |
296 | public function isConnIdentical( Redis $conn ) { |
297 | return $this->conn === $conn; |
298 | } |
299 | |
300 | public function __destruct() { |
301 | $this->pool->freeConnection( $this->server, $this->conn ); |
302 | } |
303 | } |