]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/memcached-client.php
MediaWiki 1.16.0
[autoinstalls/mediawiki.git] / includes / memcached-client.php
1 <?php
2 //
3 // +---------------------------------------------------------------------------+
4 // | memcached client, PHP                                                     |
5 // +---------------------------------------------------------------------------+
6 // | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
7 // | All rights reserved.                                                      |
8 // |                                                                           |
9 // | Redistribution and use in source and binary forms, with or without        |
10 // | modification, are permitted provided that the following conditions        |
11 // | are met:                                                                  |
12 // |                                                                           |
13 // | 1. Redistributions of source code must retain the above copyright         |
14 // |    notice, this list of conditions and the following disclaimer.          |
15 // | 2. Redistributions in binary form must reproduce the above copyright      |
16 // |    notice, this list of conditions and the following disclaimer in the    |
17 // |    documentation and/or other materials provided with the distribution.   |
18 // |                                                                           |
19 // | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
20 // | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
21 // | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
22 // | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
23 // | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
24 // | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
25 // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
26 // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
27 // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
28 // | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
29 // +---------------------------------------------------------------------------+
30 // | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
31 // | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
32 // |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
33 // |   client logic under 2-clause BSD license.                                |
34 // +---------------------------------------------------------------------------+
35 //
36 // $TCAnet$
37 //
38
39 /**
40  * This is the PHP client for memcached - a distributed memory cache daemon.
41  * More information is available at http://www.danga.com/memcached/
42  *
43  * Usage example:
44  *
45  * require_once 'memcached.php';
46  *
47  * $mc = new MWMemcached(array(
48  *              'servers' => array('127.0.0.1:10000',
49  *                                 array('192.0.0.1:10010', 2),
50  *                                 '127.0.0.1:10020'),
51  *              'debug'   => false,
52  *              'compress_threshold' => 10240,
53  *              'persistant' => true));
54  *
55  * $mc->add('key', array('some', 'array'));
56  * $mc->replace('key', 'some random string');
57  * $val = $mc->get('key');
58  *
59  * @author  Ryan T. Dean <rtdean@cytherianage.net>
60  * @version 0.1.2
61  */
62
63 // {{{ requirements
64 // }}}
65
66 // {{{ class MWMemcached
67 /**
68  * memcached client class implemented using (p)fsockopen()
69  *
70  * @author  Ryan T. Dean <rtdean@cytherianage.net>
71  * @ingroup Cache
72  */
73 class MWMemcached {
74         // {{{ properties
75         // {{{ public
76
77         // {{{ constants
78         // {{{ flags
79
80         /**
81          * Flag: indicates data is serialized
82          */
83         const SERIALIZED = 1;
84
85         /**
86          * Flag: indicates data is compressed
87          */
88         const COMPRESSED = 2;
89
90         // }}}
91
92         /**
93          * Minimum savings to store data compressed
94          */
95         const COMPRESSION_SAVINGS = 0.20;
96
97         // }}}
98
99
100         /**
101          * Command statistics
102          *
103          * @var     array
104          * @access  public
105          */
106         var $stats;
107
108         // }}}
109         // {{{ private
110
111         /**
112          * Cached Sockets that are connected
113          *
114          * @var     array
115          * @access  private
116          */
117         var $_cache_sock;
118
119         /**
120          * Current debug status; 0 - none to 9 - profiling
121          *
122          * @var     boolean
123          * @access  private
124          */
125         var $_debug;
126
127         /**
128          * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
129          *
130          * @var     array
131          * @access  private
132          */
133         var $_host_dead;
134
135         /**
136          * Is compression available?
137          *
138          * @var     boolean
139          * @access  private
140          */
141         var $_have_zlib;
142
143         /**
144          * Do we want to use compression?
145          *
146          * @var     boolean
147          * @access  private
148          */
149         var $_compress_enable;
150
151         /**
152          * At how many bytes should we compress?
153          *
154          * @var     integer
155          * @access  private
156          */
157         var $_compress_threshold;
158
159         /**
160          * Are we using persistant links?
161          *
162          * @var     boolean
163          * @access  private
164          */
165         var $_persistant;
166
167         /**
168          * If only using one server; contains ip:port to connect to
169          *
170          * @var     string
171          * @access  private
172          */
173         var $_single_sock;
174
175         /**
176          * Array containing ip:port or array(ip:port, weight)
177          *
178          * @var     array
179          * @access  private
180          */
181         var $_servers;
182
183         /**
184          * Our bit buckets
185          *
186          * @var     array
187          * @access  private
188          */
189         var $_buckets;
190
191         /**
192          * Total # of bit buckets we have
193          *
194          * @var     integer
195          * @access  private
196          */
197         var $_bucketcount;
198
199         /**
200          * # of total servers we have
201          *
202          * @var     integer
203          * @access  private
204          */
205         var $_active;
206
207         /**
208          * Stream timeout in seconds. Applies for example to fread()
209          *
210          * @var     integer
211          * @access  private
212          */
213         var $_timeout_seconds;
214
215         /**
216          * Stream timeout in microseconds
217          *
218          * @var     integer
219          * @access  private
220          */
221         var $_timeout_microseconds;
222
223         /**
224          * Connect timeout in seconds
225          */
226         var $_connect_timeout;
227
228         /**
229          * Number of connection attempts for each server
230          */
231         var $_connect_attempts;
232
233         // }}}
234         // }}}
235         // {{{ methods
236         // {{{ public functions
237         // {{{ memcached()
238
239         /**
240          * Memcache initializer
241          *
242          * @param   array    $args    Associative array of settings
243          *
244          * @return  mixed
245          */
246         public function __construct( $args ) {
247                 global $wgMemCachedTimeout;
248                 $this->set_servers( @$args['servers'] );
249                 $this->_debug = @$args['debug'];
250                 $this->stats = array();
251                 $this->_compress_threshold = @$args['compress_threshold'];
252                 $this->_persistant = array_key_exists( 'persistant', $args ) ? ( @$args['persistant'] ) : false;
253                 $this->_compress_enable = true;
254                 $this->_have_zlib = function_exists( 'gzcompress' );
255
256                 $this->_cache_sock = array();
257                 $this->_host_dead = array();
258
259                 $this->_timeout_seconds = 0;
260                 $this->_timeout_microseconds = $wgMemCachedTimeout;
261
262                 $this->_connect_timeout = 0.01;
263                 $this->_connect_attempts = 2;
264         }
265
266         // }}}
267         // {{{ add()
268
269         /**
270          * Adds a key/value to the memcache server if one isn't already set with
271          * that key
272          *
273          * @param   string  $key     Key to set with data
274          * @param   mixed   $val     Value to store
275          * @param   integer $exp     (optional) Time to expire data at
276          *
277          * @return  boolean
278          */
279         public function add( $key, $val, $exp = 0 ) {
280                 return $this->_set( 'add', $key, $val, $exp );
281         }
282
283         // }}}
284         // {{{ decr()
285
286         /**
287          * Decriment a value stored on the memcache server
288          *
289          * @param   string   $key     Key to decriment
290          * @param   integer  $amt     (optional) Amount to decriment
291          *
292          * @return  mixed    FALSE on failure, value on success
293          */
294         public function decr( $key, $amt = 1 ) {
295                 return $this->_incrdecr( 'decr', $key, $amt );
296         }
297
298         // }}}
299         // {{{ delete()
300
301         /**
302          * Deletes a key from the server, optionally after $time
303          *
304          * @param   string   $key     Key to delete
305          * @param   integer  $time    (optional) How long to wait before deleting
306          *
307          * @return  boolean  TRUE on success, FALSE on failure
308          */
309         public function delete( $key, $time = 0 ) {
310                 if ( !$this->_active ) {
311                         return false;
312                 }
313
314                 $sock = $this->get_sock( $key );
315                 if ( !is_resource( $sock ) ) {
316                         return false;
317                 }
318
319                 $key = is_array( $key ) ? $key[1] : $key;
320
321                 @$this->stats['delete']++;
322                 $cmd = "delete $key $time\r\n";
323                 if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
324                         $this->_dead_sock( $sock );
325                         return false;
326                 }
327                 $res = trim( fgets( $sock ) );
328
329                 if ( $this->_debug ) {
330                         $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) );
331                 }
332
333                 if ( $res == "DELETED" ) {
334                         return true;
335                 }
336                 return false;
337         }
338
339         // }}}
340         // {{{ disconnect_all()
341
342         /**
343          * Disconnects all connected sockets
344          */
345         public function disconnect_all() {
346                 foreach ( $this->_cache_sock as $sock ) {
347                         fclose( $sock );
348                 }
349
350                 $this->_cache_sock = array();
351         }
352
353         // }}}
354         // {{{ enable_compress()
355
356         /**
357          * Enable / Disable compression
358          *
359          * @param   boolean  $enable  TRUE to enable, FALSE to disable
360          */
361         public function enable_compress( $enable ) {
362                 $this->_compress_enable = $enable;
363         }
364
365         // }}}
366         // {{{ forget_dead_hosts()
367
368         /**
369          * Forget about all of the dead hosts
370          */
371         public function forget_dead_hosts() {
372                 $this->_host_dead = array();
373         }
374
375         // }}}
376         // {{{ get()
377
378         /**
379          * Retrieves the value associated with the key from the memcache server
380          *
381          * @param  string   $key     Key to retrieve
382          *
383          * @return  mixed
384          */
385         public function get( $key ) {
386                 wfProfileIn( __METHOD__ );
387
388                 if ( $this->_debug ) {
389                         $this->_debugprint( "get($key)\n" );
390                 }
391
392                 if ( !$this->_active ) {
393                         wfProfileOut( __METHOD__ );
394                         return false;
395                 }
396
397                 $sock = $this->get_sock( $key );
398
399                 if ( !is_resource( $sock ) ) {
400                         wfProfileOut( __METHOD__ );
401                         return false;
402                 }
403
404                 @$this->stats['get']++;
405
406                 $cmd = "get $key\r\n";
407                 if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
408                         $this->_dead_sock( $sock );
409                         wfProfileOut( __METHOD__ );
410                         return false;
411                 }
412
413                 $val = array();
414                 $this->_load_items( $sock, $val );
415
416                 if ( $this->_debug ) {
417                         foreach ( $val as $k => $v ) {
418                                 $this->_debugprint( sprintf( "MemCache: sock %s got %s\n", serialize( $sock ), $k ) );
419                         }
420                 }
421
422                 wfProfileOut( __METHOD__ );
423                 return @$val[$key];
424         }
425
426         // }}}
427         // {{{ get_multi()
428
429         /**
430          * Get multiple keys from the server(s)
431          *
432          * @param   array    $keys    Keys to retrieve
433          *
434          * @return  array
435          */
436         public function get_multi( $keys ) {
437                 if ( !$this->_active ) {
438                         return false;
439                 }
440
441                 @$this->stats['get_multi']++;
442                 $sock_keys = array();
443
444                 foreach ( $keys as $key ) {
445                         $sock = $this->get_sock( $key );
446                         if ( !is_resource( $sock ) ) {
447                                 continue;
448                         }
449                         $key = is_array( $key ) ? $key[1] : $key;
450                         if ( !isset( $sock_keys[$sock] ) ) {
451                                 $sock_keys[$sock] = array();
452                                 $socks[] = $sock;
453                         }
454                         $sock_keys[$sock][] = $key;
455                 }
456
457                 // Send out the requests
458                 foreach ( $socks as $sock ) {
459                         $cmd = 'get';
460                         foreach ( $sock_keys[$sock] as $key ) {
461                                 $cmd .= ' ' . $key;
462                         }
463                         $cmd .= "\r\n";
464
465                         if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
466                                 $gather[] = $sock;
467                         } else {
468                                 $this->_dead_sock( $sock );
469                         }
470                 }
471
472                 // Parse responses
473                 $val = array();
474                 foreach ( $gather as $sock ) {
475                         $this->_load_items( $sock, $val );
476                 }
477
478                 if ( $this->_debug ) {
479                         foreach ( $val as $k => $v ) {
480                                 $this->_debugprint( sprintf( "MemCache: got %s\n", $k ) );
481                         }
482                 }
483
484                 return $val;
485         }
486
487         // }}}
488         // {{{ incr()
489
490         /**
491          * Increments $key (optionally) by $amt
492          *
493          * @param   string   $key     Key to increment
494          * @param   integer  $amt     (optional) amount to increment
495          *
496          * @return  integer  New key value?
497          */
498         public function incr( $key, $amt = 1 ) {
499                 return $this->_incrdecr( 'incr', $key, $amt );
500         }
501
502         // }}}
503         // {{{ replace()
504
505         /**
506          * Overwrites an existing value for key; only works if key is already set
507          *
508          * @param   string   $key     Key to set value as
509          * @param   mixed    $value   Value to store
510          * @param   integer  $exp     (optional) Experiation time
511          *
512          * @return  boolean
513          */
514         public function replace( $key, $value, $exp = 0 ) {
515                 return $this->_set( 'replace', $key, $value, $exp );
516         }
517
518         // }}}
519         // {{{ run_command()
520
521         /**
522          * Passes through $cmd to the memcache server connected by $sock; returns
523          * output as an array (null array if no output)
524          *
525          * NOTE: due to a possible bug in how PHP reads while using fgets(), each
526          *       line may not be terminated by a \r\n.  More specifically, my testing
527          *       has shown that, on FreeBSD at least, each line is terminated only
528          *       with a \n.  This is with the PHP flag auto_detect_line_endings set
529          *       to falase (the default).
530          *
531          * @param   resource $sock    Socket to send command on
532          * @param   string   $cmd     Command to run
533          *
534          * @return  array    Output array
535          * @access  public
536          */
537         function run_command( $sock, $cmd ) {
538                 if ( !is_resource( $sock ) ) {
539                         return array();
540                 }
541
542                 if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
543                         return array();
544                 }
545
546                 while ( true ) {
547                         $res = fgets( $sock );
548                         $ret[] = $res;
549                         if ( preg_match( '/^END/', $res ) ) {
550                                 break;
551                         }
552                         if ( strlen( $res ) == 0 ) {
553                                 break;
554                         }
555                 }
556                 return $ret;
557         }
558
559         // }}}
560         // {{{ set()
561
562         /**
563          * Unconditionally sets a key to a given value in the memcache.  Returns true
564          * if set successfully.
565          *
566          * @param   string   $key     Key to set value as
567          * @param   mixed    $value   Value to set
568          * @param   integer  $exp     (optional) Experiation time
569          *
570          * @return  boolean  TRUE on success
571          */
572         public function set( $key, $value, $exp = 0 ) {
573                 return $this->_set( 'set', $key, $value, $exp );
574         }
575
576         // }}}
577         // {{{ set_compress_threshold()
578
579         /**
580          * Sets the compression threshold
581          *
582          * @param   integer  $thresh  Threshold to compress if larger than
583          */
584         public function set_compress_threshold( $thresh ) {
585                 $this->_compress_threshold = $thresh;
586         }
587
588         // }}}
589         // {{{ set_debug()
590
591         /**
592          * Sets the debug flag
593          *
594          * @param   boolean  $dbg     TRUE for debugging, FALSE otherwise
595          *
596          * @see     MWMemcached::__construct
597          */
598         public function set_debug( $dbg ) {
599                 $this->_debug = $dbg;
600         }
601
602         // }}}
603         // {{{ set_servers()
604
605         /**
606          * Sets the server list to distribute key gets and puts between
607          *
608          * @param   array    $list    Array of servers to connect to
609          *
610          * @see     MWMemcached::__construct()
611          */
612         public function set_servers( $list ) {
613                 $this->_servers = $list;
614                 $this->_active = count( $list );
615                 $this->_buckets = null;
616                 $this->_bucketcount = 0;
617
618                 $this->_single_sock = null;
619                 if ( $this->_active == 1 ) {
620                         $this->_single_sock = $this->_servers[0];
621                 }
622         }
623
624         /**
625          * Sets the timeout for new connections
626          *
627          * @param   integer  $seconds Number of seconds
628          * @param   integer  $microseconds  Number of microseconds
629          */
630         public function set_timeout( $seconds, $microseconds ) {
631                 $this->_timeout_seconds = $seconds;
632                 $this->_timeout_microseconds = $microseconds;
633         }
634
635         // }}}
636         // }}}
637         // {{{ private methods
638         // {{{ _close_sock()
639
640         /**
641          * Close the specified socket
642          *
643          * @param   string   $sock    Socket to close
644          *
645          * @access  private
646          */
647         function _close_sock( $sock ) {
648                 $host = array_search( $sock, $this->_cache_sock );
649                 fclose( $this->_cache_sock[$host] );
650                 unset( $this->_cache_sock[$host] );
651         }
652
653         // }}}
654         // {{{ _connect_sock()
655
656         /**
657          * Connects $sock to $host, timing out after $timeout
658          *
659          * @param   integer  $sock    Socket to connect
660          * @param   string   $host    Host:IP to connect to
661          *
662          * @return  boolean
663          * @access  private
664          */
665         function _connect_sock( &$sock, $host ) {
666                 list( $ip, $port ) = explode( ':', $host );
667                 $sock = false;
668                 $timeout = $this->_connect_timeout;
669                 $errno = $errstr = null;
670                 for( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
671                         if ( $this->_persistant == 1 ) {
672                                 $sock = @pfsockopen( $ip, $port, $errno, $errstr, $timeout );
673                         } else {
674                                 $sock = @fsockopen( $ip, $port, $errno, $errstr, $timeout );
675                         }
676                 }
677                 if ( !$sock ) {
678                         if ( $this->_debug ) {
679                                 $this->_debugprint( "Error connecting to $host: $errstr\n" );
680                         }
681                         return false;
682                 }
683
684                 // Initialise timeout
685                 stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
686
687                 return true;
688         }
689
690         // }}}
691         // {{{ _dead_sock()
692
693         /**
694          * Marks a host as dead until 30-40 seconds in the future
695          *
696          * @param   string   $sock    Socket to mark as dead
697          *
698          * @access  private
699          */
700         function _dead_sock( $sock ) {
701                 $host = array_search( $sock, $this->_cache_sock );
702                 $this->_dead_host( $host );
703         }
704
705         function _dead_host( $host ) {
706                 @list( $ip, /* $port */) = explode( ':', $host );
707                 $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
708                 $this->_host_dead[$host] = $this->_host_dead[$ip];
709                 unset( $this->_cache_sock[$host] );
710         }
711
712         // }}}
713         // {{{ get_sock()
714
715         /**
716          * get_sock
717          *
718          * @param   string   $key     Key to retrieve value for;
719          *
720          * @return  mixed    resource on success, false on failure
721          * @access  private
722          */
723         function get_sock( $key ) {
724                 if ( !$this->_active ) {
725                         return false;
726                 }
727
728                 if ( $this->_single_sock !== null ) {
729                         $this->_flush_read_buffer( $this->_single_sock );
730                         return $this->sock_to_host( $this->_single_sock );
731                 }
732
733                 $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
734
735                 if ( $this->_buckets === null ) {
736                         foreach ( $this->_servers as $v ) {
737                                 if ( is_array( $v ) ) {
738                                         for( $i = 0; $i < $v[1]; $i++ ) {
739                                                 $bu[] = $v[0];
740                                         }
741                                 } else {
742                                         $bu[] = $v;
743                                 }
744                         }
745                         $this->_buckets = $bu;
746                         $this->_bucketcount = count( $bu );
747                 }
748
749                 $realkey = is_array( $key ) ? $key[1] : $key;
750                 for( $tries = 0; $tries < 20; $tries++ ) {
751                         $host = $this->_buckets[$hv % $this->_bucketcount];
752                         $sock = $this->sock_to_host( $host );
753                         if ( is_resource( $sock ) ) {
754                                 $this->_flush_read_buffer( $sock );
755                                 return $sock;
756                         }
757                         $hv = $this->_hashfunc( $hv . $realkey );
758                 }
759
760                 return false;
761         }
762
763         // }}}
764         // {{{ _hashfunc()
765
766         /**
767          * Creates a hash integer       based on the $key
768          *
769          * @param   string   $key     Key to hash
770          *
771          * @return  integer  Hash value
772          * @access  private
773          */
774         function _hashfunc( $key ) {
775                 # Hash function must on [0,0x7ffffff]
776                 # We take the first 31 bits of the MD5 hash, which unlike the hash
777                 # function used in a previous version of this client, works
778                 return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
779         }
780
781         // }}}
782         // {{{ _incrdecr()
783
784         /**
785          * Perform increment/decriment on $key
786          *
787          * @param   string   $cmd     Command to perform
788          * @param   string   $key     Key to perform it on
789          * @param   integer  $amt     Amount to adjust
790          *
791          * @return  integer     New value of $key
792          * @access  private
793          */
794         function _incrdecr( $cmd, $key, $amt = 1 ) {
795                 if ( !$this->_active ) {
796                         return null;
797                 }
798
799                 $sock = $this->get_sock( $key );
800                 if ( !is_resource( $sock ) ) {
801                         return null;
802                 }
803
804                 $key = is_array( $key ) ? $key[1] : $key;
805                 @$this->stats[$cmd]++;
806                 if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
807                         return $this->_dead_sock( $sock );
808                 }
809
810                 $line = fgets( $sock );
811                 $match = array();
812                 if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
813                         return null;
814                 }
815                 return $match[1];
816         }
817
818         // }}}
819         // {{{ _load_items()
820
821         /**
822          * Load items into $ret from $sock
823          *
824          * @param   resource $sock    Socket to read from
825          * @param   array    $ret     Returned values
826          *
827          * @access  private
828          */
829         function _load_items( $sock, &$ret ) {
830                 while ( 1 ) {
831                         $decl = fgets( $sock );
832                         if ( $decl == "END\r\n" ) {
833                                 return true;
834                         } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
835                                 list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
836                                 $bneed = $len + 2;
837                                 $offset = 0;
838
839                                 while ( $bneed > 0 ) {
840                                         $data = fread( $sock, $bneed );
841                                         $n = strlen( $data );
842                                         if ( $n == 0 ) {
843                                                 break;
844                                         }
845                                         $offset += $n;
846                                         $bneed -= $n;
847                                         @$ret[$rkey] .= $data;
848                                 }
849
850                                 if ( $offset != $len + 2 ) {
851                                         // Something is borked!
852                                         if ( $this->_debug ) {
853                                                 $this->_debugprint( sprintf( "Something is borked!  key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) );
854                                         }
855
856                                         unset( $ret[$rkey] );
857                                         $this->_close_sock( $sock );
858                                         return false;
859                                 }
860
861                                 if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
862                                         $ret[$rkey] = gzuncompress( $ret[$rkey] );
863                                 }
864
865                                 $ret[$rkey] = rtrim( $ret[$rkey] );
866
867                                 if ( $flags & self::SERIALIZED ) {
868                                         $ret[$rkey] = unserialize( $ret[$rkey] );
869                                 }
870
871                         } else {
872                                 $this->_debugprint( "Error parsing memcached response\n" );
873                                 return 0;
874                         }
875                 }
876         }
877
878         // }}}
879         // {{{ _set()
880
881         /**
882          * Performs the requested storage operation to the memcache server
883          *
884          * @param   string   $cmd     Command to perform
885          * @param   string   $key     Key to act on
886          * @param   mixed    $val     What we need to store
887          * @param   integer  $exp     When it should expire
888          *
889          * @return  boolean
890          * @access  private
891          */
892         function _set( $cmd, $key, $val, $exp ) {
893                 if ( !$this->_active ) {
894                         return false;
895                 }
896
897                 $sock = $this->get_sock( $key );
898                 if ( !is_resource( $sock ) ) {
899                         return false;
900                 }
901
902                 @$this->stats[$cmd]++;
903
904                 $flags = 0;
905
906                 if ( !is_scalar( $val ) ) {
907                         $val = serialize( $val );
908                         $flags |= self::SERIALIZED;
909                         if ( $this->_debug ) {
910                                 $this->_debugprint( sprintf( "client: serializing data as it is not scalar\n" ) );
911                         }
912                 }
913
914                 $len = strlen( $val );
915
916                 if ( $this->_have_zlib && $this->_compress_enable &&
917                          $this->_compress_threshold && $len >= $this->_compress_threshold )
918                 {
919                         $c_val = gzcompress( $val, 9 );
920                         $c_len = strlen( $c_val );
921
922                         if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
923                                 if ( $this->_debug ) {
924                                         $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes\n", $len, $c_len ) );
925                                 }
926                                 $val = $c_val;
927                                 $len = $c_len;
928                                 $flags |= self::COMPRESSED;
929                         }
930                 }
931                 if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
932                         return $this->_dead_sock( $sock );
933                 }
934
935                 $line = trim( fgets( $sock ) );
936
937                 if ( $this->_debug ) {
938                         $this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) );
939                 }
940                 if ( $line == "STORED" ) {
941                         return true;
942                 }
943                 return false;
944         }
945
946         // }}}
947         // {{{ sock_to_host()
948
949         /**
950          * Returns the socket for the host
951          *
952          * @param   string   $host    Host:IP to get socket for
953          *
954          * @return  mixed    IO Stream or false
955          * @access  private
956          */
957         function sock_to_host( $host ) {
958                 if ( isset( $this->_cache_sock[$host] ) ) {
959                         return $this->_cache_sock[$host];
960                 }
961
962                 $sock = null;
963                 $now = time();
964                 list( $ip, /* $port */) = explode( ':', $host );
965                 if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
966                         isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
967                 ) {
968                         return null;
969                 }
970
971                 if ( !$this->_connect_sock( $sock, $host ) ) {
972                         return $this->_dead_host( $host );
973                 }
974
975                 // Do not buffer writes
976                 stream_set_write_buffer( $sock, 0 );
977
978                 $this->_cache_sock[$host] = $sock;
979
980                 return $this->_cache_sock[$host];
981         }
982
983         function _debugprint( $str ) {
984                 print( $str );
985         }
986
987         /**
988          * Write to a stream, timing out after the correct amount of time
989          *
990          * @return bool false on failure, true on success
991          */
992         /*
993         function _safe_fwrite( $f, $buf, $len = false ) {
994                 stream_set_blocking( $f, 0 );
995
996                 if ( $len === false ) {
997                         wfDebug( "Writing " . strlen( $buf ) . " bytes\n" );
998                         $bytesWritten = fwrite( $f, $buf );
999                 } else {
1000                         wfDebug( "Writing $len bytes\n" );
1001                         $bytesWritten = fwrite( $f, $buf, $len );
1002                 }
1003                 $n = stream_select( $r = null, $w = array( $f ), $e = null, 10, 0 );
1004                 #   $this->_timeout_seconds, $this->_timeout_microseconds );
1005
1006                 wfDebug( "stream_select returned $n\n" );
1007                 stream_set_blocking( $f, 1 );
1008                 return $n == 1;
1009                 return $bytesWritten;
1010         }*/
1011
1012         /**
1013          * Original behaviour
1014          */
1015         function _safe_fwrite( $f, $buf, $len = false ) {
1016                 if ( $len === false ) {
1017                         $bytesWritten = fwrite( $f, $buf );
1018                 } else {
1019                         $bytesWritten = fwrite( $f, $buf, $len );
1020                 }
1021                 return $bytesWritten;
1022         }
1023
1024         /**
1025          * Flush the read buffer of a stream
1026          */
1027         function _flush_read_buffer( $f ) {
1028                 if ( !is_resource( $f ) ) {
1029                         return;
1030                 }
1031                 $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
1032                 while ( $n == 1 && !feof( $f ) ) {
1033                         fread( $f, 1024 );
1034                         $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
1035                 }
1036         }
1037
1038         // }}}
1039         // }}}
1040         // }}}
1041 }
1042
1043 // vim: sts=3 sw=3 et
1044
1045 // }}}
1046
1047 class MemCachedClientforWiki extends MWMemcached {
1048         function _debugprint( $text ) {
1049                 wfDebug( "memcached: $text" );
1050         }
1051 }