00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00063
00064
00065
00066
00073 class MWMemcached {
00074
00075
00076
00077
00078
00079
00083 const SERIALIZED = 1;
00084
00088 const COMPRESSED = 2;
00089
00090
00091
00095 const COMPRESSION_SAVINGS = 0.20;
00096
00097
00098
00099
00106 var $stats;
00107
00108
00109
00110
00117 var $_cache_sock;
00118
00125 var $_debug;
00126
00133 var $_host_dead;
00134
00141 var $_have_zlib;
00142
00149 var $_compress_enable;
00150
00157 var $_compress_threshold;
00158
00165 var $_persistant;
00166
00173 var $_single_sock;
00174
00181 var $_servers;
00182
00189 var $_buckets;
00190
00197 var $_bucketcount;
00198
00205 var $_active;
00206
00213 var $_timeout_seconds;
00214
00221 var $_timeout_microseconds;
00222
00226 var $_connect_timeout;
00227
00231 var $_connect_attempts;
00232
00233
00234
00235
00236
00237
00238
00246 public function __construct( $args ) {
00247 global $wgMemCachedTimeout;
00248 $this->set_servers( @$args['servers'] );
00249 $this->_debug = @$args['debug'];
00250 $this->stats = array();
00251 $this->_compress_threshold = @$args['compress_threshold'];
00252 $this->_persistant = array_key_exists( 'persistant', $args ) ? ( @$args['persistant'] ) : false;
00253 $this->_compress_enable = true;
00254 $this->_have_zlib = function_exists( 'gzcompress' );
00255
00256 $this->_cache_sock = array();
00257 $this->_host_dead = array();
00258
00259 $this->_timeout_seconds = 0;
00260 $this->_timeout_microseconds = $wgMemCachedTimeout;
00261
00262 $this->_connect_timeout = 0.01;
00263 $this->_connect_attempts = 2;
00264 }
00265
00266
00267
00268
00279 public function add( $key, $val, $exp = 0 ) {
00280 return $this->_set( 'add', $key, $val, $exp );
00281 }
00282
00283
00284
00285
00294 public function decr( $key, $amt = 1 ) {
00295 return $this->_incrdecr( 'decr', $key, $amt );
00296 }
00297
00298
00299
00300
00309 public function delete( $key, $time = 0 ) {
00310 if ( !$this->_active ) {
00311 return false;
00312 }
00313
00314 $sock = $this->get_sock( $key );
00315 if ( !is_resource( $sock ) ) {
00316 return false;
00317 }
00318
00319 $key = is_array( $key ) ? $key[1] : $key;
00320
00321 @$this->stats['delete']++;
00322 $cmd = "delete $key $time\r\n";
00323 if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
00324 $this->_dead_sock( $sock );
00325 return false;
00326 }
00327 $res = trim( fgets( $sock ) );
00328
00329 if ( $this->_debug ) {
00330 $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) );
00331 }
00332
00333 if ( $res == "DELETED" ) {
00334 return true;
00335 }
00336 return false;
00337 }
00338
00339
00340
00341
00345 public function disconnect_all() {
00346 foreach ( $this->_cache_sock as $sock ) {
00347 fclose( $sock );
00348 }
00349
00350 $this->_cache_sock = array();
00351 }
00352
00353
00354
00355
00361 public function enable_compress( $enable ) {
00362 $this->_compress_enable = $enable;
00363 }
00364
00365
00366
00367
00371 public function forget_dead_hosts() {
00372 $this->_host_dead = array();
00373 }
00374
00375
00376
00377
00385 public function get( $key ) {
00386 wfProfileIn( __METHOD__ );
00387
00388 if ( $this->_debug ) {
00389 $this->_debugprint( "get($key)\n" );
00390 }
00391
00392 if ( !$this->_active ) {
00393 wfProfileOut( __METHOD__ );
00394 return false;
00395 }
00396
00397 $sock = $this->get_sock( $key );
00398
00399 if ( !is_resource( $sock ) ) {
00400 wfProfileOut( __METHOD__ );
00401 return false;
00402 }
00403
00404 @$this->stats['get']++;
00405
00406 $cmd = "get $key\r\n";
00407 if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
00408 $this->_dead_sock( $sock );
00409 wfProfileOut( __METHOD__ );
00410 return false;
00411 }
00412
00413 $val = array();
00414 $this->_load_items( $sock, $val );
00415
00416 if ( $this->_debug ) {
00417 foreach ( $val as $k => $v ) {
00418 $this->_debugprint( sprintf( "MemCache: sock %s got %s\n", serialize( $sock ), $k ) );
00419 }
00420 }
00421
00422 wfProfileOut( __METHOD__ );
00423 return @$val[$key];
00424 }
00425
00426
00427
00428
00436 public function get_multi( $keys ) {
00437 if ( !$this->_active ) {
00438 return false;
00439 }
00440
00441 @$this->stats['get_multi']++;
00442 $sock_keys = array();
00443
00444 foreach ( $keys as $key ) {
00445 $sock = $this->get_sock( $key );
00446 if ( !is_resource( $sock ) ) {
00447 continue;
00448 }
00449 $key = is_array( $key ) ? $key[1] : $key;
00450 if ( !isset( $sock_keys[$sock] ) ) {
00451 $sock_keys[$sock] = array();
00452 $socks[] = $sock;
00453 }
00454 $sock_keys[$sock][] = $key;
00455 }
00456
00457
00458 foreach ( $socks as $sock ) {
00459 $cmd = 'get';
00460 foreach ( $sock_keys[$sock] as $key ) {
00461 $cmd .= ' ' . $key;
00462 }
00463 $cmd .= "\r\n";
00464
00465 if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
00466 $gather[] = $sock;
00467 } else {
00468 $this->_dead_sock( $sock );
00469 }
00470 }
00471
00472
00473 $val = array();
00474 foreach ( $gather as $sock ) {
00475 $this->_load_items( $sock, $val );
00476 }
00477
00478 if ( $this->_debug ) {
00479 foreach ( $val as $k => $v ) {
00480 $this->_debugprint( sprintf( "MemCache: got %s\n", $k ) );
00481 }
00482 }
00483
00484 return $val;
00485 }
00486
00487
00488
00489
00498 public function incr( $key, $amt = 1 ) {
00499 return $this->_incrdecr( 'incr', $key, $amt );
00500 }
00501
00502
00503
00504
00514 public function replace( $key, $value, $exp = 0 ) {
00515 return $this->_set( 'replace', $key, $value, $exp );
00516 }
00517
00518
00519
00520
00537 function run_command( $sock, $cmd ) {
00538 if ( !is_resource( $sock ) ) {
00539 return array();
00540 }
00541
00542 if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
00543 return array();
00544 }
00545
00546 while ( true ) {
00547 $res = fgets( $sock );
00548 $ret[] = $res;
00549 if ( preg_match( '/^END/', $res ) ) {
00550 break;
00551 }
00552 if ( strlen( $res ) == 0 ) {
00553 break;
00554 }
00555 }
00556 return $ret;
00557 }
00558
00559
00560
00561
00572 public function set( $key, $value, $exp = 0 ) {
00573 return $this->_set( 'set', $key, $value, $exp );
00574 }
00575
00576
00577
00578
00584 public function set_compress_threshold( $thresh ) {
00585 $this->_compress_threshold = $thresh;
00586 }
00587
00588
00589
00590
00598 public function set_debug( $dbg ) {
00599 $this->_debug = $dbg;
00600 }
00601
00602
00603
00604
00612 public function set_servers( $list ) {
00613 $this->_servers = $list;
00614 $this->_active = count( $list );
00615 $this->_buckets = null;
00616 $this->_bucketcount = 0;
00617
00618 $this->_single_sock = null;
00619 if ( $this->_active == 1 ) {
00620 $this->_single_sock = $this->_servers[0];
00621 }
00622 }
00623
00630 public function set_timeout( $seconds, $microseconds ) {
00631 $this->_timeout_seconds = $seconds;
00632 $this->_timeout_microseconds = $microseconds;
00633 }
00634
00635
00636
00637
00638
00639
00647 function _close_sock( $sock ) {
00648 $host = array_search( $sock, $this->_cache_sock );
00649 fclose( $this->_cache_sock[$host] );
00650 unset( $this->_cache_sock[$host] );
00651 }
00652
00653
00654
00655
00665 function _connect_sock( &$sock, $host ) {
00666 list( $ip, $port ) = explode( ':', $host );
00667 $sock = false;
00668 $timeout = $this->_connect_timeout;
00669 $errno = $errstr = null;
00670 for( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
00671 if ( $this->_persistant == 1 ) {
00672 $sock = @pfsockopen( $ip, $port, $errno, $errstr, $timeout );
00673 } else {
00674 $sock = @fsockopen( $ip, $port, $errno, $errstr, $timeout );
00675 }
00676 }
00677 if ( !$sock ) {
00678 if ( $this->_debug ) {
00679 $this->_debugprint( "Error connecting to $host: $errstr\n" );
00680 }
00681 return false;
00682 }
00683
00684
00685 stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
00686
00687 return true;
00688 }
00689
00690
00691
00692
00700 function _dead_sock( $sock ) {
00701 $host = array_search( $sock, $this->_cache_sock );
00702 $this->_dead_host( $host );
00703 }
00704
00705 function _dead_host( $host ) {
00706 @list( $ip, ) = explode( ':', $host );
00707 $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
00708 $this->_host_dead[$host] = $this->_host_dead[$ip];
00709 unset( $this->_cache_sock[$host] );
00710 }
00711
00712
00713
00714
00723 function get_sock( $key ) {
00724 if ( !$this->_active ) {
00725 return false;
00726 }
00727
00728 if ( $this->_single_sock !== null ) {
00729 $this->_flush_read_buffer( $this->_single_sock );
00730 return $this->sock_to_host( $this->_single_sock );
00731 }
00732
00733 $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
00734
00735 if ( $this->_buckets === null ) {
00736 foreach ( $this->_servers as $v ) {
00737 if ( is_array( $v ) ) {
00738 for( $i = 0; $i < $v[1]; $i++ ) {
00739 $bu[] = $v[0];
00740 }
00741 } else {
00742 $bu[] = $v;
00743 }
00744 }
00745 $this->_buckets = $bu;
00746 $this->_bucketcount = count( $bu );
00747 }
00748
00749 $realkey = is_array( $key ) ? $key[1] : $key;
00750 for( $tries = 0; $tries < 20; $tries++ ) {
00751 $host = $this->_buckets[$hv % $this->_bucketcount];
00752 $sock = $this->sock_to_host( $host );
00753 if ( is_resource( $sock ) ) {
00754 $this->_flush_read_buffer( $sock );
00755 return $sock;
00756 }
00757 $hv = $this->_hashfunc( $hv . $realkey );
00758 }
00759
00760 return false;
00761 }
00762
00763
00764
00765
00774 function _hashfunc( $key ) {
00775 # Hash function must on [0,0x7ffffff]
00776 # We take the first 31 bits of the MD5 hash, which unlike the hash
00777 # function used in a previous version of this client, works
00778 return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
00779 }
00780
00781
00782
00783
00794 function _incrdecr( $cmd, $key, $amt = 1 ) {
00795 if ( !$this->_active ) {
00796 return null;
00797 }
00798
00799 $sock = $this->get_sock( $key );
00800 if ( !is_resource( $sock ) ) {
00801 return null;
00802 }
00803
00804 $key = is_array( $key ) ? $key[1] : $key;
00805 @$this->stats[$cmd]++;
00806 if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
00807 return $this->_dead_sock( $sock );
00808 }
00809
00810 $line = fgets( $sock );
00811 $match = array();
00812 if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
00813 return null;
00814 }
00815 return $match[1];
00816 }
00817
00818
00819
00820
00829 function _load_items( $sock, &$ret ) {
00830 while ( 1 ) {
00831 $decl = fgets( $sock );
00832 if ( $decl == "END\r\n" ) {
00833 return true;
00834 } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
00835 list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
00836 $bneed = $len + 2;
00837 $offset = 0;
00838
00839 while ( $bneed > 0 ) {
00840 $data = fread( $sock, $bneed );
00841 $n = strlen( $data );
00842 if ( $n == 0 ) {
00843 break;
00844 }
00845 $offset += $n;
00846 $bneed -= $n;
00847 @$ret[$rkey] .= $data;
00848 }
00849
00850 if ( $offset != $len + 2 ) {
00851
00852 if ( $this->_debug ) {
00853 $this->_debugprint( sprintf( "Something is borked! key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) );
00854 }
00855
00856 unset( $ret[$rkey] );
00857 $this->_close_sock( $sock );
00858 return false;
00859 }
00860
00861 if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
00862 $ret[$rkey] = gzuncompress( $ret[$rkey] );
00863 }
00864
00865 $ret[$rkey] = rtrim( $ret[$rkey] );
00866
00867 if ( $flags & self::SERIALIZED ) {
00868 $ret[$rkey] = unserialize( $ret[$rkey] );
00869 }
00870
00871 } else {
00872 $this->_debugprint( "Error parsing memcached response\n" );
00873 return 0;
00874 }
00875 }
00876 }
00877
00878
00879
00880
00892 function _set( $cmd, $key, $val, $exp ) {
00893 if ( !$this->_active ) {
00894 return false;
00895 }
00896
00897 $sock = $this->get_sock( $key );
00898 if ( !is_resource( $sock ) ) {
00899 return false;
00900 }
00901
00902 @$this->stats[$cmd]++;
00903
00904 $flags = 0;
00905
00906 if ( !is_scalar( $val ) ) {
00907 $val = serialize( $val );
00908 $flags |= self::SERIALIZED;
00909 if ( $this->_debug ) {
00910 $this->_debugprint( sprintf( "client: serializing data as it is not scalar\n" ) );
00911 }
00912 }
00913
00914 $len = strlen( $val );
00915
00916 if ( $this->_have_zlib && $this->_compress_enable &&
00917 $this->_compress_threshold && $len >= $this->_compress_threshold )
00918 {
00919 $c_val = gzcompress( $val, 9 );
00920 $c_len = strlen( $c_val );
00921
00922 if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
00923 if ( $this->_debug ) {
00924 $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes\n", $len, $c_len ) );
00925 }
00926 $val = $c_val;
00927 $len = $c_len;
00928 $flags |= self::COMPRESSED;
00929 }
00930 }
00931 if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
00932 return $this->_dead_sock( $sock );
00933 }
00934
00935 $line = trim( fgets( $sock ) );
00936
00937 if ( $this->_debug ) {
00938 $this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) );
00939 }
00940 if ( $line == "STORED" ) {
00941 return true;
00942 }
00943 return false;
00944 }
00945
00946
00947
00948
00957 function sock_to_host( $host ) {
00958 if ( isset( $this->_cache_sock[$host] ) ) {
00959 return $this->_cache_sock[$host];
00960 }
00961
00962 $sock = null;
00963 $now = time();
00964 list( $ip, ) = explode( ':', $host );
00965 if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
00966 isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
00967 ) {
00968 return null;
00969 }
00970
00971 if ( !$this->_connect_sock( $sock, $host ) ) {
00972 return $this->_dead_host( $host );
00973 }
00974
00975
00976 stream_set_write_buffer( $sock, 0 );
00977
00978 $this->_cache_sock[$host] = $sock;
00979
00980 return $this->_cache_sock[$host];
00981 }
00982
00983 function _debugprint( $str ) {
00984 print( $str );
00985 }
00986
00992
00993
00994
00995
00996
00997
00998
00999
01000
01001
01002
01003
01004
01005
01006
01007
01008
01009
01010
01011
01015 function _safe_fwrite( $f, $buf, $len = false ) {
01016 if ( $len === false ) {
01017 $bytesWritten = fwrite( $f, $buf );
01018 } else {
01019 $bytesWritten = fwrite( $f, $buf, $len );
01020 }
01021 return $bytesWritten;
01022 }
01023
01027 function _flush_read_buffer( $f ) {
01028 if ( !is_resource( $f ) ) {
01029 return;
01030 }
01031 $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
01032 while ( $n == 1 && !feof( $f ) ) {
01033 fread( $f, 1024 );
01034 $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
01035 }
01036 }
01037
01038
01039
01040
01041 }
01042
01043
01044
01045
01046
01047 class MemCachedClientforWiki extends MWMemcached {
01048 function _debugprint( $text ) {
01049 wfDebug( "memcached: $text" );
01050 }
01051 }