Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 404 |
|
0.00% |
0 / 38 |
CRAP | |
0.00% |
0 / 1 |
MemcachedClient | |
0.00% |
0 / 404 |
|
0.00% |
0 / 38 |
27390 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
serialize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unserialize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
add | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
decr | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
delete | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
touch | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
disconnect_all | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
enable_compress | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
forget_dead_hosts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
get | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
210 | |||
get_multi | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
210 | |||
incr | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
replace | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run_command | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
set | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cas | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
set_compress_threshold | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
set_debug | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
set_servers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
set_timeout | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
_close_sock | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
_connect_sock | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
72 | |||
_dead_sock | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
_dead_host | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
get_sock | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
132 | |||
_hashfunc | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
_incrdecr | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
72 | |||
_load_items | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
156 | |||
_set | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
420 | |||
sock_to_host | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
72 | |||
_debugprint | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
_error_log | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
_fwrite | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
_handle_error | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
_fread | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
_fgets | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
_flush_read_buffer | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | // phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother. |
3 | /** |
4 | * Memcached client for PHP. |
5 | * |
6 | * +---------------------------------------------------------------------------+ |
7 | * | memcached client, PHP | |
8 | * +---------------------------------------------------------------------------+ |
9 | * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net> | |
10 | * | All rights reserved. | |
11 | * | | |
12 | * | Redistribution and use in source and binary forms, with or without | |
13 | * | modification, are permitted provided that the following conditions | |
14 | * | are met: | |
15 | * | | |
16 | * | 1. Redistributions of source code must retain the above copyright | |
17 | * | notice, this list of conditions and the following disclaimer. | |
18 | * | 2. Redistributions in binary form must reproduce the above copyright | |
19 | * | notice, this list of conditions and the following disclaimer in the | |
20 | * | documentation and/or other materials provided with the distribution. | |
21 | * | | |
22 | * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR | |
23 | * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |
24 | * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. | |
25 | * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, | |
26 | * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | |
27 | * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
28 | * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
29 | * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
30 | * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | |
31 | * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
32 | * +---------------------------------------------------------------------------+ |
33 | * | Author: Ryan T. Dean <rtdean@cytherianage.net> | |
34 | * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick. | |
35 | * | Permission granted by Brad Fitzpatrick for relicense of ported Perl | |
36 | * | client logic under 2-clause BSD license. | |
37 | * +---------------------------------------------------------------------------+ |
38 | * |
39 | * @file |
40 | * $TCAnet$ |
41 | */ |
42 | |
43 | /** |
44 | * This is a PHP client for memcached - a distributed memory cache daemon. |
45 | * |
46 | * More information is available at http://www.danga.com/memcached/ |
47 | * |
48 | * Usage example: |
49 | * |
50 | * $mc = new MemcachedClient(array( |
51 | * 'servers' => array( |
52 | * '127.0.0.1:10000', |
53 | * array( '192.0.0.1:10010', 2 ), |
54 | * '127.0.0.1:10020' |
55 | * ), |
56 | * 'debug' => false, |
57 | * 'compress_threshold' => 10240, |
58 | * 'persistent' => true |
59 | * )); |
60 | * |
61 | * $mc->add( 'key', array( 'some', 'array' ) ); |
62 | * $mc->replace( 'key', 'some random string' ); |
63 | * $val = $mc->get( 'key' ); |
64 | * |
65 | * @author Ryan T. Dean <rtdean@cytherianage.net> |
66 | * @version 0.1.2 |
67 | */ |
68 | |
69 | use Psr\Log\LoggerInterface; |
70 | use Psr\Log\NullLogger; |
71 | use Wikimedia\AtEase\AtEase; |
72 | use Wikimedia\IPUtils; |
73 | use Wikimedia\LightweightObjectStore\StorageAwareness; |
74 | |
75 | // {{{ class MemcachedClient |
76 | /** |
77 | * memcached client class implemented using (p)fsockopen() |
78 | * |
79 | * @author Ryan T. Dean <rtdean@cytherianage.net> |
80 | * @ingroup Cache |
81 | */ |
82 | class MemcachedClient implements StorageAwareness { |
83 | // {{{ properties |
84 | // {{{ public |
85 | |
86 | // {{{ constants |
87 | // {{{ flags |
88 | |
89 | /** |
90 | * Flag: indicates data is serialized |
91 | */ |
92 | const SERIALIZED = 1; |
93 | |
94 | /** |
95 | * Flag: indicates data is compressed |
96 | */ |
97 | const COMPRESSED = 2; |
98 | |
99 | /** |
100 | * Flag: indicates data is an integer |
101 | */ |
102 | const INTVAL = 4; |
103 | |
104 | // }}} |
105 | |
106 | /** |
107 | * Minimum savings to store data compressed |
108 | */ |
109 | const COMPRESSION_SAVINGS = 0.20; |
110 | |
111 | // }}} |
112 | |
113 | /** |
114 | * Command statistics |
115 | * |
116 | * @var array |
117 | * @access public |
118 | */ |
119 | public $stats; |
120 | |
121 | // }}} |
122 | // {{{ private |
123 | |
124 | /** |
125 | * Cached Sockets that are connected |
126 | * |
127 | * @var array |
128 | * @access private |
129 | */ |
130 | public $_cache_sock; |
131 | |
132 | /** |
133 | * Current debug status; 0 - none to 9 - profiling |
134 | * |
135 | * @var bool |
136 | * @access private |
137 | */ |
138 | public $_debug; |
139 | |
140 | /** |
141 | * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again' |
142 | * |
143 | * @var array |
144 | * @access private |
145 | */ |
146 | public $_host_dead; |
147 | |
148 | /** |
149 | * Is compression available? |
150 | * |
151 | * @var bool |
152 | * @access private |
153 | */ |
154 | public $_have_zlib; |
155 | |
156 | /** |
157 | * Do we want to use compression? |
158 | * |
159 | * @var bool |
160 | * @access private |
161 | */ |
162 | public $_compress_enable; |
163 | |
164 | /** |
165 | * At how many bytes should we compress? |
166 | * |
167 | * @var int |
168 | * @access private |
169 | */ |
170 | public $_compress_threshold; |
171 | |
172 | /** |
173 | * Are we using persistent links? |
174 | * |
175 | * @var bool |
176 | * @access private |
177 | */ |
178 | public $_persistent; |
179 | |
180 | /** |
181 | * If only using one server; contains ip:port to connect to |
182 | * |
183 | * @var string |
184 | * @access private |
185 | */ |
186 | public $_single_sock; |
187 | |
188 | /** |
189 | * Array containing ip:port or array(ip:port, weight) |
190 | * |
191 | * @var array |
192 | * @access private |
193 | */ |
194 | public $_servers; |
195 | |
196 | /** |
197 | * Our bit buckets |
198 | * |
199 | * @var array |
200 | * @access private |
201 | */ |
202 | public $_buckets; |
203 | |
204 | /** |
205 | * Total # of bit buckets we have |
206 | * |
207 | * @var int |
208 | * @access private |
209 | */ |
210 | public $_bucketcount; |
211 | |
212 | /** |
213 | * # of total servers we have |
214 | * |
215 | * @var int |
216 | * @access private |
217 | */ |
218 | public $_active; |
219 | |
220 | /** |
221 | * Stream timeout in seconds. Applies for example to fread() |
222 | * |
223 | * @var int |
224 | * @access private |
225 | */ |
226 | public $_timeout_seconds; |
227 | |
228 | /** |
229 | * Stream timeout in microseconds |
230 | * |
231 | * @var int |
232 | * @access private |
233 | */ |
234 | public $_timeout_microseconds; |
235 | |
236 | /** |
237 | * Connect timeout in seconds |
238 | */ |
239 | public $_connect_timeout; |
240 | |
241 | /** |
242 | * Number of connection attempts for each server |
243 | */ |
244 | public $_connect_attempts; |
245 | |
246 | /** @var int StorageAwareness:ERR_* constant of the last cache command */ |
247 | public $_last_cmd_status = self::ERR_NONE; |
248 | |
249 | /** |
250 | * @var LoggerInterface |
251 | */ |
252 | private $_logger; |
253 | |
254 | |
255 | // }}} |
256 | // }}} |
257 | // {{{ methods |
258 | // {{{ public functions |
259 | // {{{ memcached() |
260 | |
261 | /** |
262 | * Memcache initializer |
263 | * |
264 | * @param array $args Associative array of settings |
265 | */ |
266 | public function __construct( $args ) { |
267 | $this->set_servers( $args['servers'] ?? array() ); |
268 | $this->_debug = $args['debug'] ?? false; |
269 | $this->stats = array(); |
270 | $this->_compress_threshold = $args['compress_threshold'] ?? 0; |
271 | $this->_persistent = $args['persistent'] ?? false; |
272 | $this->_compress_enable = true; |
273 | $this->_have_zlib = function_exists( 'gzcompress' ); |
274 | |
275 | $this->_cache_sock = array(); |
276 | $this->_host_dead = array(); |
277 | |
278 | $this->_timeout_seconds = 0; |
279 | $this->_timeout_microseconds = $args['timeout'] ?? 500_000; |
280 | |
281 | $this->_connect_timeout = $args['connect_timeout'] ?? 0.1; |
282 | $this->_connect_attempts = 2; |
283 | |
284 | $this->_logger = $args['logger'] ?? new NullLogger(); |
285 | } |
286 | |
287 | // }}} |
288 | |
289 | /** |
290 | * @param mixed $value |
291 | * @return string|integer |
292 | */ |
293 | public function serialize( $value ) { |
294 | return serialize( $value ); |
295 | } |
296 | |
297 | /** |
298 | * @param string $value |
299 | * @return mixed |
300 | */ |
301 | public function unserialize( $value ) { |
302 | return unserialize( $value ); |
303 | } |
304 | |
305 | // {{{ add() |
306 | |
307 | /** |
308 | * Adds a key/value to the memcache server if one isn't already set with |
309 | * that key |
310 | * |
311 | * @param string $key Key to set with data |
312 | * @param mixed $val Value to store |
313 | * @param int $exp (optional) Expiration time. This can be a number of seconds |
314 | * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or |
315 | * longer must be the timestamp of the time at which the mapping should expire. It |
316 | * is safe to use timestamps in all cases, regardless of expiration |
317 | * eg: strtotime("+3 hour") |
318 | * |
319 | * @return bool |
320 | */ |
321 | public function add( $key, $val, $exp = 0 ) { |
322 | $this->_last_cmd_status = self::ERR_NONE; |
323 | |
324 | return $this->_set( 'add', $key, $val, $exp ); |
325 | } |
326 | |
327 | // }}} |
328 | // {{{ decr() |
329 | |
330 | /** |
331 | * Decrease a value stored on the memcache server |
332 | * |
333 | * @param string $key Key to decrease |
334 | * @param int $amt (optional) amount to decrease |
335 | * |
336 | * @return mixed False on failure, value on success |
337 | */ |
338 | public function decr( $key, $amt = 1 ) { |
339 | $this->_last_cmd_status = self::ERR_NONE; |
340 | |
341 | return $this->_incrdecr( 'decr', $key, $amt ); |
342 | } |
343 | |
344 | // }}} |
345 | // {{{ delete() |
346 | |
347 | /** |
348 | * Deletes a key from the server, optionally after $time |
349 | * |
350 | * @param string $key Key to delete |
351 | * @param int $time (optional) how long to wait before deleting |
352 | * |
353 | * @return bool True on success, false on failure |
354 | */ |
355 | public function delete( $key, $time = 0 ) { |
356 | $this->_last_cmd_status = self::ERR_NONE; |
357 | |
358 | if ( !$this->_active ) { |
359 | return false; |
360 | } |
361 | |
362 | $sock = $this->get_sock( $key ); |
363 | if ( !$sock ) { |
364 | return false; |
365 | } |
366 | |
367 | $key = is_array( $key ) ? $key[1] : $key; |
368 | |
369 | if ( isset( $this->stats['delete'] ) ) { |
370 | $this->stats['delete']++; |
371 | } else { |
372 | $this->stats['delete'] = 1; |
373 | } |
374 | $cmd = "delete $key $time\r\n"; |
375 | if ( !$this->_fwrite( $sock, $cmd ) ) { |
376 | return false; |
377 | } |
378 | $res = $this->_fgets( $sock ); |
379 | |
380 | if ( $this->_debug ) { |
381 | $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) ); |
382 | } |
383 | |
384 | if ( $res == "DELETED" || $res == "NOT_FOUND" ) { |
385 | return true; |
386 | } |
387 | |
388 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
389 | |
390 | return false; |
391 | } |
392 | |
393 | /** |
394 | * Changes the TTL on a key from the server to $time |
395 | * |
396 | * @param string $key |
397 | * @param int $time TTL in seconds |
398 | * |
399 | * @return bool True on success, false on failure |
400 | */ |
401 | public function touch( $key, $time = 0 ) { |
402 | $this->_last_cmd_status = self::ERR_NONE; |
403 | |
404 | if ( !$this->_active ) { |
405 | return false; |
406 | } |
407 | |
408 | $sock = $this->get_sock( $key ); |
409 | if ( !$sock ) { |
410 | return false; |
411 | } |
412 | |
413 | $key = is_array( $key ) ? $key[1] : $key; |
414 | |
415 | if ( isset( $this->stats['touch'] ) ) { |
416 | $this->stats['touch']++; |
417 | } else { |
418 | $this->stats['touch'] = 1; |
419 | } |
420 | $cmd = "touch $key $time\r\n"; |
421 | if ( !$this->_fwrite( $sock, $cmd ) ) { |
422 | return false; |
423 | } |
424 | $res = $this->_fgets( $sock ); |
425 | |
426 | if ( $this->_debug ) { |
427 | $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) ); |
428 | } |
429 | |
430 | if ( $res == "TOUCHED" ) { |
431 | return true; |
432 | } |
433 | |
434 | return false; |
435 | } |
436 | |
437 | // }}} |
438 | // {{{ disconnect_all() |
439 | |
440 | /** |
441 | * Disconnects all connected sockets |
442 | */ |
443 | public function disconnect_all() { |
444 | foreach ( $this->_cache_sock as $sock ) { |
445 | fclose( $sock ); |
446 | } |
447 | |
448 | $this->_cache_sock = array(); |
449 | } |
450 | |
451 | // }}} |
452 | // {{{ enable_compress() |
453 | |
454 | /** |
455 | * Enable / Disable compression |
456 | * |
457 | * @param bool $enable True to enable, false to disable |
458 | */ |
459 | public function enable_compress( $enable ) { |
460 | $this->_compress_enable = $enable; |
461 | } |
462 | |
463 | // }}} |
464 | // {{{ forget_dead_hosts() |
465 | |
466 | /** |
467 | * Forget about all of the dead hosts |
468 | */ |
469 | public function forget_dead_hosts() { |
470 | $this->_host_dead = array(); |
471 | } |
472 | |
473 | // }}} |
474 | // {{{ get() |
475 | |
476 | /** |
477 | * Retrieves the value associated with the key from the memcache server |
478 | * |
479 | * @param array|string $key key to retrieve |
480 | * @param float $casToken [optional] |
481 | * |
482 | * @return mixed |
483 | */ |
484 | public function get( $key, &$casToken = null ) { |
485 | $getToken = ( func_num_args() >= 2 ); |
486 | |
487 | $this->_last_cmd_status = self::ERR_NONE; |
488 | |
489 | if ( $this->_debug ) { |
490 | $this->_debugprint( "get($key)" ); |
491 | } |
492 | |
493 | if ( !is_array( $key ) && strval( $key ) === '' ) { |
494 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
495 | $this->_debugprint( "Skipping key which equals to an empty string" ); |
496 | return false; |
497 | } |
498 | |
499 | if ( !$this->_active ) { |
500 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
501 | |
502 | return false; |
503 | } |
504 | |
505 | $sock = $this->get_sock( $key ); |
506 | |
507 | if ( !$sock ) { |
508 | $this->_last_cmd_status = self::ERR_UNREACHABLE; |
509 | |
510 | return false; |
511 | } |
512 | |
513 | $key = is_array( $key ) ? $key[1] : $key; |
514 | if ( isset( $this->stats['get'] ) ) { |
515 | $this->stats['get']++; |
516 | } else { |
517 | $this->stats['get'] = 1; |
518 | } |
519 | |
520 | $cmd = $getToken ? "gets" : "get"; |
521 | $cmd .= " $key\r\n"; |
522 | if ( !$this->_fwrite( $sock, $cmd ) ) { |
523 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
524 | |
525 | return false; |
526 | } |
527 | |
528 | $val = array(); |
529 | if ( !$this->_load_items( $sock, $val, $casToken ) ) { |
530 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
531 | } |
532 | |
533 | if ( $this->_debug ) { |
534 | foreach ( $val as $k => $v ) { |
535 | $this->_debugprint( |
536 | sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) ); |
537 | } |
538 | } |
539 | |
540 | $value = false; |
541 | if ( isset( $val[$key] ) ) { |
542 | $value = $val[$key]; |
543 | } |
544 | return $value; |
545 | } |
546 | |
547 | // }}} |
548 | // {{{ get_multi() |
549 | |
550 | /** |
551 | * Get multiple keys from the server(s) |
552 | * |
553 | * @param array $keys Keys to retrieve |
554 | * |
555 | * @return array |
556 | */ |
557 | public function get_multi( $keys ) { |
558 | $this->_last_cmd_status = self::ERR_NONE; |
559 | |
560 | if ( !$this->_active ) { |
561 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
562 | |
563 | return array(); |
564 | } |
565 | |
566 | if ( isset( $this->stats['get_multi'] ) ) { |
567 | $this->stats['get_multi']++; |
568 | } else { |
569 | $this->stats['get_multi'] = 1; |
570 | } |
571 | $sock_keys = array(); |
572 | $socks = array(); |
573 | foreach ( $keys as $key ) { |
574 | $sock = $this->get_sock( $key ); |
575 | if ( !$sock ) { |
576 | $this->_last_cmd_status = self::ERR_UNREACHABLE; |
577 | continue; |
578 | } |
579 | $key = is_array( $key ) ? $key[1] : $key; |
580 | $sockValue = intval( $sock ); |
581 | |
582 | if ( !isset( $sock_keys[$sockValue] ) ) { |
583 | $sock_keys[$sockValue] = array(); |
584 | $socks[] = $sock; |
585 | } |
586 | $sock_keys[$sockValue][] = $key; |
587 | } |
588 | |
589 | $gather = array(); |
590 | // Send out the requests |
591 | foreach ( $socks as $sock ) { |
592 | $cmd = 'get'; |
593 | foreach ( $sock_keys[intval( $sock )] as $key ) { |
594 | $cmd .= ' ' . $key; |
595 | } |
596 | $cmd .= "\r\n"; |
597 | |
598 | if ( $this->_fwrite( $sock, $cmd ) ) { |
599 | $gather[] = $sock; |
600 | } else { |
601 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
602 | } |
603 | } |
604 | |
605 | // Parse responses |
606 | $val = array(); |
607 | foreach ( $gather as $sock ) { |
608 | if ( !$this->_load_items( $sock, $val ) ) { |
609 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
610 | } |
611 | } |
612 | |
613 | if ( $this->_debug ) { |
614 | foreach ( $val as $k => $v ) { |
615 | $this->_debugprint( sprintf( "MemCache: got %s", $k ) ); |
616 | } |
617 | } |
618 | |
619 | return $val; |
620 | } |
621 | |
622 | // }}} |
623 | // {{{ incr() |
624 | |
625 | /** |
626 | * Increments $key (optionally) by $amt |
627 | * |
628 | * @param string $key Key to increment |
629 | * @param int $amt (optional) amount to increment |
630 | * |
631 | * @return int|null Null if the key does not exist yet (this does NOT |
632 | * create new mappings if the key does not exist). If the key does |
633 | * exist, this returns the new value for that key. |
634 | */ |
635 | public function incr( $key, $amt = 1 ) { |
636 | return $this->_incrdecr( 'incr', $key, $amt ); |
637 | } |
638 | |
639 | // }}} |
640 | // {{{ replace() |
641 | |
642 | /** |
643 | * Overwrites an existing value for key; only works if key is already set |
644 | * |
645 | * @param string $key Key to set value as |
646 | * @param mixed $value Value to store |
647 | * @param int $exp (optional) Expiration time. This can be a number of seconds |
648 | * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or |
649 | * longer must be the timestamp of the time at which the mapping should expire. It |
650 | * is safe to use timestamps in all cases, regardless of expiration |
651 | * eg: strtotime("+3 hour") |
652 | * |
653 | * @return bool |
654 | */ |
655 | public function replace( $key, $value, $exp = 0 ) { |
656 | return $this->_set( 'replace', $key, $value, $exp ); |
657 | } |
658 | |
659 | // }}} |
660 | // {{{ run_command() |
661 | |
662 | /** |
663 | * Passes through $cmd to the memcache server connected by $sock; returns |
664 | * output as an array (null array if no output) |
665 | * |
666 | * @param Resource $sock Socket to send command on |
667 | * @param string $cmd Command to run |
668 | * |
669 | * @return array Output array |
670 | */ |
671 | public function run_command( $sock, $cmd ) { |
672 | if ( !$sock ) { |
673 | return array(); |
674 | } |
675 | |
676 | if ( !$this->_fwrite( $sock, $cmd ) ) { |
677 | return array(); |
678 | } |
679 | |
680 | $ret = array(); |
681 | while ( true ) { |
682 | $res = $this->_fgets( $sock ); |
683 | $ret[] = $res; |
684 | if ( preg_match( '/^END/', $res ) ) { |
685 | break; |
686 | } |
687 | if ( strlen( $res ) == 0 ) { |
688 | break; |
689 | } |
690 | } |
691 | return $ret; |
692 | } |
693 | |
694 | // }}} |
695 | // {{{ set() |
696 | |
697 | /** |
698 | * Unconditionally sets a key to a given value in the memcache. Returns true |
699 | * if set successfully. |
700 | * |
701 | * @param string $key Key to set value as |
702 | * @param mixed $value Value to set |
703 | * @param int $exp (optional) Expiration time. This can be a number of seconds |
704 | * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or |
705 | * longer must be the timestamp of the time at which the mapping should expire. It |
706 | * is safe to use timestamps in all cases, regardless of expiration |
707 | * eg: strtotime("+3 hour") |
708 | * |
709 | * @return bool True on success |
710 | */ |
711 | public function set( $key, $value, $exp = 0 ) { |
712 | return $this->_set( 'set', $key, $value, $exp ); |
713 | } |
714 | |
715 | // }}} |
716 | // {{{ cas() |
717 | |
718 | /** |
719 | * Sets a key to a given value in the memcache if the current value still corresponds |
720 | * to a known, given value. Returns true if set successfully. |
721 | * |
722 | * @param float $casToken Current known value |
723 | * @param string $key Key to set value as |
724 | * @param mixed $value Value to set |
725 | * @param int $exp (optional) Expiration time. This can be a number of seconds |
726 | * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or |
727 | * longer must be the timestamp of the time at which the mapping should expire. It |
728 | * is safe to use timestamps in all cases, regardless of expiration |
729 | * eg: strtotime("+3 hour") |
730 | * |
731 | * @return bool True on success |
732 | */ |
733 | public function cas( $casToken, $key, $value, $exp = 0 ) { |
734 | return $this->_set( 'cas', $key, $value, $exp, $casToken ); |
735 | } |
736 | |
737 | // }}} |
738 | // {{{ set_compress_threshold() |
739 | |
740 | /** |
741 | * Set the compression threshold |
742 | * |
743 | * @param int $thresh Threshold to compress if larger than |
744 | */ |
745 | public function set_compress_threshold( $thresh ) { |
746 | $this->_compress_threshold = $thresh; |
747 | } |
748 | |
749 | // }}} |
750 | // {{{ set_debug() |
751 | |
752 | /** |
753 | * Set the debug flag |
754 | * |
755 | * @see __construct() |
756 | * @param bool $dbg True for debugging, false otherwise |
757 | */ |
758 | public function set_debug( $dbg ) { |
759 | $this->_debug = $dbg; |
760 | } |
761 | |
762 | // }}} |
763 | // {{{ set_servers() |
764 | |
765 | /** |
766 | * Set the server list to distribute key gets and puts between |
767 | * |
768 | * @see __construct() |
769 | * @param array $list Array of servers to connect to |
770 | */ |
771 | public function set_servers( $list ) { |
772 | $this->_servers = $list; |
773 | $this->_active = count( $list ); |
774 | $this->_buckets = null; |
775 | $this->_bucketcount = 0; |
776 | |
777 | $this->_single_sock = null; |
778 | if ( $this->_active == 1 ) { |
779 | $this->_single_sock = $this->_servers[0]; |
780 | } |
781 | } |
782 | |
783 | /** |
784 | * Sets the timeout for new connections |
785 | * |
786 | * @param int $seconds Number of seconds |
787 | * @param int $microseconds Number of microseconds |
788 | */ |
789 | public function set_timeout( $seconds, $microseconds ) { |
790 | $this->_timeout_seconds = $seconds; |
791 | $this->_timeout_microseconds = $microseconds; |
792 | } |
793 | |
794 | // }}} |
795 | // }}} |
796 | // {{{ private methods |
797 | // {{{ _close_sock() |
798 | |
799 | /** |
800 | * Close the specified socket |
801 | * |
802 | * @param string $sock Socket to close |
803 | * |
804 | * @access private |
805 | */ |
806 | function _close_sock( $sock ) { |
807 | $host = array_search( $sock, $this->_cache_sock ); |
808 | fclose( $this->_cache_sock[$host] ); |
809 | unset( $this->_cache_sock[$host] ); |
810 | } |
811 | |
812 | // }}} |
813 | // {{{ _connect_sock() |
814 | |
815 | /** |
816 | * Connects $sock to $host, timing out after $timeout |
817 | * |
818 | * @param int $sock Socket to connect |
819 | * @param string $host Host:IP to connect to |
820 | * |
821 | * @return bool |
822 | * @access private |
823 | */ |
824 | function _connect_sock( &$sock, $host ) { |
825 | $port = null; |
826 | $hostAndPort = IPUtils::splitHostAndPort( $host ); |
827 | if ( $hostAndPort ) { |
828 | $ip = $hostAndPort[0]; |
829 | if ( $hostAndPort[1] ) { |
830 | $port = $hostAndPort[1]; |
831 | } |
832 | } else { |
833 | $ip = $host; |
834 | } |
835 | $sock = false; |
836 | $timeout = $this->_connect_timeout; |
837 | $errno = $errstr = null; |
838 | for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) { |
839 | AtEase::suppressWarnings(); |
840 | if ( $this->_persistent == 1 ) { |
841 | $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout ); |
842 | } else { |
843 | $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout ); |
844 | } |
845 | AtEase::restoreWarnings(); |
846 | } |
847 | if ( !$sock ) { |
848 | $this->_error_log( "Error connecting to $host: $errstr" ); |
849 | $this->_dead_host( $host ); |
850 | return false; |
851 | } |
852 | |
853 | // Initialise timeout |
854 | stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds ); |
855 | |
856 | // If the connection was persistent, flush the read buffer in case there |
857 | // was a previous incomplete request on this connection |
858 | if ( $this->_persistent ) { |
859 | $this->_flush_read_buffer( $sock ); |
860 | } |
861 | return true; |
862 | } |
863 | |
864 | // }}} |
865 | // {{{ _dead_sock() |
866 | |
867 | /** |
868 | * Marks a host as dead until 30-40 seconds in the future |
869 | * |
870 | * @param string $sock Socket to mark as dead |
871 | * |
872 | * @access private |
873 | */ |
874 | function _dead_sock( $sock ) { |
875 | $host = array_search( $sock, $this->_cache_sock ); |
876 | $this->_dead_host( $host ); |
877 | } |
878 | |
879 | /** |
880 | * @param string $host |
881 | */ |
882 | function _dead_host( $host ) { |
883 | $hostAndPort = IPUtils::splitHostAndPort( $host ); |
884 | if ( $hostAndPort ) { |
885 | $ip = $hostAndPort[0]; |
886 | } else { |
887 | $ip = $host; |
888 | } |
889 | $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) ); |
890 | $this->_host_dead[$host] = $this->_host_dead[$ip]; |
891 | unset( $this->_cache_sock[$host] ); |
892 | } |
893 | |
894 | // }}} |
895 | // {{{ get_sock() |
896 | |
897 | /** |
898 | * get_sock |
899 | * |
900 | * @param string $key Key to retrieve value for; |
901 | * |
902 | * @return Resource|bool Resource on success, false on failure |
903 | * @access private |
904 | */ |
905 | function get_sock( $key ) { |
906 | if ( !$this->_active ) { |
907 | return false; |
908 | } |
909 | |
910 | if ( $this->_single_sock !== null ) { |
911 | return $this->sock_to_host( $this->_single_sock ); |
912 | } |
913 | |
914 | $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key ); |
915 | if ( $this->_buckets === null ) { |
916 | $bu = array(); |
917 | foreach ( $this->_servers as $v ) { |
918 | if ( is_array( $v ) ) { |
919 | for ( $i = 0; $i < $v[1]; $i++ ) { |
920 | $bu[] = $v[0]; |
921 | } |
922 | } else { |
923 | $bu[] = $v; |
924 | } |
925 | } |
926 | $this->_buckets = $bu; |
927 | $this->_bucketcount = count( $bu ); |
928 | } |
929 | |
930 | $realkey = is_array( $key ) ? $key[1] : $key; |
931 | for ( $tries = 0; $tries < 20; $tries++ ) { |
932 | $host = $this->_buckets[$hv % $this->_bucketcount]; |
933 | $sock = $this->sock_to_host( $host ); |
934 | if ( $sock ) { |
935 | return $sock; |
936 | } |
937 | $hv = $this->_hashfunc( $hv . $realkey ); |
938 | } |
939 | |
940 | return false; |
941 | } |
942 | |
943 | // }}} |
944 | // {{{ _hashfunc() |
945 | |
946 | /** |
947 | * Creates a hash integer based on the $key |
948 | * |
949 | * @param string $key Key to hash |
950 | * |
951 | * @return int Hash value |
952 | * @access private |
953 | */ |
954 | function _hashfunc( $key ) { |
955 | # Hash function must be in [0,0x7ffffff] |
956 | # We take the first 31 bits of the MD5 hash, which unlike the hash |
957 | # function used in a previous version of this client, works |
958 | return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; |
959 | } |
960 | |
961 | // }}} |
962 | // {{{ _incrdecr() |
963 | |
964 | /** |
965 | * Perform increment/decrement on $key |
966 | * |
967 | * @param string $cmd Command to perform |
968 | * @param string|array $key Key to perform it on |
969 | * @param int $amt Amount to adjust |
970 | * |
971 | * @return int New value of $key |
972 | * @access private |
973 | */ |
974 | function _incrdecr( $cmd, $key, $amt = 1 ) { |
975 | $this->_last_cmd_status = self::ERR_NONE; |
976 | |
977 | if ( !$this->_active ) { |
978 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
979 | |
980 | return null; |
981 | } |
982 | |
983 | $sock = $this->get_sock( $key ); |
984 | if ( !$sock ) { |
985 | $this->_last_cmd_status = self::ERR_UNREACHABLE; |
986 | |
987 | return null; |
988 | } |
989 | |
990 | $key = is_array( $key ) ? $key[1] : $key; |
991 | if ( isset( $this->stats[$cmd] ) ) { |
992 | $this->stats[$cmd]++; |
993 | } else { |
994 | $this->stats[$cmd] = 1; |
995 | } |
996 | if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { |
997 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
998 | |
999 | return null; |
1000 | } |
1001 | |
1002 | $line = $this->_fgets( $sock ); |
1003 | if ( $this->_debug ) { |
1004 | $this->_debugprint( "$cmd($key): $line" ); |
1005 | } |
1006 | |
1007 | $match = array(); |
1008 | if ( !preg_match( '/^(\d+)/', $line, $match ) ) { |
1009 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
1010 | |
1011 | return null; |
1012 | } |
1013 | |
1014 | return (int)$match[1]; |
1015 | } |
1016 | |
1017 | // }}} |
1018 | // {{{ _load_items() |
1019 | |
1020 | /** |
1021 | * Load items into $ret from $sock |
1022 | * |
1023 | * @param Resource $sock Socket to read from |
1024 | * @param array $ret returned values |
1025 | * @param float $casToken [optional] |
1026 | * @return bool True for success, false for failure |
1027 | * |
1028 | * @access private |
1029 | */ |
1030 | function _load_items( $sock, &$ret, &$casToken = null ) { |
1031 | $results = array(); |
1032 | |
1033 | while ( 1 ) { |
1034 | $decl = $this->_fgets( $sock ); |
1035 | |
1036 | if ( $decl === false ) { |
1037 | /* |
1038 | * If nothing can be read, something is wrong because we know exactly when |
1039 | * to stop reading (right after "END") and we return right after that. |
1040 | */ |
1041 | return false; |
1042 | } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)(?: (\d+))?$/', $decl, $match ) ) { |
1043 | /* |
1044 | * Read all data returned. This can be either one or multiple values. |
1045 | * Save all that data (in an array) to be processed later: we'll first |
1046 | * want to continue reading until "END" before doing anything else, |
1047 | * to make sure that we don't leave our client in a state where it's |
1048 | * output is not yet fully read. |
1049 | */ |
1050 | $results[] = array( |
1051 | $match[1], // rkey |
1052 | $match[2], // flags |
1053 | $match[3], // len |
1054 | $match[4] ?? null, // casToken (appears with "gets" but not "get") |
1055 | $this->_fread( $sock, $match[3] + 2 ), // data |
1056 | ); |
1057 | } elseif ( $decl == "END" ) { |
1058 | /** |
1059 | * All data has been read, time to process the data and build |
1060 | * meaningful return values. |
1061 | */ |
1062 | foreach ( $results as [ $rkey, $flags, /* length */, $casToken, $data ] ) { |
1063 | if ( $data === false || substr( $data, -2 ) !== "\r\n" ) { |
1064 | $this->_handle_error( $sock, |
1065 | 'line ending missing from data block from $1' ); |
1066 | return false; |
1067 | } |
1068 | $data = substr( $data, 0, -2 ); |
1069 | $ret[$rkey] = $data; |
1070 | |
1071 | if ( $this->_have_zlib && $flags & self::COMPRESSED ) { |
1072 | $ret[$rkey] = gzuncompress( $ret[$rkey] ); |
1073 | } |
1074 | |
1075 | /* |
1076 | * This unserialize is the exact reason that we only want to |
1077 | * process data after having read until "END" (instead of doing |
1078 | * this right away): "unserialize" can trigger outside code: |
1079 | * in the event that $ret[$rkey] is a serialized object, |
1080 | * unserializing it will trigger __wakeup() if present. If that |
1081 | * function attempted to read from memcached (while we did not |
1082 | * yet read "END"), these 2 calls would collide. |
1083 | */ |
1084 | if ( $flags & self::SERIALIZED ) { |
1085 | $ret[$rkey] = $this->unserialize( $ret[$rkey] ); |
1086 | } elseif ( $flags & self::INTVAL ) { |
1087 | $ret[$rkey] = intval( $ret[$rkey] ); |
1088 | } |
1089 | } |
1090 | |
1091 | return true; |
1092 | } else { |
1093 | $this->_handle_error( $sock, 'Error parsing response from $1' ); |
1094 | return false; |
1095 | } |
1096 | } |
1097 | } |
1098 | |
1099 | // }}} |
1100 | // {{{ _set() |
1101 | |
1102 | /** |
1103 | * Performs the requested storage operation to the memcache server |
1104 | * |
1105 | * @param string $cmd Command to perform |
1106 | * @param string $key Key to act on |
1107 | * @param mixed $val What we need to store |
1108 | * @param int $exp (optional) Expiration time. This can be a number of seconds |
1109 | * to cache for (up to 30 days inclusive). Any timespans of 30 days + 1 second or |
1110 | * longer must be the timestamp of the time at which the mapping should expire. It |
1111 | * is safe to use timestamps in all cases, regardless of expiration |
1112 | * eg: strtotime("+3 hour") |
1113 | * @param float $casToken [optional] |
1114 | * |
1115 | * @return bool |
1116 | * @access private |
1117 | */ |
1118 | function _set( $cmd, $key, $val, $exp, $casToken = null ) { |
1119 | $this->_last_cmd_status = self::ERR_NONE; |
1120 | |
1121 | if ( !$this->_active ) { |
1122 | $this->_last_cmd_status = self::ERR_UNEXPECTED; |
1123 | |
1124 | return false; |
1125 | } |
1126 | |
1127 | $sock = $this->get_sock( $key ); |
1128 | if ( !$sock ) { |
1129 | $this->_last_cmd_status = self::ERR_UNREACHABLE; |
1130 | |
1131 | return false; |
1132 | } |
1133 | |
1134 | if ( isset( $this->stats[$cmd] ) ) { |
1135 | $this->stats[$cmd]++; |
1136 | } else { |
1137 | $this->stats[$cmd] = 1; |
1138 | } |
1139 | |
1140 | $flags = 0; |
1141 | |
1142 | if ( is_int( $val ) ) { |
1143 | $flags |= self::INTVAL; |
1144 | } elseif ( !is_scalar( $val ) ) { |
1145 | $val = $this->serialize( $val ); |
1146 | $flags |= self::SERIALIZED; |
1147 | if ( $this->_debug ) { |
1148 | $this->_debugprint( "client: serializing data as it is not scalar" ); |
1149 | } |
1150 | } |
1151 | |
1152 | $len = strlen( $val ); |
1153 | |
1154 | if ( $this->_have_zlib && $this->_compress_enable |
1155 | && $this->_compress_threshold && $len >= $this->_compress_threshold |
1156 | ) { |
1157 | $c_val = gzcompress( $val, 9 ); |
1158 | $c_len = strlen( $c_val ); |
1159 | |
1160 | if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) { |
1161 | if ( $this->_debug ) { |
1162 | $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) ); |
1163 | } |
1164 | $val = $c_val; |
1165 | $len = $c_len; |
1166 | $flags |= self::COMPRESSED; |
1167 | } |
1168 | } |
1169 | |
1170 | $command = "$cmd $key $flags $exp $len"; |
1171 | if ( $casToken ) { |
1172 | $command .= " $casToken"; |
1173 | } |
1174 | |
1175 | if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) { |
1176 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
1177 | |
1178 | return false; |
1179 | } |
1180 | |
1181 | $line = $this->_fgets( $sock ); |
1182 | if ( $this->_debug ) { |
1183 | $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) ); |
1184 | } |
1185 | |
1186 | if ( $line === "STORED" ) { |
1187 | return true; |
1188 | } elseif ( $line === "NOT_STORED" && $cmd === "set" ) { |
1189 | // "Not stored" is always used as the mcrouter response with AllAsyncRoute |
1190 | return true; |
1191 | } |
1192 | |
1193 | if ( $line === false ) { |
1194 | $this->_last_cmd_status = self::ERR_NO_RESPONSE; |
1195 | } |
1196 | |
1197 | return false; |
1198 | } |
1199 | |
1200 | // }}} |
1201 | // {{{ sock_to_host() |
1202 | |
1203 | /** |
1204 | * Returns the socket for the host |
1205 | * |
1206 | * @param string $host Host:IP to get socket for |
1207 | * |
1208 | * @return Resource|bool IO Stream or false |
1209 | * @access private |
1210 | */ |
1211 | function sock_to_host( $host ) { |
1212 | if ( isset( $this->_cache_sock[$host] ) ) { |
1213 | return $this->_cache_sock[$host]; |
1214 | } |
1215 | |
1216 | $sock = null; |
1217 | $now = time(); |
1218 | $hostAndPort = IPUtils::splitHostAndPort( $host ); |
1219 | if ( $hostAndPort ) { |
1220 | $ip = $hostAndPort[0]; |
1221 | } else { |
1222 | $ip = $host; |
1223 | } |
1224 | if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now || |
1225 | isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now |
1226 | ) { |
1227 | return null; |
1228 | } |
1229 | |
1230 | if ( !$this->_connect_sock( $sock, $host ) ) { |
1231 | return null; |
1232 | } |
1233 | |
1234 | // Do not buffer writes |
1235 | stream_set_write_buffer( $sock, 0 ); |
1236 | |
1237 | $this->_cache_sock[$host] = $sock; |
1238 | |
1239 | return $this->_cache_sock[$host]; |
1240 | } |
1241 | |
1242 | /** |
1243 | * @param string $text |
1244 | */ |
1245 | function _debugprint( $text ) { |
1246 | $this->_logger->debug( $text ); |
1247 | } |
1248 | |
1249 | /** |
1250 | * @param string $text |
1251 | */ |
1252 | function _error_log( $text ) { |
1253 | $this->_logger->error( "Memcached error: $text" ); |
1254 | } |
1255 | |
1256 | /** |
1257 | * Write to a stream. If there is an error, mark the socket dead. |
1258 | * |
1259 | * @param Resource $sock The socket |
1260 | * @param string $buf The string to write |
1261 | * @return bool True on success, false on failure |
1262 | */ |
1263 | function _fwrite( $sock, $buf ) { |
1264 | $bytesWritten = 0; |
1265 | $bufSize = strlen( $buf ); |
1266 | while ( $bytesWritten < $bufSize ) { |
1267 | $result = fwrite( $sock, $buf ); |
1268 | $data = stream_get_meta_data( $sock ); |
1269 | if ( $data['timed_out'] ) { |
1270 | $this->_handle_error( $sock, 'timeout writing to $1' ); |
1271 | return false; |
1272 | } |
1273 | // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3. |
1274 | if ( $result === false || $result === 0 ) { |
1275 | $this->_handle_error( $sock, 'error writing to $1' ); |
1276 | return false; |
1277 | } |
1278 | $bytesWritten += $result; |
1279 | } |
1280 | |
1281 | return true; |
1282 | } |
1283 | |
1284 | /** |
1285 | * Handle an I/O error. Mark the socket dead and log an error. |
1286 | * |
1287 | * @param Resource $sock |
1288 | * @param string $msg |
1289 | */ |
1290 | function _handle_error( $sock, $msg ) { |
1291 | $peer = stream_socket_get_name( $sock, true /** remote **/ ); |
1292 | if ( strval( $peer ) === '' ) { |
1293 | $peer = array_search( $sock, $this->_cache_sock ); |
1294 | if ( $peer === false ) { |
1295 | $peer = '[unknown host]'; |
1296 | } |
1297 | } |
1298 | $msg = str_replace( '$1', $peer, $msg ); |
1299 | $this->_error_log( "$msg" ); |
1300 | $this->_dead_sock( $sock ); |
1301 | } |
1302 | |
1303 | /** |
1304 | * Read the specified number of bytes from a stream. If there is an error, |
1305 | * mark the socket dead. |
1306 | * |
1307 | * @param Resource $sock The socket |
1308 | * @param int $len The number of bytes to read |
1309 | * @return string|bool The string on success, false on failure. |
1310 | */ |
1311 | function _fread( $sock, $len ) { |
1312 | $buf = ''; |
1313 | while ( $len > 0 ) { |
1314 | $result = fread( $sock, $len ); |
1315 | $data = stream_get_meta_data( $sock ); |
1316 | if ( $data['timed_out'] ) { |
1317 | $this->_handle_error( $sock, 'timeout reading from $1' ); |
1318 | return false; |
1319 | } |
1320 | if ( $result === false ) { |
1321 | $this->_handle_error( $sock, 'error reading buffer from $1' ); |
1322 | return false; |
1323 | } |
1324 | if ( $result === '' ) { |
1325 | // This will happen if the remote end of the socket is shut down |
1326 | $this->_handle_error( $sock, 'unexpected end of file reading from $1' ); |
1327 | return false; |
1328 | } |
1329 | $len -= strlen( $result ); |
1330 | $buf .= $result; |
1331 | } |
1332 | return $buf; |
1333 | } |
1334 | |
1335 | /** |
1336 | * Read a line from a stream. If there is an error, mark the socket dead. |
1337 | * The \r\n line ending is stripped from the response. |
1338 | * |
1339 | * @param Resource $sock The socket |
1340 | * @return string|bool The string on success, false on failure |
1341 | */ |
1342 | function _fgets( $sock ) { |
1343 | $result = fgets( $sock ); |
1344 | // fgets() may return a partial line if there is a select timeout after |
1345 | // a successful recv(), so we have to check for a timeout even if we |
1346 | // got a string response. |
1347 | $data = stream_get_meta_data( $sock ); |
1348 | if ( $data['timed_out'] ) { |
1349 | $this->_handle_error( $sock, 'timeout reading line from $1' ); |
1350 | return false; |
1351 | } |
1352 | if ( $result === false ) { |
1353 | $this->_handle_error( $sock, 'error reading line from $1' ); |
1354 | return false; |
1355 | } |
1356 | if ( substr( $result, -2 ) === "\r\n" ) { |
1357 | $result = substr( $result, 0, -2 ); |
1358 | } elseif ( substr( $result, -1 ) === "\n" ) { |
1359 | $result = substr( $result, 0, -1 ); |
1360 | } else { |
1361 | $this->_handle_error( $sock, 'line ending missing in response from $1' ); |
1362 | return false; |
1363 | } |
1364 | return $result; |
1365 | } |
1366 | |
1367 | /** |
1368 | * Flush the read buffer of a stream |
1369 | * @param Resource $f |
1370 | */ |
1371 | function _flush_read_buffer( $f ) { |
1372 | if ( !$f ) { |
1373 | return; |
1374 | } |
1375 | $r = array( $f ); |
1376 | $w = null; |
1377 | $e = null; |
1378 | $n = stream_select( $r, $w, $e, 0, 0 ); |
1379 | while ( $n == 1 && !feof( $f ) ) { |
1380 | fread( $f, 1024 ); |
1381 | $r = array( $f ); |
1382 | $w = null; |
1383 | $e = null; |
1384 | $n = stream_select( $r, $w, $e, 0, 0 ); |
1385 | } |
1386 | } |
1387 | |
1388 | // }}} |
1389 | // }}} |
1390 | // }}} |
1391 | } |
1392 | |
1393 | // }}} |