24use Wikimedia\ScopedCallback;
52 parent::__construct( $params );
56 'compress_threshold' => 1500,
57 'connect_timeout' => 0.5,
59 'serializer' =>
'php',
60 'use_binary_protocol' =>
false,
61 'allow_tcp_nagle_delay' =>
true
64 if ( $params[
'persistent'] ) {
68 $connectionPoolId = md5(
serialize( $params ) );
69 $client =
new Memcached( $connectionPoolId );
71 $client =
new Memcached();
74 $this->initializeClient( $client, $params );
76 $this->client = $client;
80 ini_set(
'memcached.compression_threshold', $params[
'compress_threshold'] );
91 private function initializeClient( Memcached $client, array $params ) {
92 if ( $client->getServerList() ) {
93 $this->logger->debug( __METHOD__ .
": pre-initialized client instance." );
98 $this->logger->debug( __METHOD__ .
": initializing new client instance." );
101 Memcached::OPT_NO_BLOCK =>
false,
102 Memcached::OPT_BUFFER_WRITES =>
false,
103 Memcached::OPT_NOREPLY =>
false,
105 Memcached::OPT_BINARY_PROTOCOL => $params[
'use_binary_protocol'],
107 Memcached::OPT_CONNECT_TIMEOUT => $params[
'connect_timeout'] * 1000,
108 Memcached::OPT_SEND_TIMEOUT => $params[
'timeout'],
109 Memcached::OPT_RECV_TIMEOUT => $params[
'timeout'],
110 Memcached::OPT_POLL_TIMEOUT => $params[
'timeout'] / 1000,
112 Memcached::OPT_TCP_NODELAY => !$params[
'allow_tcp_nagle_delay'],
114 Memcached::OPT_LIBKETAMA_COMPATIBLE =>
true
116 if ( isset( $params[
'retry_timeout'] ) ) {
117 $options[Memcached::OPT_RETRY_TIMEOUT] = $params[
'retry_timeout'];
119 if ( isset( $params[
'server_failure_limit'] ) ) {
120 $options[Memcached::OPT_SERVER_FAILURE_LIMIT] = $params[
'server_failure_limit'];
122 if ( $params[
'serializer'] ===
'php' ) {
123 $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_PHP;
124 } elseif ( $params[
'serializer'] ===
'igbinary' ) {
126 if ( !Memcached::HAVE_IGBINARY ) {
127 throw new RuntimeException(
128 __CLASS__ .
': the igbinary extension is not available ' .
129 'but igbinary serialization was requested.'
132 $options[Memcached::OPT_SERIALIZER] = Memcached::SERIALIZER_IGBINARY;
135 if ( !$client->setOptions( $options ) ) {
136 throw new RuntimeException(
137 "Invalid options: " . json_encode( $options, JSON_PRETTY_PRINT )
142 foreach ( $params[
'servers'] as $host ) {
143 if ( preg_match(
'/^\[(.+)\]:(\d+)$/', $host, $m ) ) {
144 $servers[] = [ $m[1], (int)$m[2] ];
145 } elseif ( preg_match(
'/^([^:]+):(\d+)$/', $host, $m ) ) {
146 $servers[] = [ $m[1], (int)$m[2] ];
148 $servers[] = [ $host, false ];
152 if ( !$client->addServers( $servers ) ) {
153 throw new RuntimeException(
"Failed to inject server address list" );
165 private function noReplyScope( $flags ) {
166 if ( $flags !==
true && !( $flags & self::WRITE_BACKGROUND ) ) {
169 $client = $this->client;
170 $client->setOption( Memcached::OPT_NOREPLY,
true );
171 return new ScopedCallback(
static function () use ( $client ) {
172 $client->setOption( Memcached::OPT_NOREPLY,
false );
176 protected function doGet( $key, $flags = 0, &$casToken =
null ) {
177 $getToken = ( $casToken === self::PASS_BY_REF );
180 $this->
debug(
"get($key)" );
187 $flags = Memcached::GET_EXTENDED;
188 $res = $this->client->get( $routeKey,
null, $flags );
189 if ( is_array(
$res ) ) {
190 $result =
$res[
'value'];
191 $casToken =
$res[
'cas'];
196 $result = $this->client->get( $routeKey );
202 protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
203 $this->
debug(
"set($key)" );
207 $noReplyScope = $this->noReplyScope( $flags );
208 $result = $this->client->set( $routeKey, $value, $this->
fixExpiry( $exptime ) );
209 ScopedCallback::consume( $noReplyScope );
211 return ( !$result && $this->client->getResultCode() === Memcached::RES_NOTSTORED )
217 protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
218 $this->
debug(
"cas($key)" );
221 $result = $this->client->cas(
231 $this->
debug(
"delete($key)" );
234 $noReplyScope = $this->noReplyScope( $flags );
235 $result = $this->client->delete( $routeKey );
236 ScopedCallback::consume( $noReplyScope );
238 return ( !$result && $this->client->getResultCode() === Memcached::RES_NOTFOUND )
244 protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) {
245 $this->
debug(
"add($key)" );
248 $noReplyScope = $this->noReplyScope( $flags );
249 $result = $this->client->add(
254 ScopedCallback::consume( $noReplyScope );
260 $this->
debug(
"incrWithInit($key)" );
263 $scope = $this->noReplyScope(
true );
264 $this->
checkResult( $key, $this->client->add( $routeKey, $init - $step, $this->fixExpiry( $exptime ) ) );
265 $this->
checkResult( $key, $this->client->increment( $routeKey, $step ) );
266 ScopedCallback::consume( $scope );
272 $this->
debug(
"incrWithInit($key)" );
275 $result = $this->client->increment( $routeKey, $step );
277 if ( $newValue ===
false && !$this->
getLastError( $watchPoint ) ) {
279 $result = $this->client->add( $routeKey, $init, $this->
fixExpiry( $exptime ) );
280 $newValue = $this->
checkResult( $key, $result ) ? $init :
false;
281 if ( $newValue ===
false && !$this->
getLastError( $watchPoint ) ) {
283 $result = $this->client->increment( $routeKey, $step );
303 static $statusByCode = [
304 Memcached::RES_HOST_LOOKUP_FAILURE => self::ERR_UNREACHABLE,
305 Memcached::RES_SERVER_MARKED_DEAD => self::ERR_UNREACHABLE,
306 Memcached::RES_SERVER_TEMPORARILY_DISABLED => self::ERR_UNREACHABLE,
307 Memcached::RES_UNKNOWN_READ_FAILURE => self::ERR_NO_RESPONSE,
308 Memcached::RES_WRITE_FAILURE => self::ERR_NO_RESPONSE,
309 Memcached::RES_PARTIAL_READ => self::ERR_NO_RESPONSE,
312 3 => self::ERR_UNREACHABLE,
313 27 => self::ERR_UNREACHABLE,
314 6 => self::ERR_NO_RESPONSE
317 if ( $result !==
false ) {
321 $client = $this->client;
322 $code = $client->getResultCode();
324 case Memcached::RES_SUCCESS:
326 case Memcached::RES_DATA_EXISTS:
327 case Memcached::RES_NOTSTORED:
328 case Memcached::RES_NOTFOUND:
329 $this->
debug(
"result: " . $client->getResultMessage() );
332 $msg = $client->getResultMessage();
334 if ( $key !==
false ) {
335 $server = $client->getServerByKey( $key );
336 $logCtx[
'memcached-server'] =
"{$server['host']}:{$server['port']}";
337 $logCtx[
'memcached-key'] = $key;
338 $msg =
"Memcached error for key \"{memcached-key}\" " .
339 "on server \"{memcached-server}\": $msg";
341 $msg =
"Memcached error: $msg";
343 $this->logger->error( $msg, $logCtx );
344 $this->
setLastError( $statusByCode[$code] ?? self::ERR_UNEXPECTED );
350 $this->
debug(
'getMulti(' . implode(
', ',
$keys ) .
')' );
353 foreach (
$keys as $key ) {
360 $resByRouteKey = $this->client->getMulti( $routeKeys );
362 if ( is_array( $resByRouteKey ) ) {
364 foreach ( $resByRouteKey as $routeKey => $value ) {
375 protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
376 $this->
debug(
'setMulti(' . implode(
', ', array_keys( $data ) ) .
')' );
379 $dataByRouteKey = [];
380 foreach ( $data as $key => $value ) {
384 $noReplyScope = $this->noReplyScope( $flags );
388 $result = @$this->client->setMulti( $dataByRouteKey, $exptime );
389 ScopedCallback::consume( $noReplyScope );
394 $this->
debug(
'deleteMulti(' . implode(
', ',
$keys ) .
')' );
397 foreach (
$keys as $key ) {
401 $noReplyScope = $this->noReplyScope( $flags );
402 $resultArray = $this->client->deleteMulti( $routeKeys ) ?: [];
403 ScopedCallback::consume( $noReplyScope );
406 foreach ( $resultArray as $code ) {
407 if ( !in_array( $code, [
true, Memcached::RES_NOTFOUND ],
true ) ) {
417 $this->
debug(
"touch($key)" );
422 $result = $this->client->touch( $routeKey, $this->
fixExpiry( $exptime ) );
428 if ( is_int( $value ) ) {
432 $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
433 if ( $serializer === Memcached::SERIALIZER_PHP ) {
435 } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
436 return igbinary_serialize( $value );
439 throw new UnexpectedValueException( __METHOD__ .
": got serializer '$serializer'." );
447 $serializer = $this->client->getOption( Memcached::OPT_SERIALIZER );
448 if ( $serializer === Memcached::SERIALIZER_PHP ) {
450 } elseif ( $serializer === Memcached::SERIALIZER_IGBINARY ) {
451 return igbinary_unserialize( $value );
454 throw new UnexpectedValueException( __METHOD__ .
": got serializer '$serializer'." );
getLastError( $watchPoint=0)
Get the "last error" registry.
setLastError( $error)
Set the "last error" registry due to a problem encountered during an attempted operation.
watchErrors()
Get a "watch point" token that can be used to get the "last error" to occur after now.
int $lastError
BagOStuff:ERR_* constant of the last error that occurred.
isInteger( $value)
Check if a value is an integer.
Base class for memcached clients.
validateKeyAndPrependRoute( $key)
A wrapper class for the PECL memcached client.
doAdd( $key, $value, $exptime=0, $flags=0)
Insert an item if it does not already exist.
doGet( $key, $flags=0, &$casToken=null)
Get an item.
doGetMulti(array $keys, $flags=0)
Get an associative array containing the item for each of the keys that have items.
doCas( $casToken, $key, $value, $exptime=0, $flags=0)
Set an item if the current CAS token matches the provided CAS token.
doSet( $key, $value, $exptime=0, $flags=0)
Set an item.
doChangeTTL( $key, $exptime, $flags)
doDeleteMulti(array $keys, $flags=0)
doSetMulti(array $data, $exptime=0, $flags=0)
checkResult( $key, $result)
Check the return value from a client method call and take any necessary action.
doDelete( $key, $flags=0)
Delete an item.
__construct( $params)
Available parameters are:
doIncrWithInitAsync( $key, $exptime, $step, $init)
doIncrWithInitSync( $key, $exptime, $step, $init)