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