69 use Psr\Log\LoggerInterface;
70 use Psr\Log\NullLogger;
261 $this->_debug =
$args[
'debug'] ??
false;
262 $this->stats =
array();
263 $this->_compress_threshold =
$args[
'compress_threshold'] ?? 0;
264 $this->_persistent =
$args[
'persistent'] ??
false;
265 $this->_compress_enable =
true;
266 $this->_have_zlib = function_exists(
'gzcompress' );
268 $this->_cache_sock =
array();
269 $this->_host_dead =
array();
271 $this->_timeout_seconds = 0;
272 $this->_timeout_microseconds =
$args[
'timeout'] ?? 500000;
274 $this->_connect_timeout =
$args[
'connect_timeout'] ?? 0.1;
275 $this->_connect_attempts = 2;
277 $this->_logger =
$args[
'logger'] ??
new NullLogger();
297 public function add( $key, $val, $exp = 0 ) {
298 return $this->
_set(
'add', $key, $val, $exp );
312 public function decr( $key, $amt = 1 ) {
313 return $this->
_incrdecr(
'decr', $key, $amt );
327 public function delete( $key,
$time = 0 ) {
328 if ( !$this->_active ) {
333 if ( !is_resource( $sock ) ) {
337 $key = is_array( $key ) ? $key[1] : $key;
339 if ( isset( $this->stats[
'delete'] ) ) {
340 $this->stats[
'delete']++;
342 $this->stats[
'delete'] = 1;
344 $cmd =
"delete $key $time\r\n";
345 if ( !$this->
_fwrite( $sock, $cmd ) ) {
350 if ( $this->_debug ) {
351 $this->
_debugprint( sprintf(
"MemCache: delete %s (%s)", $key,
$res ) );
354 if (
$res ==
"DELETED" ||
$res ==
"NOT_FOUND" ) {
370 if ( !$this->_active ) {
375 if ( !is_resource( $sock ) ) {
379 $key = is_array( $key ) ? $key[1] : $key;
381 if ( isset( $this->stats[
'touch'] ) ) {
382 $this->stats[
'touch']++;
384 $this->stats[
'touch'] = 1;
386 $cmd =
"touch $key $time\r\n";
387 if ( !$this->
_fwrite( $sock, $cmd ) ) {
392 if ( $this->_debug ) {
393 $this->
_debugprint( sprintf(
"MemCache: touch %s (%s)", $key,
$res ) );
396 if (
$res ==
"TOUCHED" ) {
408 public function lock( $key, $timeout = 0 ) {
429 foreach ( $this->_cache_sock
as $sock ) {
433 $this->_cache_sock =
array();
445 $this->_compress_enable = $enable;
455 $this->_host_dead =
array();
469 public function get( $key, &$casToken =
null ) {
470 if ( $this->_debug ) {
474 if ( !is_array( $key ) && strval( $key ) ===
'' ) {
475 $this->
_debugprint(
"Skipping key which equals to an empty string" );
479 if ( !$this->_active ) {
485 if ( !is_resource( $sock ) ) {
489 $key = is_array( $key ) ? $key[1] : $key;
490 if ( isset( $this->stats[
'get'] ) ) {
491 $this->stats[
'get']++;
493 $this->stats[
'get'] = 1;
496 $cmd =
"gets $key\r\n";
497 if ( !$this->
_fwrite( $sock, $cmd ) ) {
504 if ( $this->_debug ) {
505 foreach ( $val
as $k => $v ) {
511 if ( isset( $val[$key] ) ) {
528 if ( !$this->_active ) {
532 if ( isset( $this->stats[
'get_multi'] ) ) {
533 $this->stats[
'get_multi']++;
535 $this->stats[
'get_multi'] = 1;
537 $sock_keys =
array();
541 if ( !is_resource( $sock ) ) {
544 $key = is_array( $key ) ? $key[1] : $key;
545 if ( !isset( $sock_keys[$sock] ) ) {
546 $sock_keys[intval( $sock )] =
array();
549 $sock_keys[intval( $sock )][] = $key;
554 foreach ( $socks
as $sock ) {
556 foreach ( $sock_keys[intval( $sock )]
as $key ) {
561 if ( $this->
_fwrite( $sock, $cmd ) ) {
568 foreach ( $gather
as $sock ) {
572 if ( $this->_debug ) {
573 foreach ( $val
as $k => $v ) {
574 $this->
_debugprint( sprintf(
"MemCache: got %s", $k ) );
594 public function incr( $key, $amt = 1 ) {
595 return $this->
_incrdecr(
'incr', $key, $amt );
615 return $this->
_set(
'replace', $key,
$value, $exp );
631 if ( !is_resource( $sock ) ) {
635 if ( !$this->
_fwrite( $sock, $cmd ) ) {
643 if ( preg_match(
'/^END/',
$res ) ) {
646 if ( strlen(
$res ) == 0 ) {
670 public function set( $key,
$value, $exp = 0 ) {
671 return $this->
_set(
'set', $key,
$value, $exp );
692 public function cas( $casToken, $key,
$value, $exp = 0 ) {
693 return $this->
_set(
'cas', $key,
$value, $exp, $casToken );
705 $this->_compress_threshold = $thresh;
718 $this->_debug = $dbg;
731 $this->_servers = $list;
732 $this->_active =
count( $list );
733 $this->_buckets =
null;
734 $this->_bucketcount = 0;
736 $this->_single_sock =
null;
737 if ( $this->_active == 1 ) {
738 $this->_single_sock = $this->_servers[0];
749 $this->_timeout_seconds = $seconds;
750 $this->_timeout_microseconds = $microseconds;
766 $host = array_search( $sock, $this->_cache_sock );
767 fclose( $this->_cache_sock[$host] );
768 unset( $this->_cache_sock[$host] );
784 list( $ip, $port ) = preg_split(
'/:(?=\d)/', $host );
787 $errno = $errstr =
null;
789 Wikimedia\suppressWarnings();
790 if ( $this->_persistent == 1 ) {
791 $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
793 $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
795 Wikimedia\restoreWarnings();
798 $this->
_error_log(
"Error connecting to $host: $errstr" );
804 stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
808 if ( $this->_persistent ) {
825 $host = array_search( $sock, $this->_cache_sock );
833 $ip = explode(
':', $host )[0];
834 $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
835 $this->_host_dead[$host] = $this->_host_dead[$ip];
836 unset( $this->_cache_sock[$host] );
851 if ( !$this->_active ) {
855 if ( $this->_single_sock !==
null ) {
859 $hv = is_array( $key ) ? intval( $key[0] ) : $this->
_hashfunc( $key );
860 if ( $this->_buckets ===
null ) {
862 foreach ( $this->_servers
as $v ) {
863 if ( is_array( $v ) ) {
864 for ( $i = 0; $i < $v[1]; $i++ ) {
871 $this->_buckets = $bu;
872 $this->_bucketcount =
count( $bu );
875 $realkey = is_array( $key ) ? $key[1] : $key;
876 for ( $tries = 0; $tries < 20; $tries++ ) {
879 if ( is_resource( $sock ) ) {
882 $hv = $this->
_hashfunc( $hv . $realkey );
900 # Hash function must be in [0,0x7ffffff]
901 # We take the first 31 bits of the MD5 hash, which unlike the hash
902 # function used in a previous version of this client, works
903 return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
920 if ( !$this->_active ) {
925 if ( !is_resource( $sock ) ) {
929 $key = is_array( $key ) ? $key[1] : $key;
930 if ( isset( $this->stats[$cmd] ) ) {
931 $this->stats[$cmd]++;
933 $this->stats[$cmd] = 1;
935 if ( !$this->
_fwrite( $sock,
"$cmd $key $amt\r\n" ) ) {
941 if ( !preg_match(
'/^(\d+)/',
$line, $match ) ) {
964 $decl = $this->
_fgets( $sock );
966 if ( $decl ===
false ) {
972 } elseif ( preg_match(
'/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
985 $this->
_fread( $sock, $match[3] + 2 ),
987 } elseif ( $decl ==
"END" ) {
988 if (
count( $results ) == 0 ) {
996 foreach ( $results
as $vars ) {
999 if (
$data ===
false || substr(
$data, -2 ) !==
"\r\n" ) {
1001 'line ending missing from data block from $1' );
1007 if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
1008 $ret[$rkey] = gzuncompress(
$ret[$rkey] );
1020 if ( $flags & self::SERIALIZED ) {
1022 } elseif ( $flags & self::INTVAL ) {
1023 $ret[$rkey] = intval(
$ret[$rkey] );
1029 $this->
_handle_error( $sock,
'Error parsing response from $1' );
1054 function _set( $cmd, $key, $val, $exp, $casToken =
null ) {
1055 if ( !$this->_active ) {
1060 if ( !is_resource( $sock ) ) {
1064 if ( isset( $this->stats[$cmd] ) ) {
1065 $this->stats[$cmd]++;
1067 $this->stats[$cmd] = 1;
1072 if ( is_int( $val ) ) {
1074 } elseif ( !is_scalar( $val ) ) {
1077 if ( $this->_debug ) {
1078 $this->
_debugprint( sprintf(
"client: serializing data as it is not scalar" ) );
1082 $len = strlen( $val );
1084 if ( $this->_have_zlib && $this->_compress_enable
1085 && $this->_compress_threshold && $len >= $this->_compress_threshold
1087 $c_val = gzcompress( $val, 9 );
1088 $c_len = strlen( $c_val );
1090 if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
1091 if ( $this->_debug ) {
1092 $this->
_debugprint( sprintf(
"client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
1100 $command =
"$cmd $key $flags $exp $len";
1105 if ( !$this->
_fwrite( $sock,
"$command\r\n$val\r\n" ) ) {
1111 if ( $this->_debug ) {
1114 if (
$line ===
"STORED" ) {
1116 } elseif (
$line ===
"NOT_STORED" && $cmd ===
"set" ) {
1136 if ( isset( $this->_cache_sock[$host] ) ) {
1137 return $this->_cache_sock[$host];
1142 list( $ip, ) = explode(
':', $host );
1143 if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
1144 isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
1154 stream_set_write_buffer( $sock, 0 );
1156 $this->_cache_sock[$host] = $sock;
1158 return $this->_cache_sock[$host];
1165 $this->_logger->debug( $text );
1172 $this->_logger->error(
"Memcached error: $text" );
1184 $bufSize = strlen( $buf );
1185 while ( $bytesWritten < $bufSize ) {
1186 $result = fwrite( $sock, $buf );
1187 $data = stream_get_meta_data( $sock );
1188 if (
$data[
'timed_out'] ) {
1210 $peer = stream_socket_get_name( $sock,
true );
1211 if ( strval( $peer ) ===
'' ) {
1212 $peer = array_search( $sock, $this->_cache_sock );
1213 if ( $peer ===
false ) {
1214 $peer =
'[unknown host]';
1217 $msg = str_replace(
'$1', $peer, $msg );
1232 while ( $len > 0 ) {
1233 $result = fread( $sock, $len );
1234 $data = stream_get_meta_data( $sock );
1235 if (
$data[
'timed_out'] ) {
1240 $this->
_handle_error( $sock,
'error reading buffer from $1' );
1245 $this->
_handle_error( $sock,
'unexpected end of file reading from $1' );
1266 $data = stream_get_meta_data( $sock );
1267 if (
$data[
'timed_out'] ) {
1268 $this->
_handle_error( $sock,
'timeout reading line from $1' );
1272 $this->
_handle_error( $sock,
'error reading line from $1' );
1275 if ( substr(
$result, -2 ) ===
"\r\n" ) {
1277 } elseif ( substr(
$result, -1 ) ===
"\n" ) {
1280 $this->
_handle_error( $sock,
'line ending missing in response from $1' );
1291 if ( !is_resource(
$f ) ) {
1297 $n = stream_select( $r, $w,
$e, 0, 0 );
1298 while ( $n == 1 && !feof(
$f ) ) {
1303 $n = stream_select( $r, $w,
$e, 0, 0 );