These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Object caching using Redis (http://redis.io/). |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | */ |
||
22 | |||
23 | /** |
||
24 | * Redis-based caching module for redis server >= 2.6.12 |
||
25 | * |
||
26 | * @note: avoid use of Redis::MULTI transactions for twemproxy support |
||
27 | */ |
||
28 | class RedisBagOStuff extends BagOStuff { |
||
29 | /** @var RedisConnectionPool */ |
||
30 | protected $redisPool; |
||
31 | /** @var array List of server names */ |
||
32 | protected $servers; |
||
33 | /** @var array Map of (tag => server name) */ |
||
34 | protected $serverTagMap; |
||
35 | /** @var bool */ |
||
36 | protected $automaticFailover; |
||
37 | |||
38 | /** |
||
39 | * Construct a RedisBagOStuff object. Parameters are: |
||
40 | * |
||
41 | * - servers: An array of server names. A server name may be a hostname, |
||
42 | * a hostname/port combination or the absolute path of a UNIX socket. |
||
43 | * If a hostname is specified but no port, the standard port number |
||
44 | * 6379 will be used. Arrays keys can be used to specify the tag to |
||
45 | * hash on in place of the host/port. Required. |
||
46 | * |
||
47 | * - connectTimeout: The timeout for new connections, in seconds. Optional, |
||
48 | * default is 1 second. |
||
49 | * |
||
50 | * - persistent: Set this to true to allow connections to persist across |
||
51 | * multiple web requests. False by default. |
||
52 | * |
||
53 | * - password: The authentication password, will be sent to Redis in |
||
54 | * clear text. Optional, if it is unspecified, no AUTH command will be |
||
55 | * sent. |
||
56 | * |
||
57 | * - automaticFailover: If this is false, then each key will be mapped to |
||
58 | * a single server, and if that server is down, any requests for that key |
||
59 | * will fail. If this is true, a connection failure will cause the client |
||
60 | * to immediately try the next server in the list (as determined by a |
||
61 | * consistent hashing algorithm). True by default. This has the |
||
62 | * potential to create consistency issues if a server is slow enough to |
||
63 | * flap, for example if it is in swap death. |
||
64 | * @param array $params |
||
65 | */ |
||
66 | function __construct( $params ) { |
||
67 | parent::__construct( $params ); |
||
68 | $redisConf = [ 'serializer' => 'none' ]; // manage that in this class |
||
69 | foreach ( [ 'connectTimeout', 'persistent', 'password' ] as $opt ) { |
||
70 | if ( isset( $params[$opt] ) ) { |
||
71 | $redisConf[$opt] = $params[$opt]; |
||
72 | } |
||
73 | } |
||
74 | $this->redisPool = RedisConnectionPool::singleton( $redisConf ); |
||
75 | |||
76 | $this->servers = $params['servers']; |
||
77 | foreach ( $this->servers as $key => $server ) { |
||
78 | $this->serverTagMap[is_int( $key ) ? $server : $key] = $server; |
||
79 | } |
||
80 | |||
81 | if ( isset( $params['automaticFailover'] ) ) { |
||
82 | $this->automaticFailover = $params['automaticFailover']; |
||
83 | } else { |
||
84 | $this->automaticFailover = true; |
||
85 | } |
||
86 | |||
87 | $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE; |
||
88 | } |
||
89 | |||
90 | View Code Duplication | protected function doGet( $key, $flags = 0 ) { |
|
91 | list( $server, $conn ) = $this->getConnection( $key ); |
||
92 | if ( !$conn ) { |
||
93 | return false; |
||
94 | } |
||
95 | try { |
||
96 | $value = $conn->get( $key ); |
||
97 | $result = $this->unserialize( $value ); |
||
98 | } catch ( RedisException $e ) { |
||
99 | $result = false; |
||
100 | $this->handleException( $conn, $e ); |
||
101 | } |
||
102 | |||
103 | $this->logRequest( 'get', $key, $server, $result ); |
||
104 | return $result; |
||
105 | } |
||
106 | |||
107 | View Code Duplication | public function set( $key, $value, $expiry = 0, $flags = 0 ) { |
|
108 | list( $server, $conn ) = $this->getConnection( $key ); |
||
109 | if ( !$conn ) { |
||
110 | return false; |
||
111 | } |
||
112 | $expiry = $this->convertToRelative( $expiry ); |
||
113 | try { |
||
114 | if ( $expiry ) { |
||
115 | $result = $conn->setex( $key, $expiry, $this->serialize( $value ) ); |
||
116 | } else { |
||
117 | // No expiry, that is very different from zero expiry in Redis |
||
118 | $result = $conn->set( $key, $this->serialize( $value ) ); |
||
119 | } |
||
120 | } catch ( RedisException $e ) { |
||
121 | $result = false; |
||
122 | $this->handleException( $conn, $e ); |
||
123 | } |
||
124 | |||
125 | $this->logRequest( 'set', $key, $server, $result ); |
||
126 | return $result; |
||
127 | } |
||
128 | |||
129 | public function delete( $key ) { |
||
130 | list( $server, $conn ) = $this->getConnection( $key ); |
||
131 | if ( !$conn ) { |
||
132 | return false; |
||
133 | } |
||
134 | try { |
||
135 | $conn->delete( $key ); |
||
136 | // Return true even if the key didn't exist |
||
137 | $result = true; |
||
138 | } catch ( RedisException $e ) { |
||
139 | $result = false; |
||
140 | $this->handleException( $conn, $e ); |
||
141 | } |
||
142 | |||
143 | $this->logRequest( 'delete', $key, $server, $result ); |
||
144 | return $result; |
||
145 | } |
||
146 | |||
147 | public function getMulti( array $keys, $flags = 0 ) { |
||
148 | $batches = []; |
||
149 | $conns = []; |
||
150 | View Code Duplication | foreach ( $keys as $key ) { |
|
151 | list( $server, $conn ) = $this->getConnection( $key ); |
||
152 | if ( !$conn ) { |
||
153 | continue; |
||
154 | } |
||
155 | $conns[$server] = $conn; |
||
156 | $batches[$server][] = $key; |
||
157 | } |
||
158 | $result = []; |
||
159 | foreach ( $batches as $server => $batchKeys ) { |
||
160 | $conn = $conns[$server]; |
||
161 | try { |
||
162 | $conn->multi( Redis::PIPELINE ); |
||
163 | foreach ( $batchKeys as $key ) { |
||
164 | $conn->get( $key ); |
||
165 | } |
||
166 | $batchResult = $conn->exec(); |
||
167 | if ( $batchResult === false ) { |
||
168 | $this->debug( "multi request to $server failed" ); |
||
169 | continue; |
||
170 | } |
||
171 | foreach ( $batchResult as $i => $value ) { |
||
172 | if ( $value !== false ) { |
||
173 | $result[$batchKeys[$i]] = $this->unserialize( $value ); |
||
174 | } |
||
175 | } |
||
176 | } catch ( RedisException $e ) { |
||
177 | $this->handleException( $conn, $e ); |
||
178 | } |
||
179 | } |
||
180 | |||
181 | $this->debug( "getMulti for " . count( $keys ) . " keys " . |
||
182 | "returned " . count( $result ) . " results" ); |
||
183 | return $result; |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * @param array $data |
||
188 | * @param int $expiry |
||
189 | * @return bool |
||
190 | */ |
||
191 | public function setMulti( array $data, $expiry = 0 ) { |
||
192 | $batches = []; |
||
193 | $conns = []; |
||
194 | View Code Duplication | foreach ( $data as $key => $value ) { |
|
195 | list( $server, $conn ) = $this->getConnection( $key ); |
||
196 | if ( !$conn ) { |
||
197 | continue; |
||
198 | } |
||
199 | $conns[$server] = $conn; |
||
200 | $batches[$server][] = $key; |
||
201 | } |
||
202 | |||
203 | $expiry = $this->convertToRelative( $expiry ); |
||
204 | $result = true; |
||
205 | foreach ( $batches as $server => $batchKeys ) { |
||
206 | $conn = $conns[$server]; |
||
207 | try { |
||
208 | $conn->multi( Redis::PIPELINE ); |
||
209 | foreach ( $batchKeys as $key ) { |
||
210 | if ( $expiry ) { |
||
211 | $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) ); |
||
212 | } else { |
||
213 | $conn->set( $key, $this->serialize( $data[$key] ) ); |
||
214 | } |
||
215 | } |
||
216 | $batchResult = $conn->exec(); |
||
217 | if ( $batchResult === false ) { |
||
218 | $this->debug( "setMulti request to $server failed" ); |
||
219 | continue; |
||
220 | } |
||
221 | foreach ( $batchResult as $value ) { |
||
222 | if ( $value === false ) { |
||
223 | $result = false; |
||
224 | } |
||
225 | } |
||
226 | } catch ( RedisException $e ) { |
||
227 | $this->handleException( $server, $conn, $e ); |
||
228 | $result = false; |
||
229 | } |
||
230 | } |
||
231 | |||
232 | return $result; |
||
233 | } |
||
234 | |||
235 | View Code Duplication | public function add( $key, $value, $expiry = 0 ) { |
|
236 | list( $server, $conn ) = $this->getConnection( $key ); |
||
237 | if ( !$conn ) { |
||
238 | return false; |
||
239 | } |
||
240 | $expiry = $this->convertToRelative( $expiry ); |
||
241 | try { |
||
242 | if ( $expiry ) { |
||
243 | $result = $conn->set( |
||
244 | $key, |
||
245 | $this->serialize( $value ), |
||
246 | [ 'nx', 'ex' => $expiry ] |
||
247 | ); |
||
248 | } else { |
||
249 | $result = $conn->setnx( $key, $this->serialize( $value ) ); |
||
250 | } |
||
251 | } catch ( RedisException $e ) { |
||
252 | $result = false; |
||
253 | $this->handleException( $conn, $e ); |
||
254 | } |
||
255 | |||
256 | $this->logRequest( 'add', $key, $server, $result ); |
||
257 | return $result; |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * Non-atomic implementation of incr(). |
||
262 | * |
||
263 | * Probably all callers actually want incr() to atomically initialise |
||
264 | * values to zero if they don't exist, as provided by the Redis INCR |
||
265 | * command. But we are constrained by the memcached-like interface to |
||
266 | * return null in that case. Once the key exists, further increments are |
||
267 | * atomic. |
||
268 | * @param string $key Key to increase |
||
269 | * @param int $value Value to add to $key (Default 1) |
||
270 | * @return int|bool New value or false on failure |
||
271 | */ |
||
272 | View Code Duplication | public function incr( $key, $value = 1 ) { |
|
273 | list( $server, $conn ) = $this->getConnection( $key ); |
||
274 | if ( !$conn ) { |
||
275 | return false; |
||
276 | } |
||
277 | try { |
||
278 | if ( !$conn->exists( $key ) ) { |
||
279 | return null; |
||
280 | } |
||
281 | // @FIXME: on races, the key may have a 0 TTL |
||
282 | $result = $conn->incrBy( $key, $value ); |
||
283 | } catch ( RedisException $e ) { |
||
284 | $result = false; |
||
285 | $this->handleException( $conn, $e ); |
||
286 | } |
||
287 | |||
288 | $this->logRequest( 'incr', $key, $server, $result ); |
||
289 | return $result; |
||
290 | } |
||
291 | |||
292 | View Code Duplication | public function changeTTL( $key, $expiry = 0 ) { |
|
293 | list( $server, $conn ) = $this->getConnection( $key ); |
||
294 | if ( !$conn ) { |
||
295 | return false; |
||
296 | } |
||
297 | |||
298 | $expiry = $this->convertToRelative( $expiry ); |
||
299 | try { |
||
300 | $result = $conn->expire( $key, $expiry ); |
||
301 | } catch ( RedisException $e ) { |
||
0 ignored issues
–
show
|
|||
302 | $result = false; |
||
303 | $this->handleException( $conn, $e ); |
||
304 | } |
||
305 | |||
306 | $this->logRequest( 'expire', $key, $server, $result ); |
||
307 | return $result; |
||
308 | } |
||
309 | |||
310 | public function modifySimpleRelayEvent( array $event ) { |
||
311 | if ( array_key_exists( 'val', $event ) ) { |
||
312 | $event['val'] = serialize( $event['val'] ); // this class uses PHP serialization |
||
313 | } |
||
314 | |||
315 | return $event; |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * @param mixed $data |
||
320 | * @return string |
||
321 | */ |
||
322 | protected function serialize( $data ) { |
||
323 | // Serialize anything but integers so INCR/DECR work |
||
324 | // Do not store integer-like strings as integers to avoid type confusion (bug 60563) |
||
325 | return is_int( $data ) ? $data : serialize( $data ); |
||
326 | } |
||
327 | |||
328 | /** |
||
329 | * @param string $data |
||
330 | * @return mixed |
||
331 | */ |
||
332 | protected function unserialize( $data ) { |
||
333 | $int = intval( $data ); |
||
334 | return $data === (string)$int ? $int : unserialize( $data ); |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * Get a Redis object with a connection suitable for fetching the specified key |
||
339 | * @param string $key |
||
340 | * @return array (server, RedisConnRef) or (false, false) |
||
341 | */ |
||
342 | protected function getConnection( $key ) { |
||
343 | $candidates = array_keys( $this->serverTagMap ); |
||
344 | |||
345 | if ( count( $this->servers ) > 1 ) { |
||
346 | ArrayUtils::consistentHashSort( $candidates, $key, '/' ); |
||
347 | if ( !$this->automaticFailover ) { |
||
348 | $candidates = array_slice( $candidates, 0, 1 ); |
||
349 | } |
||
350 | } |
||
351 | |||
352 | while ( ( $tag = array_shift( $candidates ) ) !== null ) { |
||
353 | $server = $this->serverTagMap[$tag]; |
||
354 | $conn = $this->redisPool->getConnection( $server, $this->logger ); |
||
355 | if ( !$conn ) { |
||
356 | continue; |
||
357 | } |
||
358 | |||
359 | // If automatic failover is enabled, check that the server's link |
||
360 | // to its master (if any) is up -- but only if there are other |
||
361 | // viable candidates left to consider. Also, getMasterLinkStatus() |
||
362 | // does not work with twemproxy, though $candidates will be empty |
||
363 | // by now in such cases. |
||
364 | if ( $this->automaticFailover && $candidates ) { |
||
365 | try { |
||
366 | if ( $this->getMasterLinkStatus( $conn ) === 'down' ) { |
||
367 | // If the master cannot be reached, fail-over to the next server. |
||
368 | // If masters are in data-center A, and replica DBs in data-center B, |
||
369 | // this helps avoid the case were fail-over happens in A but not |
||
370 | // to the corresponding server in B (e.g. read/write mismatch). |
||
371 | continue; |
||
372 | } |
||
373 | } catch ( RedisException $e ) { |
||
0 ignored issues
–
show
The class
RedisException does not exist. Did you forget a USE statement, or did you not list all dependencies?
Scrutinizer analyzes your It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis. ![]() |
|||
374 | // Server is not accepting commands |
||
375 | $this->handleException( $conn, $e ); |
||
376 | continue; |
||
377 | } |
||
378 | } |
||
379 | |||
380 | return [ $server, $conn ]; |
||
381 | } |
||
382 | |||
383 | $this->setLastError( BagOStuff::ERR_UNREACHABLE ); |
||
384 | |||
385 | return [ false, false ]; |
||
386 | } |
||
387 | |||
388 | /** |
||
389 | * Check the master link status of a Redis server that is configured as a replica DB. |
||
390 | * @param RedisConnRef $conn |
||
391 | * @return string|null Master link status (either 'up' or 'down'), or null |
||
392 | * if the server is not a replica DB. |
||
393 | */ |
||
394 | protected function getMasterLinkStatus( RedisConnRef $conn ) { |
||
395 | $info = $conn->info(); |
||
0 ignored issues
–
show
The method
info does not exist on object<RedisConnRef> ? Since you implemented __call , maybe consider adding a @method annotation.
If you implement This is often the case, when class ParentClass {
private $data = array();
public function __call($method, array $args) {
if (0 === strpos($method, 'get')) {
return $this->data[strtolower(substr($method, 3))];
}
throw new \LogicException(sprintf('Unsupported method: %s', $method));
}
}
/**
* If this class knows which fields exist, you can specify the methods here:
*
* @method string getName()
*/
class SomeClass extends ParentClass { }
![]() |
|||
396 | return isset( $info['master_link_status'] ) |
||
397 | ? $info['master_link_status'] |
||
398 | : null; |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * Log a fatal error |
||
403 | * @param string $msg |
||
404 | */ |
||
405 | protected function logError( $msg ) { |
||
406 | $this->logger->error( "Redis error: $msg" ); |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * The redis extension throws an exception in response to various read, write |
||
411 | * and protocol errors. Sometimes it also closes the connection, sometimes |
||
412 | * not. The safest response for us is to explicitly destroy the connection |
||
413 | * object and let it be reopened during the next request. |
||
414 | * @param RedisConnRef $conn |
||
415 | * @param Exception $e |
||
416 | */ |
||
417 | protected function handleException( RedisConnRef $conn, $e ) { |
||
418 | $this->setLastError( BagOStuff::ERR_UNEXPECTED ); |
||
419 | $this->redisPool->handleError( $conn, $e ); |
||
420 | } |
||
421 | |||
422 | /** |
||
423 | * Send information about a single request to the debug log |
||
424 | * @param string $method |
||
425 | * @param string $key |
||
426 | * @param string $server |
||
427 | * @param bool $result |
||
428 | */ |
||
429 | public function logRequest( $method, $key, $server, $result ) { |
||
430 | $this->debug( "$method $key on $server: " . |
||
431 | ( $result === false ? "failure" : "success" ) ); |
||
432 | } |
||
433 | } |
||
434 |
Scrutinizer analyzes your
composer.json
/composer.lock
file if available to determine the classes, and functions that are defined by your dependencies.It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.