diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-01-18 16:46:04 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-01-18 16:46:04 +0100 |
commit | 63601400e476c6cf43d985f3e7b9864681695ed4 (patch) | |
tree | f7846203a952e38aaf66989d0a4702779f549962 /includes/objectcache | |
parent | 8ff01378c9e0207f9169b81966a51def645b6a51 (diff) |
Update to MediaWiki 1.20.2
this update includes:
* adjusted Arch Linux skin
* updated FluxBBAuthPlugin
* patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024
Diffstat (limited to 'includes/objectcache')
-rw-r--r-- | includes/objectcache/APCBagOStuff.php | 60 | ||||
-rw-r--r-- | includes/objectcache/BagOStuff.php | 119 | ||||
-rw-r--r-- | includes/objectcache/DBABagOStuff.php | 127 | ||||
-rw-r--r-- | includes/objectcache/EhcacheBagOStuff.php | 82 | ||||
-rw-r--r-- | includes/objectcache/EmptyBagOStuff.php | 37 | ||||
-rw-r--r-- | includes/objectcache/HashBagOStuff.php | 44 | ||||
-rw-r--r-- | includes/objectcache/MemcachedBagOStuff.php | 180 | ||||
-rw-r--r-- | includes/objectcache/MemcachedClient.php | 281 | ||||
-rw-r--r-- | includes/objectcache/MemcachedPeclBagOStuff.php | 237 | ||||
-rw-r--r-- | includes/objectcache/MemcachedPhpBagOStuff.php | 139 | ||||
-rw-r--r-- | includes/objectcache/MultiWriteBagOStuff.php | 86 | ||||
-rw-r--r-- | includes/objectcache/ObjectCache.php | 48 | ||||
-rw-r--r-- | includes/objectcache/ObjectCacheSessionHandler.php | 145 | ||||
-rw-r--r-- | includes/objectcache/RedisBagOStuff.php | 413 | ||||
-rw-r--r-- | includes/objectcache/SqlBagOStuff.php | 314 | ||||
-rw-r--r-- | includes/objectcache/WinCacheBagOStuff.php | 24 | ||||
-rw-r--r-- | includes/objectcache/XCacheBagOStuff.php | 44 |
17 files changed, 2011 insertions, 369 deletions
diff --git a/includes/objectcache/APCBagOStuff.php b/includes/objectcache/APCBagOStuff.php index dd4a76e1..1a0de218 100644 --- a/includes/objectcache/APCBagOStuff.php +++ b/includes/objectcache/APCBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using PHP's APC accelerator. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * This is a wrapper for APC's shared memory functions @@ -6,28 +27,62 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + /** + * @param $key string + * @return mixed + */ public function get( $key ) { $val = apc_fetch( $key ); if ( is_string( $val ) ) { - $val = unserialize( $val ); + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } } return $val; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { - apc_store( $key, serialize( $value ), $exptime ); + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + apc_store( $key, $value, $exptime ); return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { apc_delete( $key ); return true; } + public function incr( $key, $value = 1 ) { + return apc_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return apc_dec( $key, $value ); + } + + /** + * @return Array + */ public function keys() { $info = apc_cache_info( 'user' ); $list = $info['cache_list']; @@ -40,4 +95,3 @@ class APCBagOStuff extends BagOStuff { return $keys; } } - diff --git a/includes/objectcache/BagOStuff.php b/includes/objectcache/BagOStuff.php index 81ad6621..7bbaff93 100644 --- a/includes/objectcache/BagOStuff.php +++ b/includes/objectcache/BagOStuff.php @@ -56,8 +56,7 @@ abstract class BagOStuff { /** * Get an item with the given key. Returns false if it does not exist. * @param $key string - * - * @return bool|Object + * @return mixed Returns false on failure */ abstract public function get( $key ); @@ -66,6 +65,7 @@ abstract class BagOStuff { * @param $key string * @param $value mixed * @param $exptime int Either an interval in seconds or a unix timestamp for expiry + * @return bool success */ abstract public function set( $key, $value, $exptime = 0 ); @@ -73,19 +73,33 @@ abstract class BagOStuff { * Delete an item. * @param $key string * @param $time int Amount of time to delay the operation (mostly memcached-specific) + * @return bool True if the item was deleted or not found, false on failure */ abstract public function delete( $key, $time = 0 ); + /** + * @param $key string + * @param $timeout integer + * @return bool success + */ public function lock( $key, $timeout = 0 ) { /* stub */ return true; } + /** + * @param $key string + * @return bool success + */ public function unlock( $key ) { /* stub */ return true; } + /** + * @todo: what is this? + * @return Array + */ public function keys() { /* stub */ return array(); @@ -93,12 +107,12 @@ abstract class BagOStuff { /** * Delete all objects expiring before a certain date. - * @param $date The reference date in MW format - * @param $progressCallback Optional, a function which will be called + * @param $date string The reference date in MW format + * @param $progressCallback callback|bool Optional, a function which will be called * regularly during long-running operations with the percentage progress * as the first parameter. * - * @return true on success, false if unimplemented + * @return bool on success, false if unimplemented */ public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { // stub @@ -107,45 +121,83 @@ abstract class BagOStuff { /* *** Emulated functions *** */ - public function add( $key, $value, $exptime = 0 ) { - if ( !$this->get( $key ) ) { - $this->set( $key, $value, $exptime ); + /** + * Get an associative array containing the item for each of the keys that have items. + * @param $keys Array List of strings + * @return Array + */ + public function getMulti( array $keys ) { + $res = array(); + foreach ( $keys as $key ) { + $val = $this->get( $key ); + if ( $val !== false ) { + $res[$key] = $val; + } + } + return $res; + } - return true; + /** + * @param $key string + * @param $value mixed + * @param $exptime integer + * @return bool success + */ + public function add( $key, $value, $exptime = 0 ) { + if ( $this->get( $key ) === false ) { + return $this->set( $key, $value, $exptime ); } + return false; // key already set } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool success + */ public function replace( $key, $value, $exptime = 0 ) { if ( $this->get( $key ) !== false ) { - $this->set( $key, $value, $exptime ); + return $this->set( $key, $value, $exptime ); } + return false; // key not already set } /** + * Increase stored value of $key by $value while preserving its TTL * @param $key String: Key to increase * @param $value Integer: Value to add to $key (Default 1) - * @return null if lock is not possible else $key value increased by $value + * @return integer|bool New value or false on failure */ public function incr( $key, $value = 1 ) { if ( !$this->lock( $key ) ) { - return null; + return false; } - - $value = intval( $value ); - - if ( ( $n = $this->get( $key ) ) !== false ) { - $n += $value; - $this->set( $key, $n ); // exptime? + $n = $this->get( $key ); + if ( $this->isInteger( $n ) ) { // key exists? + $n += intval( $value ); + $this->set( $key, max( 0, $n ) ); // exptime? + } else { + $n = false; } $this->unlock( $key ); return $n; } + /** + * Decrease stored value of $key by $value while preserving its TTL + * @param $key String + * @param $value Integer + * @return integer + */ public function decr( $key, $value = 1 ) { return $this->incr( $key, - $value ); } + /** + * @param $text string + */ public function debug( $text ) { if ( $this->debugMode ) { $class = get_class( $this ); @@ -155,6 +207,8 @@ abstract class BagOStuff { /** * Convert an optionally relative time to an absolute time + * @param $exptime integer + * @return int */ protected function convertExpiry( $exptime ) { if ( ( $exptime != 0 ) && ( $exptime < 86400 * 3650 /* 10 years */ ) ) { @@ -163,6 +217,33 @@ abstract class BagOStuff { return $exptime; } } -} + /** + * Convert an optionally absolute expiry time to a relative time. If an + * absolute time is specified which is in the past, use a short expiry time. + * + * @param $exptime integer + * @return integer + */ + protected function convertToRelative( $exptime ) { + if ( $exptime >= 86400 * 3650 /* 10 years */ ) { + $exptime -= time(); + if ( $exptime <= 0 ) { + $exptime = 1; + } + return $exptime; + } else { + return $exptime; + } + } + /** + * Check if a value is an integer + * + * @param $value mixed + * @return bool + */ + protected function isInteger( $value ) { + return ( is_int( $value ) || ctype_digit( $value ) ); + } +} diff --git a/includes/objectcache/DBABagOStuff.php b/includes/objectcache/DBABagOStuff.php index ade8c0a9..36ced496 100644 --- a/includes/objectcache/DBABagOStuff.php +++ b/includes/objectcache/DBABagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using DBA backend. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * Cache that uses DBA as a backend. @@ -7,23 +28,24 @@ * for systems that don't have it. * * On construction you can pass array( 'dir' => '/some/path' ); as a parameter - * to override the default DBA files directory (wgTmpDirectory). + * to override the default DBA files directory (wfTempDir()). * * @ingroup Cache */ class DBABagOStuff extends BagOStuff { var $mHandler, $mFile, $mReader, $mWriter, $mDisabled; + /** + * @param $params array + */ public function __construct( $params ) { global $wgDBAhandler; if ( !isset( $params['dir'] ) ) { - global $wgTmpDirectory; - $params['dir'] = $wgTmpDirectory; + $params['dir'] = wfTempDir(); } - $this->mFile = $params['dir']."/mw-cache-" . wfWikiID(); - $this->mFile .= '.db'; + $this->mFile = $params['dir'] . '/mw-cache-' . wfWikiID() . '.db'; wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" ); $this->mHandler = $wgDBAhandler; } @@ -35,7 +57,7 @@ class DBABagOStuff extends BagOStuff { * * @return string */ - function encode( $value, $expiry ) { + protected function encode( $value, $expiry ) { # Convert to absolute time $expiry = $this->convertExpiry( $expiry ); @@ -43,11 +65,12 @@ class DBABagOStuff extends BagOStuff { } /** + * @param $blob string * @return array list containing value first and expiry second */ - function decode( $blob ) { + protected function decode( $blob ) { if ( !is_string( $blob ) ) { - return array( null, 0 ); + return array( false, 0 ); } else { return array( unserialize( substr( $blob, 11 ) ), @@ -56,7 +79,10 @@ class DBABagOStuff extends BagOStuff { } } - function getReader() { + /** + * @return resource + */ + protected function getReader() { if ( file_exists( $this->mFile ) ) { $handle = dba_open( $this->mFile, 'rl', $this->mHandler ); } else { @@ -70,7 +96,10 @@ class DBABagOStuff extends BagOStuff { return $handle; } - function getWriter() { + /** + * @return resource + */ + protected function getWriter() { $handle = dba_open( $this->mFile, 'cl', $this->mHandler ); if ( !$handle ) { @@ -80,14 +109,18 @@ class DBABagOStuff extends BagOStuff { return $handle; } - function get( $key ) { + /** + * @param $key string + * @return mixed + */ + public function get( $key ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); $handle = $this->getReader(); if ( !$handle ) { wfProfileOut( __METHOD__ ); - return null; + return false; } $val = dba_fetch( $key, $handle ); @@ -96,20 +129,26 @@ class DBABagOStuff extends BagOStuff { # Must close ASAP because locks are held dba_close( $handle ); - if ( !is_null( $val ) && $expiry && $expiry < time() ) { + if ( $val !== false && $expiry && $expiry < time() ) { # Key is expired, delete it $handle = $this->getWriter(); dba_delete( $key, $handle ); dba_close( $handle ); wfDebug( __METHOD__ . ": $key expired\n" ); - $val = null; + $val = false; } wfProfileOut( __METHOD__ ); return $val; } - function set( $key, $value, $exptime = 0 ) { + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); @@ -128,7 +167,12 @@ class DBABagOStuff extends BagOStuff { return $ret; } - function delete( $key, $time = 0 ) { + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__ . "($key)\n" ); @@ -138,14 +182,20 @@ class DBABagOStuff extends BagOStuff { return false; } - $ret = dba_delete( $key, $handle ); + $ret = !dba_exists( $key, $handle ) || dba_delete( $key, $handle ); dba_close( $handle ); wfProfileOut( __METHOD__ ); return $ret; } - function add( $key, $value, $exptime = 0 ) { + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ + public function add( $key, $value, $exptime = 0 ) { wfProfileIn( __METHOD__ ); $blob = $this->encode( $value, $exptime ); @@ -163,7 +213,7 @@ class DBABagOStuff extends BagOStuff { if ( !$ret ) { list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); - if ( $expiry < time() ) { + if ( $expiry && $expiry < time() ) { # Yes expired, delete and try again dba_delete( $key, $handle ); $ret = dba_insert( $key, $blob, $handle ); @@ -177,6 +227,44 @@ class DBABagOStuff extends BagOStuff { return $ret; } + /** + * @param $key string + * @param $step integer + * @return integer|bool + */ + public function incr( $key, $step = 1 ) { + wfProfileIn( __METHOD__ ); + + $handle = $this->getWriter(); + + if ( !$handle ) { + wfProfileOut( __METHOD__ ); + return false; + } + + list( $value, $expiry ) = $this->decode( dba_fetch( $key, $handle ) ); + if ( $value !== false ) { + if ( $expiry && $expiry < time() ) { + # Key is expired, delete it + dba_delete( $key, $handle ); + wfDebug( __METHOD__ . ": $key expired\n" ); + $value = false; + } else { + $value += $step; + $blob = $this->encode( $value, $expiry ); + + $ret = dba_replace( $key, $blob, $handle ); + $value = $ret ? $value : false; + } + } + + dba_close( $handle ); + + wfProfileOut( __METHOD__ ); + + return ( $value === false ) ? false : (int)$value; + } + function keys() { $reader = $this->getReader(); $k1 = dba_firstkey( $reader ); @@ -196,4 +284,3 @@ class DBABagOStuff extends BagOStuff { return $result; } } - diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php index 75aad27a..f86cf157 100644 --- a/includes/objectcache/EhcacheBagOStuff.php +++ b/includes/objectcache/EhcacheBagOStuff.php @@ -1,8 +1,31 @@ <?php +/** + * Object caching using the Ehcache RESTful web service. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * Client for the Ehcache RESTful web service - http://ehcache.org/documentation/cache_server.html * TODO: Simplify configuration and add to the installer. + * + * @ingroup Cache */ class EhcacheBagOStuff extends BagOStuff { var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions, @@ -10,6 +33,9 @@ class EhcacheBagOStuff extends BagOStuff { var $curls = array(); + /** + * @param $params array + */ function __construct( $params ) { if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) { throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' ); @@ -36,6 +62,10 @@ class EhcacheBagOStuff extends BagOStuff { ); } + /** + * @param $key string + * @return bool|mixed + */ public function get( $key ) { wfProfileIn( __METHOD__ ); $response = $this->doItemRequest( $key ); @@ -70,6 +100,12 @@ class EhcacheBagOStuff extends BagOStuff { return $data; } + /** + * @param $key string + * @param $value mixed + * @param $expiry int + * @return bool + */ public function set( $key, $value, $expiry = 0 ) { wfProfileIn( __METHOD__ ); $expiry = $this->convertExpiry( $expiry ); @@ -107,6 +143,11 @@ class EhcacheBagOStuff extends BagOStuff { return $result; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { wfProfileIn( __METHOD__ ); $response = $this->doItemRequest( $key, @@ -122,6 +163,10 @@ class EhcacheBagOStuff extends BagOStuff { return $result; } + /** + * @param $key string + * @return string + */ protected function getCacheUrl( $key ) { if ( count( $this->servers ) == 1 ) { $server = reset( $this->servers ); @@ -149,6 +194,13 @@ class EhcacheBagOStuff extends BagOStuff { return $this->curls[$cacheUrl]; } + /** + * @param $key string + * @param $data + * @param $type + * @param $ttl + * @return int + */ protected function attemptPut( $key, $data, $type, $ttl ) { // In initial benchmarking, it was 30 times faster to use CURLOPT_POST // than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because @@ -173,6 +225,10 @@ class EhcacheBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return bool + */ protected function createCache( $key ) { wfDebug( __METHOD__.": creating cache for $key\n" ); $response = $this->doCacheRequest( $key, @@ -185,21 +241,26 @@ class EhcacheBagOStuff extends BagOStuff { wfDebug( __CLASS__.": failed to create cache for $key\n" ); return false; } - if ( $response['http_code'] == 201 /* created */ - || $response['http_code'] == 409 /* already there */ ) - { - return true; - } else { - return false; - } + return ( $response['http_code'] == 201 /* created */ + || $response['http_code'] == 409 /* already there */ ); } + /** + * @param $key string + * @param $curlOptions array + * @return array|bool|mixed + */ protected function doCacheRequest( $key, $curlOptions = array() ) { $cacheUrl = $this->getCacheUrl( $key ); $curl = $this->getCurl( $cacheUrl ); return $this->doRequest( $curl, $cacheUrl, $curlOptions ); } + /** + * @param $key string + * @param $curlOptions array + * @return array|bool|mixed + */ protected function doItemRequest( $key, $curlOptions = array() ) { $cacheUrl = $this->getCacheUrl( $key ); $curl = $this->getCurl( $cacheUrl ); @@ -207,6 +268,13 @@ class EhcacheBagOStuff extends BagOStuff { return $this->doRequest( $curl, $url, $curlOptions ); } + /** + * @param $curl + * @param $url string + * @param $curlOptions array + * @return array|bool|mixed + * @throws MWException + */ protected function doRequest( $curl, $url, $curlOptions = array() ) { if ( array_diff_key( $curlOptions, $this->curlOptions ) ) { // var_dump( array_diff_key( $curlOptions, $this->curlOptions ) ); diff --git a/includes/objectcache/EmptyBagOStuff.php b/includes/objectcache/EmptyBagOStuff.php index 2aee6b12..bd28b241 100644 --- a/includes/objectcache/EmptyBagOStuff.php +++ b/includes/objectcache/EmptyBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Dummy object caching. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * A BagOStuff object with no objects in it. Used to provide a no-op object to calling code. @@ -6,14 +27,30 @@ * @ingroup Cache */ class EmptyBagOStuff extends BagOStuff { + + /** + * @param $key string + * @return bool + */ function get( $key ) { return false; } + /** + * @param $key string + * @param $value mixed + * @param $exp int + * @return bool + */ function set( $key, $value, $exp = 0 ) { return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ function delete( $key, $time = 0 ) { return true; } diff --git a/includes/objectcache/HashBagOStuff.php b/includes/objectcache/HashBagOStuff.php index 36773306..799f26a3 100644 --- a/includes/objectcache/HashBagOStuff.php +++ b/includes/objectcache/HashBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using PHP arrays. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * This is a test of the interface, mainly. It stores things in an associative @@ -13,6 +34,10 @@ class HashBagOStuff extends BagOStuff { $this->bag = array(); } + /** + * @param $key string + * @return bool + */ protected function expire( $key ) { $et = $this->bag[$key][1]; @@ -25,6 +50,10 @@ class HashBagOStuff extends BagOStuff { return true; } + /** + * @param $key string + * @return bool|mixed + */ function get( $key ) { if ( !isset( $this->bag[$key] ) ) { return false; @@ -37,10 +66,22 @@ class HashBagOStuff extends BagOStuff { return $this->bag[$key][0]; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ function set( $key, $value, $exptime = 0 ) { $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) ); + return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ function delete( $key, $time = 0 ) { if ( !isset( $this->bag[$key] ) ) { return false; @@ -51,6 +92,9 @@ class HashBagOStuff extends BagOStuff { return true; } + /** + * @return array + */ function keys() { return array_keys( $this->bag ); } diff --git a/includes/objectcache/MemcachedBagOStuff.php b/includes/objectcache/MemcachedBagOStuff.php new file mode 100644 index 00000000..813c2727 --- /dev/null +++ b/includes/objectcache/MemcachedBagOStuff.php @@ -0,0 +1,180 @@ +<?php +/** + * Base class for memcached clients. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ + +/** + * Base class for memcached clients. + * + * @ingroup Cache + */ +class MemcachedBagOStuff extends BagOStuff { + protected $client; + + /** + * Fill in the defaults for any parameters missing from $params, using the + * backwards-compatible global variables + */ + protected function applyDefaultParams( $params ) { + if ( !isset( $params['servers'] ) ) { + $params['servers'] = $GLOBALS['wgMemCachedServers']; + } + if ( !isset( $params['debug'] ) ) { + $params['debug'] = $GLOBALS['wgMemCachedDebug']; + } + if ( !isset( $params['persistent'] ) ) { + $params['persistent'] = $GLOBALS['wgMemCachedPersistent']; + } + if ( !isset( $params['compress_threshold'] ) ) { + $params['compress_threshold'] = 1500; + } + if ( !isset( $params['timeout'] ) ) { + $params['timeout'] = $GLOBALS['wgMemCachedTimeout']; + } + if ( !isset( $params['connect_timeout'] ) ) { + $params['connect_timeout'] = 0.5; + } + return $params; + } + + /** + * @param $key string + * @return Mixed + */ + public function get( $key ) { + return $this->client->get( $this->encodeKey( $key ) ); + } + + /** + * @param $key string + * @param $value + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { + return $this->client->set( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { + return $this->client->delete( $this->encodeKey( $key ), $time ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime int (default 0) + * @return Mixed + */ + public function add( $key, $value, $exptime = 0 ) { + return $this->client->add( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime + * @return Mixed + */ + public function replace( $key, $value, $exptime = 0 ) { + return $this->client->replace( $this->encodeKey( $key ), $value, + $this->fixExpiry( $exptime ) ); + } + + /** + * Get the underlying client object. This is provided for debugging + * purposes. + */ + public function getClient() { + return $this->client; + } + + /** + * Encode a key for use on the wire inside the memcached protocol. + * + * We encode spaces and line breaks to avoid protocol errors. We encode + * the other control characters for compatibility with libmemcached + * verify_key. We leave other punctuation alone, to maximise backwards + * compatibility. + * @param $key string + * @return string + */ + public function encodeKey( $key ) { + return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/', + array( $this, 'encodeKeyCallback' ), $key ); + } + + /** + * @param $m array + * @return string + */ + protected function encodeKeyCallback( $m ) { + return rawurlencode( $m[0] ); + } + + /** + * TTLs higher than 30 days will be detected as absolute TTLs + * (UNIX timestamps), and will result in the cache entry being + * discarded immediately because the expiry is in the past. + * Clamp expiries >30d at 30d, unless they're >=1e9 in which + * case they are likely to really be absolute (1e9 = 2011-09-09) + */ + function fixExpiry( $expiry ) { + if ( $expiry > 2592000 && $expiry < 1000000000 ) { + $expiry = 2592000; + } + return $expiry; + } + + /** + * Decode a key encoded with encodeKey(). This is provided as a convenience + * function for debugging. + * + * @param $key string + * + * @return string + */ + public function decodeKey( $key ) { + return urldecode( $key ); + } + + /** + * Send a debug message to the log + */ + protected function debugLog( $text ) { + global $wgDebugLogGroups; + if( !isset( $wgDebugLogGroups['memcached'] ) ) { + # Prefix message since it will end up in main debug log file + $text = "memcached: $text"; + } + if ( substr( $text, -1 ) !== "\n" ) { + $text .= "\n"; + } + wfDebugLog( 'memcached', $text ); + } +} + diff --git a/includes/objectcache/MemcachedClient.php b/includes/objectcache/MemcachedClient.php index 868ad69f..536ba6ea 100644 --- a/includes/objectcache/MemcachedClient.php +++ b/includes/objectcache/MemcachedClient.php @@ -1,5 +1,7 @@ <?php /** + * Memcached client for PHP. + * * +---------------------------------------------------------------------------+ * | memcached client, PHP | * +---------------------------------------------------------------------------+ @@ -257,7 +259,7 @@ class MWMemcached { $this->_host_dead = array(); $this->_timeout_seconds = 0; - $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 100000; + $this->_timeout_microseconds = isset( $args['timeout'] ) ? $args['timeout'] : 500000; $this->_connect_timeout = isset( $args['connect_timeout'] ) ? $args['connect_timeout'] : 0.1; $this->_connect_attempts = 2; @@ -328,27 +330,36 @@ class MWMemcached { $this->stats['delete'] = 1; } $cmd = "delete $key $time\r\n"; - if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { - $this->_dead_sock( $sock ); + if( !$this->_fwrite( $sock, $cmd ) ) { return false; } - $res = trim( fgets( $sock ) ); + $res = $this->_fgets( $sock ); if ( $this->_debug ) { $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) ); } - if ( $res == "DELETED" ) { + if ( $res == "DELETED" || $res == "NOT_FOUND" ) { return true; } + return false; } + /** + * @param $key + * @param $timeout int + * @return bool + */ public function lock( $key, $timeout = 0 ) { /* stub */ return true; } + /** + * @param $key + * @return bool + */ public function unlock( $key ) { /* stub */ return true; @@ -427,8 +438,7 @@ class MWMemcached { } $cmd = "get $key\r\n"; - if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { - $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, $cmd ) ) { wfProfileOut( __METHOD__ ); return false; } @@ -471,7 +481,7 @@ class MWMemcached { $this->stats['get_multi'] = 1; } $sock_keys = array(); - + $socks = array(); foreach ( $keys as $key ) { $sock = $this->get_sock( $key ); if ( !is_resource( $sock ) ) { @@ -479,24 +489,23 @@ class MWMemcached { } $key = is_array( $key ) ? $key[1] : $key; if ( !isset( $sock_keys[$sock] ) ) { - $sock_keys[$sock] = array(); + $sock_keys[ intval( $sock ) ] = array(); $socks[] = $sock; } - $sock_keys[$sock][] = $key; + $sock_keys[ intval( $sock ) ][] = $key; } + $gather = array(); // Send out the requests foreach ( $socks as $sock ) { $cmd = 'get'; - foreach ( $sock_keys[$sock] as $key ) { + foreach ( $sock_keys[ intval( $sock ) ] as $key ) { $cmd .= ' ' . $key; } $cmd .= "\r\n"; - if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { + if ( $this->_fwrite( $sock, $cmd ) ) { $gather[] = $sock; - } else { - $this->_dead_sock( $sock ); } } @@ -559,12 +568,6 @@ class MWMemcached { * Passes through $cmd to the memcache server connected by $sock; returns * output as an array (null array if no output) * - * NOTE: due to a possible bug in how PHP reads while using fgets(), each - * line may not be terminated by a \r\n. More specifically, my testing - * has shown that, on FreeBSD at least, each line is terminated only - * with a \n. This is with the PHP flag auto_detect_line_endings set - * to falase (the default). - * * @param $sock Resource: socket to send command on * @param $cmd String: command to run * @@ -575,12 +578,13 @@ class MWMemcached { return array(); } - if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) { + if ( !$this->_fwrite( $sock, $cmd ) ) { return array(); } + $ret = array(); while ( true ) { - $res = fgets( $sock ); + $res = $this->_fgets( $sock ); $ret[] = $res; if ( preg_match( '/^END/', $res ) ) { break; @@ -717,15 +721,19 @@ class MWMemcached { wfRestoreWarnings(); } if ( !$sock ) { - if ( $this->_debug ) { - $this->_debugprint( "Error connecting to $host: $errstr\n" ); - } + $this->_error_log( "Error connecting to $host: $errstr\n" ); + $this->_dead_host( $host ); return false; } // Initialise timeout stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds ); + // If the connection was persistent, flush the read buffer in case there + // was a previous incomplete request on this connection + if ( $this->_persistent ) { + $this->_flush_read_buffer( $sock ); + } return true; } @@ -744,6 +752,9 @@ class MWMemcached { $this->_dead_host( $host ); } + /** + * @param $host + */ function _dead_host( $host ) { $parts = explode( ':', $host ); $ip = $parts[0]; @@ -769,13 +780,12 @@ class MWMemcached { } if ( $this->_single_sock !== null ) { - $this->_flush_read_buffer( $this->_single_sock ); return $this->sock_to_host( $this->_single_sock ); } $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key ); - if ( $this->_buckets === null ) { + $bu = array(); foreach ( $this->_servers as $v ) { if ( is_array( $v ) ) { for( $i = 0; $i < $v[1]; $i++ ) { @@ -794,7 +804,6 @@ class MWMemcached { $host = $this->_buckets[$hv % $this->_bucketcount]; $sock = $this->sock_to_host( $host ); if ( is_resource( $sock ) ) { - $this->_flush_read_buffer( $sock ); return $sock; } $hv = $this->_hashfunc( $hv . $realkey ); @@ -815,7 +824,7 @@ class MWMemcached { * @access private */ function _hashfunc( $key ) { - # Hash function must on [0,0x7ffffff] + # Hash function must be in [0,0x7ffffff] # We take the first 31 bits of the MD5 hash, which unlike the hash # function used in a previous version of this client, works return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; @@ -850,11 +859,11 @@ class MWMemcached { } else { $this->stats[$cmd] = 1; } - if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { - return $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) { + return null; } - $line = fgets( $sock ); + $line = $this->_fgets( $sock ); $match = array(); if ( !preg_match( '/^(\d+)/', $line, $match ) ) { return null; @@ -870,58 +879,42 @@ class MWMemcached { * * @param $sock Resource: socket to read from * @param $ret Array: returned values + * @return boolean True for success, false for failure * * @access private */ function _load_items( $sock, &$ret ) { while ( 1 ) { - $decl = fgets( $sock ); - if ( $decl == "END\r\n" ) { + $decl = $this->_fgets( $sock ); + if( $decl === false ) { + return false; + } elseif ( $decl == "END" ) { return true; - } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) { + } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)$/', $decl, $match ) ) { list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] ); - $bneed = $len + 2; - $offset = 0; - - while ( $bneed > 0 ) { - $data = fread( $sock, $bneed ); - $n = strlen( $data ); - if ( $n == 0 ) { - break; - } - $offset += $n; - $bneed -= $n; - if ( isset( $ret[$rkey] ) ) { - $ret[$rkey] .= $data; - } else { - $ret[$rkey] = $data; - } + $data = $this->_fread( $sock, $len + 2 ); + if ( $data === false ) { + return false; } - - if ( $offset != $len + 2 ) { - // Something is borked! - if ( $this->_debug ) { - $this->_debugprint( sprintf( "Something is borked! key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) ); - } - - unset( $ret[$rkey] ); - $this->_close_sock( $sock ); + if ( substr( $data, -2 ) !== "\r\n" ) { + $this->_handle_error( $sock, + 'line ending missing from data block from $1' ); return false; } + $data = substr( $data, 0, -2 ); + $ret[$rkey] = $data; if ( $this->_have_zlib && $flags & self::COMPRESSED ) { $ret[$rkey] = gzuncompress( $ret[$rkey] ); } - $ret[$rkey] = rtrim( $ret[$rkey] ); - if ( $flags & self::SERIALIZED ) { $ret[$rkey] = unserialize( $ret[$rkey] ); } } else { - $this->_debugprint( "Error parsing memcached response\n" ); - return 0; + $this->_handle_error( $sock, 'Error parsing response from $1' ); + return false; } } } @@ -960,15 +953,6 @@ class MWMemcached { $this->stats[$cmd] = 1; } - // TTLs higher than 30 days will be detected as absolute TTLs - // (UNIX timestamps), and will result in the cache entry being - // discarded immediately because the expiry is in the past. - // Clamp expiries >30d at 30d, unless they're >=1e9 in which - // case they are likely to really be absolute (1e9 = 2011-09-09) - if ( $exp > 2592000 && $exp < 1000000000 ) { - $exp = 2592000; - } - $flags = 0; if ( !is_scalar( $val ) ) { @@ -996,11 +980,11 @@ class MWMemcached { $flags |= self::COMPRESSED; } } - if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) { - return $this->_dead_sock( $sock ); + if ( !$this->_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) { + return false; } - $line = trim( fgets( $sock ) ); + $line = $this->_fgets( $sock ); if ( $this->_debug ) { $this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) ); @@ -1037,7 +1021,7 @@ class MWMemcached { } if ( !$this->_connect_sock( $sock, $host ) ) { - return $this->_dead_host( $host ); + return null; } // Do not buffer writes @@ -1048,49 +1032,136 @@ class MWMemcached { return $this->_cache_sock[$host]; } - function _debugprint( $str ) { - print( $str ); + /** + * @param $text string + */ + function _debugprint( $text ) { + global $wgDebugLogGroups; + if( !isset( $wgDebugLogGroups['memcached'] ) ) { + # Prefix message since it will end up in main debug log file + $text = "memcached: $text"; + } + wfDebugLog( 'memcached', $text ); + } + + /** + * @param $text string + */ + function _error_log( $text ) { + wfDebugLog( 'memcached-serious', "Memcached error: $text" ); } /** - * Write to a stream, timing out after the correct amount of time + * Write to a stream. If there is an error, mark the socket dead. * - * @return Boolean: false on failure, true on success + * @param $sock The socket + * @param $buf The string to write + * @return bool True on success, false on failure */ - /* - function _safe_fwrite( $f, $buf, $len = false ) { - stream_set_blocking( $f, 0 ); + function _fwrite( $sock, $buf ) { + $bytesWritten = 0; + $bufSize = strlen( $buf ); + while ( $bytesWritten < $bufSize ) { + $result = fwrite( $sock, $buf ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout writing to $1' ); + return false; + } + // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3. + if ( $result === false || $result === 0 ) { + $this->_handle_error( $sock, 'error writing to $1' ); + return false; + } + $bytesWritten += $result; + } - if ( $len === false ) { - wfDebug( "Writing " . strlen( $buf ) . " bytes\n" ); - $bytesWritten = fwrite( $f, $buf ); - } else { - wfDebug( "Writing $len bytes\n" ); - $bytesWritten = fwrite( $f, $buf, $len ); + return true; + } + + /** + * Handle an I/O error. Mark the socket dead and log an error. + */ + function _handle_error( $sock, $msg ) { + $peer = stream_socket_get_name( $sock, true /** remote **/ ); + if ( strval( $peer ) === '' ) { + $peer = array_search( $sock, $this->_cache_sock ); + if ( $peer === false ) { + $peer = '[unknown host]'; + } } - $n = stream_select( $r = null, $w = array( $f ), $e = null, 10, 0 ); - # $this->_timeout_seconds, $this->_timeout_microseconds ); + $msg = str_replace( '$1', $peer, $msg ); + $this->_error_log( "$msg\n" ); + $this->_dead_sock( $sock ); + } - wfDebug( "stream_select returned $n\n" ); - stream_set_blocking( $f, 1 ); - return $n == 1; - return $bytesWritten; - }*/ + /** + * Read the specified number of bytes from a stream. If there is an error, + * mark the socket dead. + * + * @param $sock The socket + * @param $len The number of bytes to read + * @return The string on success, false on failure. + */ + function _fread( $sock, $len ) { + $buf = ''; + while ( $len > 0 ) { + $result = fread( $sock, $len ); + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading buffer from $1' ); + return false; + } + if ( $result === '' ) { + // This will happen if the remote end of the socket is shut down + $this->_handle_error( $sock, 'unexpected end of file reading from $1' ); + return false; + } + $len -= strlen( $result ); + $buf .= $result; + } + return $buf; + } /** - * Original behaviour + * Read a line from a stream. If there is an error, mark the socket dead. + * The \r\n line ending is stripped from the response. + * + * @param $sock The socket + * @return The string on success, false on failure */ - function _safe_fwrite( $f, $buf, $len = false ) { - if ( $len === false ) { - $bytesWritten = fwrite( $f, $buf ); + function _fgets( $sock ) { + $result = fgets( $sock ); + // fgets() may return a partial line if there is a select timeout after + // a successful recv(), so we have to check for a timeout even if we + // got a string response. + $data = stream_get_meta_data( $sock ); + if ( $data['timed_out'] ) { + $this->_handle_error( $sock, 'timeout reading line from $1' ); + return false; + } + if ( $result === false ) { + $this->_handle_error( $sock, 'error reading line from $1' ); + return false; + } + if ( substr( $result, -2 ) === "\r\n" ) { + $result = substr( $result, 0, -2 ); + } elseif ( substr( $result, -1 ) === "\n" ) { + $result = substr( $result, 0, -1 ); } else { - $bytesWritten = fwrite( $f, $buf, $len ); + $this->_handle_error( $sock, 'line ending missing in response from $1' ); + return false; } - return $bytesWritten; + return $result; } /** * Flush the read buffer of a stream + * @param $f Resource */ function _flush_read_buffer( $f ) { if ( !is_resource( $f ) ) { @@ -1108,12 +1179,8 @@ class MWMemcached { // }}} } -// vim: sts=3 sw=3 et // }}} class MemCachedClientforWiki extends MWMemcached { - function _debugprint( $text ) { - wfDebug( "memcached: $text" ); - } } diff --git a/includes/objectcache/MemcachedPeclBagOStuff.php b/includes/objectcache/MemcachedPeclBagOStuff.php new file mode 100644 index 00000000..76886ebb --- /dev/null +++ b/includes/objectcache/MemcachedPeclBagOStuff.php @@ -0,0 +1,237 @@ +<?php +/** + * Object caching using memcached. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ + +/** + * A wrapper class for the PECL memcached client + * + * @ingroup Cache + */ +class MemcachedPeclBagOStuff extends MemcachedBagOStuff { + + /** + * Constructor + * + * Available parameters are: + * - servers: The list of IP:port combinations holding the memcached servers. + * - persistent: Whether to use a persistent connection + * - compress_threshold: The minimum size an object must be before it is compressed + * - timeout: The read timeout in microseconds + * - connect_timeout: The connect timeout in seconds + * - serializer: May be either "php" or "igbinary". Igbinary produces more compact + * values, but serialization is much slower unless the php.ini option + * igbinary.compact_strings is off. + */ + function __construct( $params ) { + $params = $this->applyDefaultParams( $params ); + + if ( $params['persistent'] ) { + // The pool ID must be unique to the server/option combination. + // The Memcached object is essentially shared for each pool ID. + // We can only resuse a pool ID if we keep the config consistent. + $this->client = new Memcached( md5( serialize( $params ) ) ); + if ( count( $this->client->getServerList() ) ) { + wfDebug( __METHOD__ . ": persistent Memcached object already loaded.\n" ); + return; // already initialized; don't add duplicate servers + } + } else { + $this->client = new Memcached; + } + + if ( !isset( $params['serializer'] ) ) { + $params['serializer'] = 'php'; + } + + // The compression threshold is an undocumented php.ini option for some + // reason. There's probably not much harm in setting it globally, for + // compatibility with the settings for the PHP client. + ini_set( 'memcached.compression_threshold', $params['compress_threshold'] ); + + // Set timeouts + $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 ); + $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] ); + $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] ); + $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 ); + + // Set libketama mode since it's recommended by the documentation and + // is as good as any. There's no way to configure libmemcached to use + // hashes identical to the ones currently in use by the PHP client, and + // even implementing one of the libmemcached hashes in pure PHP for + // forwards compatibility would require MWMemcached::get_sock() to be + // rewritten. + $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true ); + + // Set the serializer + switch ( $params['serializer'] ) { + case 'php': + $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP ); + break; + case 'igbinary': + if ( !Memcached::HAVE_IGBINARY ) { + throw new MWException( __CLASS__.': the igbinary extension is not available ' . + 'but igbinary serialization was requested.' ); + } + $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY ); + break; + default: + throw new MWException( __CLASS__.': invalid value for serializer parameter' ); + } + $servers = array(); + foreach ( $params['servers'] as $host ) { + $servers[] = IP::splitHostAndPort( $host ); // (ip, port) + } + $this->client->addServers( $servers ); + } + + /** + * @param $key string + * @return Mixed + */ + public function get( $key ) { + $this->debugLog( "get($key)" ); + return $this->checkResult( $key, parent::get( $key ) ); + } + + /** + * @param $key string + * @param $value + * @param $exptime int + * @return bool + */ + public function set( $key, $value, $exptime = 0 ) { + $this->debugLog( "set($key)" ); + return $this->checkResult( $key, parent::set( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $time int + * @return bool + */ + public function delete( $key, $time = 0 ) { + $this->debugLog( "delete($key)" ); + $result = parent::delete( $key, $time ); + if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) { + // "Not found" is counted as success in our interface + return true; + } else { + return $this->checkResult( $key, $result ); + } + } + + /** + * @param $key string + * @param $value int + * @param $exptime int + * @return Mixed + */ + public function add( $key, $value, $exptime = 0 ) { + $this->debugLog( "add($key)" ); + return $this->checkResult( $key, parent::add( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @param $exptime + * @return Mixed + */ + public function replace( $key, $value, $exptime = 0 ) { + $this->debugLog( "replace($key)" ); + return $this->checkResult( $key, parent::replace( $key, $value, $exptime ) ); + } + + /** + * @param $key string + * @param $value int + * @return Mixed + */ + public function incr( $key, $value = 1 ) { + $this->debugLog( "incr($key)" ); + $result = $this->client->increment( $key, $value ); + return $this->checkResult( $key, $result ); + } + + /** + * @param $key string + * @param $value int + * @return Mixed + */ + public function decr( $key, $value = 1 ) { + $this->debugLog( "decr($key)" ); + $result = $this->client->decrement( $key, $value ); + return $this->checkResult( $key, $result ); + } + + /** + * Check the return value from a client method call and take any necessary + * action. Returns the value that the wrapper function should return. At + * present, the return value is always the same as the return value from + * the client, but some day we might find a case where it should be + * different. + * + * @param $key string The key used by the caller, or false if there wasn't one. + * @param $result Mixed The return value + * @return Mixed + */ + protected function checkResult( $key, $result ) { + if ( $result !== false ) { + return $result; + } + switch ( $this->client->getResultCode() ) { + case Memcached::RES_SUCCESS: + break; + case Memcached::RES_DATA_EXISTS: + case Memcached::RES_NOTSTORED: + case Memcached::RES_NOTFOUND: + $this->debugLog( "result: " . $this->client->getResultMessage() ); + break; + default: + $msg = $this->client->getResultMessage(); + if ( $key !== false ) { + $server = $this->client->getServerByKey( $key ); + $serverName = "{$server['host']}:{$server['port']}"; + $msg = "Memcached error for key \"$key\" on server \"$serverName\": $msg"; + } else { + $msg = "Memcached error: $msg"; + } + wfDebugLog( 'memcached-serious', $msg ); + } + return $result; + } + + /** + * @param $keys Array + * @return Array + */ + public function getMulti( array $keys ) { + $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' ); + $callback = array( $this, 'encodeKey' ); + $result = $this->client->getMulti( array_map( $callback, $keys ) ); + return $this->checkResult( false, $result ); + } + + /* NOTE: there is no cas() method here because it is currently not supported + * by the BagOStuff interface and other BagOStuff subclasses, such as + * SqlBagOStuff. + */ +} diff --git a/includes/objectcache/MemcachedPhpBagOStuff.php b/includes/objectcache/MemcachedPhpBagOStuff.php index 14016683..a46dc716 100644 --- a/includes/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/objectcache/MemcachedPhpBagOStuff.php @@ -1,14 +1,32 @@ <?php +/** + * Object caching using memcached. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * A wrapper class for the pure-PHP memcached client, exposing a BagOStuff interface. + * + * @ingroup Cache */ -class MemcachedPhpBagOStuff extends BagOStuff { - - /** - * @var MemCachedClientforWiki - */ - protected $client; +class MemcachedPhpBagOStuff extends MemcachedBagOStuff { /** * Constructor. @@ -24,24 +42,7 @@ class MemcachedPhpBagOStuff extends BagOStuff { * @param $params array */ function __construct( $params ) { - if ( !isset( $params['servers'] ) ) { - $params['servers'] = $GLOBALS['wgMemCachedServers']; - } - if ( !isset( $params['debug'] ) ) { - $params['debug'] = $GLOBALS['wgMemCachedDebug']; - } - if ( !isset( $params['persistent'] ) ) { - $params['persistent'] = $GLOBALS['wgMemCachedPersistent']; - } - if ( !isset( $params['compress_threshold'] ) ) { - $params['compress_threshold'] = 1500; - } - if ( !isset( $params['timeout'] ) ) { - $params['timeout'] = $GLOBALS['wgMemCachedTimeout']; - } - if ( !isset( $params['connect_timeout'] ) ) { - $params['connect_timeout'] = 0.1; - } + $params = $this->applyDefaultParams( $params ); $this->client = new MemCachedClientforWiki( $params ); $this->client->set_servers( $params['servers'] ); @@ -56,36 +57,18 @@ class MemcachedPhpBagOStuff extends BagOStuff { } /** - * @param $key string - * @return Mixed - */ - public function get( $key ) { - return $this->client->get( $this->encodeKey( $key ) ); - } - - /** - * @param $key string - * @param $value - * @param $exptime int - * @return bool + * @param $keys Array + * @return Array */ - public function set( $key, $value, $exptime = 0 ) { - return $this->client->set( $this->encodeKey( $key ), $value, $exptime ); - } - - /** - * @param $key string - * @param $time int - * @return bool - */ - public function delete( $key, $time = 0 ) { - return $this->client->delete( $this->encodeKey( $key ), $time ); + public function getMulti( array $keys ) { + $callback = array( $this, 'encodeKey' ); + return $this->client->get_multi( array_map( $callback, $keys ) ); } /** * @param $key * @param $timeout int - * @return + * @return bool */ public function lock( $key, $timeout = 0 ) { return $this->client->lock( $this->encodeKey( $key ), $timeout ); @@ -98,26 +81,7 @@ class MemcachedPhpBagOStuff extends BagOStuff { public function unlock( $key ) { return $this->client->unlock( $this->encodeKey( $key ) ); } - - /** - * @param $key string - * @param $value int - * @return Mixed - */ - public function add( $key, $value, $exptime = 0 ) { - return $this->client->add( $this->encodeKey( $key ), $value, $exptime ); - } - - /** - * @param $key string - * @param $value int - * @param $exptime - * @return Mixed - */ - public function replace( $key, $value, $exptime = 0 ) { - return $this->client->replace( $this->encodeKey( $key ), $value, $exptime ); - } - + /** * @param $key string * @param $value int @@ -135,44 +99,5 @@ class MemcachedPhpBagOStuff extends BagOStuff { public function decr( $key, $value = 1 ) { return $this->client->decr( $this->encodeKey( $key ), $value ); } - - /** - * Get the underlying client object. This is provided for debugging - * purposes. - * - * @return MemCachedClientforWiki - */ - public function getClient() { - return $this->client; - } - - /** - * Encode a key for use on the wire inside the memcached protocol. - * - * We encode spaces and line breaks to avoid protocol errors. We encode - * the other control characters for compatibility with libmemcached - * verify_key. We leave other punctuation alone, to maximise backwards - * compatibility. - */ - public function encodeKey( $key ) { - return preg_replace_callback( '/[\x00-\x20\x25\x7f]+/', - array( $this, 'encodeKeyCallback' ), $key ); - } - - protected function encodeKeyCallback( $m ) { - return rawurlencode( $m[0] ); - } - - /** - * Decode a key encoded with encodeKey(). This is provided as a convenience - * function for debugging. - * - * @param $key string - * - * @return string - */ - public function decodeKey( $key ) { - return urldecode( $key ); - } } diff --git a/includes/objectcache/MultiWriteBagOStuff.php b/includes/objectcache/MultiWriteBagOStuff.php index 0d95a846..e496ddd8 100644 --- a/includes/objectcache/MultiWriteBagOStuff.php +++ b/includes/objectcache/MultiWriteBagOStuff.php @@ -1,9 +1,32 @@ <?php +/** + * Wrapper for object caching in different caches. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * A cache class that replicates all writes to multiple child caches. Reads * are implemented by reading from the caches in the order they are given in * the configuration until a cache gives a positive result. + * + * @ingroup Cache */ class MultiWriteBagOStuff extends BagOStuff { var $caches; @@ -11,11 +34,12 @@ class MultiWriteBagOStuff extends BagOStuff { /** * Constructor. Parameters are: * - * - caches: This should have a numbered array of cache parameter + * - caches: This should have a numbered array of cache parameter * structures, in the style required by $wgObjectCaches. See * the documentation of $wgObjectCaches for more detail. * * @param $params array + * @throws MWException */ public function __construct( $params ) { if ( !isset( $params['caches'] ) ) { @@ -28,10 +52,17 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $debug bool + */ public function setDebug( $debug ) { $this->doWrite( 'setDebug', $debug ); } + /** + * @param $key string + * @return bool|mixed + */ public function get( $key ) { foreach ( $this->caches as $cache ) { $value = $cache->get( $key ); @@ -42,30 +73,68 @@ class MultiWriteBagOStuff extends BagOStuff { return false; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { return $this->doWrite( 'set', $key, $value, $exptime ); } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { return $this->doWrite( 'delete', $key, $time ); } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function add( $key, $value, $exptime = 0 ) { return $this->doWrite( 'add', $key, $value, $exptime ); } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function replace( $key, $value, $exptime = 0 ) { return $this->doWrite( 'replace', $key, $value, $exptime ); } + /** + * @param $key string + * @param $value int + * @return bool|null + */ public function incr( $key, $value = 1 ) { return $this->doWrite( 'incr', $key, $value ); } + /** + * @param $key string + * @param $value int + * @return bool + */ public function decr( $key, $value = 1 ) { return $this->doWrite( 'decr', $key, $value ); - } + } + /** + * @param $key string + * @param $timeout int + * @return bool + */ public function lock( $key, $timeout = 0 ) { // Lock only the first cache, to avoid deadlocks if ( isset( $this->caches[0] ) ) { @@ -75,6 +144,10 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return bool + */ public function unlock( $key ) { if ( isset( $this->caches[0] ) ) { return $this->caches[0]->unlock( $key ); @@ -83,6 +156,10 @@ class MultiWriteBagOStuff extends BagOStuff { } } + /** + * @param $method string + * @return bool + */ protected function doWrite( $method /*, ... */ ) { $ret = true; $args = func_get_args(); @@ -97,9 +174,12 @@ class MultiWriteBagOStuff extends BagOStuff { } /** - * Delete objects expiring before a certain date. + * Delete objects expiring before a certain date. * * Succeed if any of the child caches succeed. + * @param $date string + * @param $progressCallback bool|callback + * @return bool */ public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { $ret = false; diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 77ca8371..9b360f32 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -1,19 +1,40 @@ <?php /** - * Functions to get cache objects + * Functions to get cache objects. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup Cache */ + +/** + * Functions to get cache objects + * + * @ingroup Cache + */ class ObjectCache { static $instances = array(); /** * Get a cached instance of the specified type of cache object. * - * @param $id + * @param $id string * - * @return object + * @return ObjectCache */ static function getInstance( $id ) { if ( isset( self::$instances[$id] ) ) { @@ -35,8 +56,9 @@ class ObjectCache { /** * Create a new cache object of the specified type. * - * @param $id + * @param $id string * + * @throws MWException * @return ObjectCache */ static function newFromId( $id ) { @@ -55,6 +77,7 @@ class ObjectCache { * * @param $params array * + * @throws MWException * @return ObjectCache */ static function newFromParams( $params ) { @@ -71,6 +94,15 @@ class ObjectCache { /** * Factory function referenced from DefaultSettings.php for CACHE_ANYTHING + * + * CACHE_ANYTHING means that stuff has to be cached, not caching is not an option. + * If a caching method is configured for any of the main caches ($wgMainCacheType, + * $wgMessageCacheType, $wgParserCacheType), then CACHE_ANYTHING will effectively + * be an alias to the configured cache choice for that. + * If no cache choice is configured (by default $wgMainCacheType is CACHE_NONE), + * then CACHE_ANYTHING will forward to CACHE_DB. + * @param $params array + * @return ObjectCache */ static function newAnything( $params ) { global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; @@ -86,6 +118,8 @@ class ObjectCache { /** * Factory function referenced from DefaultSettings.php for CACHE_ACCEL. * + * @param $params array + * @throws MWException * @return ObjectCache */ static function newAccelerator( $params ) { @@ -104,8 +138,10 @@ class ObjectCache { /** * Factory function that creates a memcached client object. - * The idea of this is that it might eventually detect and automatically - * support the PECL extension, assuming someone can get it to compile. + * + * This always uses the PHP client, since the PECL client has a different + * hashing scheme and a different interpretation of the flags bitfield, so + * switching between the two clients randomly would be disasterous. * * @param $params array * diff --git a/includes/objectcache/ObjectCacheSessionHandler.php b/includes/objectcache/ObjectCacheSessionHandler.php new file mode 100644 index 00000000..f55da94d --- /dev/null +++ b/includes/objectcache/ObjectCacheSessionHandler.php @@ -0,0 +1,145 @@ +<?php +/** + * Session storage in object cache. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ + +/** + * Session storage in object cache. + * Used if $wgSessionsInObjectCache is true. + * + * @ingroup Cache + */ +class ObjectCacheSessionHandler { + /** + * Install a session handler for the current web request + */ + static function install() { + session_set_save_handler( + array( __CLASS__, 'open' ), + array( __CLASS__, 'close' ), + array( __CLASS__, 'read' ), + array( __CLASS__, 'write' ), + array( __CLASS__, 'destroy' ), + array( __CLASS__, 'gc' ) ); + + // It's necessary to register a shutdown function to call session_write_close(), + // because by the time the request shutdown function for the session module is + // called, $wgMemc has already been destroyed. Shutdown functions registered + // this way are called before object destruction. + register_shutdown_function( array( __CLASS__, 'handleShutdown' ) ); + } + + /** + * Get the cache storage object to use for session storage + */ + static function getCache() { + global $wgSessionCacheType; + return ObjectCache::getInstance( $wgSessionCacheType ); + } + + /** + * Get a cache key for the given session id. + * + * @param $id String: session id + * @return String: cache key + */ + static function getKey( $id ) { + return wfMemcKey( 'session', $id ); + } + + /** + * Callback when opening a session. + * + * @param $save_path String: path used to store session files, unused + * @param $session_name String: session name + * @return Boolean: success + */ + static function open( $save_path, $session_name ) { + return true; + } + + /** + * Callback when closing a session. + * NOP. + * + * @return Boolean: success + */ + static function close() { + return true; + } + + /** + * Callback when reading session data. + * + * @param $id String: session id + * @return Mixed: session data + */ + static function read( $id ) { + $data = self::getCache()->get( self::getKey( $id ) ); + if( $data === false ) { + return ''; + } + return $data; + } + + /** + * Callback when writing session data. + * + * @param $id String: session id + * @param $data Mixed: session data + * @return Boolean: success + */ + static function write( $id, $data ) { + global $wgObjectCacheSessionExpiry; + self::getCache()->set( self::getKey( $id ), $data, $wgObjectCacheSessionExpiry ); + return true; + } + + /** + * Callback to destroy a session when calling session_destroy(). + * + * @param $id String: session id + * @return Boolean: success + */ + static function destroy( $id ) { + self::getCache()->delete( self::getKey( $id ) ); + return true; + } + + /** + * Callback to execute garbage collection. + * NOP: Object caches perform garbage collection implicitly + * + * @param $maxlifetime Integer: maximum session life time + * @return Boolean: success + */ + static function gc( $maxlifetime ) { + return true; + } + + /** + * Shutdown function. See the comment inside ObjectCacheSessionHandler::install + * for rationale. + */ + static function handleShutdown() { + session_write_close(); + } +} diff --git a/includes/objectcache/RedisBagOStuff.php b/includes/objectcache/RedisBagOStuff.php new file mode 100644 index 00000000..c5966cdb --- /dev/null +++ b/includes/objectcache/RedisBagOStuff.php @@ -0,0 +1,413 @@ +<?php +/** + * Object caching using Redis (http://redis.io/). + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + + +class RedisBagOStuff extends BagOStuff { + protected $connectTimeout, $persistent, $password, $automaticFailover; + + /** + * A list of server names, from $params['servers'] + */ + protected $servers; + + /** + * A cache of Redis objects, representing connections to Redis servers. + * The key is the server name. + */ + protected $conns = array(); + + /** + * An array listing "dead" servers which have had a connection error in + * the past. Servers are marked dead for a limited period of time, to + * avoid excessive overhead from repeated connection timeouts. The key in + * the array is the server name, the value is the UNIX timestamp at which + * the server is resurrected. + */ + protected $deadServers = array(); + + /** + * Construct a RedisBagOStuff object. Parameters are: + * + * - servers: An array of server names. A server name may be a hostname, + * a hostname/port combination or the absolute path of a UNIX socket. + * If a hostname is specified but no port, the standard port number + * 6379 will be used. Required. + * + * - connectTimeout: The timeout for new connections, in seconds. Optional, + * default is 1 second. + * + * - persistent: Set this to true to allow connections to persist across + * multiple web requests. False by default. + * + * - password: The authentication password, will be sent to Redis in + * clear text. Optional, if it is unspecified, no AUTH command will be + * sent. + * + * - automaticFailover: If this is false, then each key will be mapped to + * a single server, and if that server is down, any requests for that key + * will fail. If this is true, a connection failure will cause the client + * to immediately try the next server in the list (as determined by a + * consistent hashing algorithm). True by default. This has the + * potential to create consistency issues if a server is slow enough to + * flap, for example if it is in swap death. + */ + function __construct( $params ) { + if ( !extension_loaded( 'redis' ) ) { + throw new MWException( __CLASS__. ' requires the phpredis extension: ' . + 'https://github.com/nicolasff/phpredis' ); + } + + $this->servers = $params['servers']; + $this->connectTimeout = isset( $params['connectTimeout'] ) + ? $params['connectTimeout'] : 1; + $this->persistent = !empty( $params['persistent'] ); + if ( isset( $params['password'] ) ) { + $this->password = $params['password']; + } + if ( isset( $params['automaticFailover'] ) ) { + $this->automaticFailover = $params['automaticFailover']; + } else { + $this->automaticFailover = true; + } + } + + public function get( $key ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + try { + $result = $conn->get( $key ); + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'get', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function set( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + $expiry = $this->convertToRelative( $expiry ); + try { + if ( !$expiry ) { + // No expiry, that is very different from zero expiry in Redis + $result = $conn->set( $key, $value ); + } else { + $result = $conn->setex( $key, $expiry, $value ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'set', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function delete( $key, $time = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + try { + $conn->delete( $key ); + // Return true even if the key didn't exist + $result = true; + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'delete', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function getMulti( array $keys ) { + wfProfileIn( __METHOD__ ); + $batches = array(); + $conns = array(); + foreach ( $keys as $key ) { + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + continue; + } + $conns[$server] = $conn; + $batches[$server][] = $key; + } + $result = array(); + foreach ( $batches as $server => $batchKeys ) { + $conn = $conns[$server]; + try { + $conn->multi( Redis::PIPELINE ); + foreach ( $batchKeys as $key ) { + $conn->get( $key ); + } + $batchResult = $conn->exec(); + if ( $batchResult === false ) { + $this->debug( "multi request to $server failed" ); + continue; + } + foreach ( $batchResult as $i => $value ) { + if ( $value !== false ) { + $result[$batchKeys[$i]] = $value; + } + } + } catch ( RedisException $e ) { + $this->handleException( $server, $e ); + } + } + + $this->debug( "getMulti for " . count( $keys ) . " keys " . + "returned " . count( $result ) . " results" ); + wfProfileOut( __METHOD__ ); + return $result; + } + + public function add( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + $expiry = $this->convertToRelative( $expiry ); + try { + $result = $conn->setnx( $key, $value ); + if ( $result && $expiry ) { + $conn->expire( $key, $expiry ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + $this->logRequest( 'add', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Non-atomic implementation of replace(). Could perhaps be done atomically + * with WATCH or scripting, but this function is rarely used. + */ + public function replace( $key, $value, $expiry = 0 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + if ( !$conn->exists( $key ) ) { + wfProfileOut( __METHOD__ ); + return false; + } + + $expiry = $this->convertToRelative( $expiry ); + try { + if ( !$expiry ) { + $result = $conn->set( $key, $value ); + } else { + $result = $conn->setex( $key, $expiry, $value ); + } + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'replace', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Non-atomic implementation of incr(). + * + * Probably all callers actually want incr() to atomically initialise + * values to zero if they don't exist, as provided by the Redis INCR + * command. But we are constrained by the memcached-like interface to + * return null in that case. Once the key exists, further increments are + * atomic. + */ + public function incr( $key, $value = 1 ) { + wfProfileIn( __METHOD__ ); + list( $server, $conn ) = $this->getConnection( $key ); + if ( !$conn ) { + wfProfileOut( __METHOD__ ); + return false; + } + if ( !$conn->exists( $key ) ) { + wfProfileOut( __METHOD__ ); + return null; + } + try { + $result = $conn->incrBy( $key, $value ); + } catch ( RedisException $e ) { + $result = false; + $this->handleException( $server, $e ); + } + + $this->logRequest( 'incr', $key, $server, $result ); + wfProfileOut( __METHOD__ ); + return $result; + } + + /** + * Get a Redis object with a connection suitable for fetching the specified key + */ + protected function getConnection( $key ) { + if ( count( $this->servers ) === 1 ) { + $candidates = $this->servers; + } else { + // Use consistent hashing + $hashes = array(); + foreach ( $this->servers as $server ) { + $hashes[$server] = md5( $server . '/' . $key ); + } + asort( $hashes ); + if ( !$this->automaticFailover ) { + reset( $hashes ); + $candidates = array( key( $hashes ) ); + } else { + $candidates = array_keys( $hashes ); + } + } + + foreach ( $candidates as $server ) { + $conn = $this->getConnectionToServer( $server ); + if ( $conn ) { + return array( $server, $conn ); + } + } + return array( false, false ); + } + + /** + * Get a connection to the server with the specified name. Connections + * are cached, and failures are persistent to avoid multiple timeouts. + * + * @return Redis object, or false on failure + */ + protected function getConnectionToServer( $server ) { + if ( isset( $this->deadServers[$server] ) ) { + $now = time(); + if ( $now > $this->deadServers[$server] ) { + // Dead time expired + unset( $this->deadServers[$server] ); + } else { + // Server is dead + $this->debug( "server $server is marked down for another " . + ($this->deadServers[$server] - $now ) . + " seconds, can't get connection" ); + return false; + } + } + + if ( isset( $this->conns[$server] ) ) { + return $this->conns[$server]; + } + + if ( substr( $server, 0, 1 ) === '/' ) { + // UNIX domain socket + // These are required by the redis extension to start with a slash, but + // we still need to set the port to a special value to make it work. + $host = $server; + $port = 0; + } else { + // TCP connection + $hostPort = IP::splitHostAndPort( $server ); + if ( !$hostPort ) { + throw new MWException( __CLASS__.": invalid configured server \"$server\"" ); + } + list( $host, $port ) = $hostPort; + if ( $port === false ) { + $port = 6379; + } + } + $conn = new Redis; + try { + if ( $this->persistent ) { + $this->debug( "opening persistent connection to $host:$port" ); + $result = $conn->pconnect( $host, $port, $this->connectTimeout ); + } else { + $this->debug( "opening non-persistent connection to $host:$port" ); + $result = $conn->connect( $host, $port, $this->connectTimeout ); + } + if ( !$result ) { + $this->logError( "could not connect to server $server" ); + // Mark server down for 30s to avoid further timeouts + $this->deadServers[$server] = time() + 30; + return false; + } + if ( $this->password !== null ) { + if ( !$conn->auth( $this->password ) ) { + $this->logError( "authentication error connecting to $server" ); + } + } + } catch ( RedisException $e ) { + $this->deadServers[$server] = time() + 30; + wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" ); + return false; + } + + $conn->setOption( Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP ); + $this->conns[$server] = $conn; + return $conn; + } + + /** + * Log a fatal error + */ + protected function logError( $msg ) { + wfDebugLog( 'redis', "Redis error: $msg\n" ); + } + + /** + * The redis extension throws an exception in response to various read, write + * and protocol errors. Sometimes it also closes the connection, sometimes + * not. The safest response for us is to explicitly destroy the connection + * object and let it be reopened during the next request. + */ + protected function handleException( $server, $e ) { + wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" ); + unset( $this->conns[$server] ); + } + + /** + * Send information about a single request to the debug log + */ + public function logRequest( $method, $key, $server, $result ) { + $this->debug( "$method $key on $server: " . + ( $result === false ? "failure" : "success" ) ); + } +} + diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index 93d22f11..54051dc1 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using a SQL database. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * Class to store objects in the database @@ -6,7 +27,6 @@ * @ingroup Cache */ class SqlBagOStuff extends BagOStuff { - /** * @var LoadBalancer */ @@ -22,6 +42,9 @@ class SqlBagOStuff extends BagOStuff { var $shards = 1; var $tableName = 'objectcache'; + protected $connFailureTime = 0; // UNIX timestamp + protected $connFailureError; // exception + /** * Constructor. Parameters are: * - server: A server info structure in the format required by each @@ -66,25 +89,40 @@ class SqlBagOStuff extends BagOStuff { * @return DatabaseBase */ protected function getDB() { + global $wgDebugDBTransactions; + + # Don't keep timing out trying to connect for each call if the DB is down + if ( $this->connFailureError && ( time() - $this->connFailureTime ) < 60 ) { + throw $this->connFailureError; + } + if ( !isset( $this->db ) ) { # If server connection info was given, use that if ( $this->serverInfo ) { + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf( "Using provided serverInfo for SqlBagOStuff\n" ) ); + } $this->lb = new LoadBalancer( array( 'servers' => array( $this->serverInfo ) ) ); $this->db = $this->lb->getConnection( DB_MASTER ); $this->db->clearFlag( DBO_TRX ); } else { - # We must keep a separate connection to MySQL in order to avoid deadlocks - # However, SQLite has an opposite behaviour. - # @todo Investigate behaviour for other databases - if ( wfGetDB( DB_MASTER )->getType() == 'sqlite' ) { - $this->db = wfGetDB( DB_MASTER ); - } else { + /* + * We must keep a separate connection to MySQL in order to avoid deadlocks + * However, SQLite has an opposite behaviour. And PostgreSQL needs to know + * if we are in transaction or no + */ + if ( wfGetDB( DB_MASTER )->getType() == 'mysql' ) { $this->lb = wfGetLBFactory()->newMainLB(); $this->db = $this->lb->getConnection( DB_MASTER ); - $this->db->clearFlag( DBO_TRX ); + $this->db->clearFlag( DBO_TRX ); // auto-commit mode + } else { + $this->db = wfGetDB( DB_MASTER ); } } + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf( "Connection %s will be used for SqlBagOStuff\n", $this->db ) ); + } } return $this->db; @@ -92,6 +130,8 @@ class SqlBagOStuff extends BagOStuff { /** * Get the table name for a given key + * @param $key string + * @return string */ protected function getTableByKey( $key ) { if ( $this->shards > 1 ) { @@ -104,6 +144,8 @@ class SqlBagOStuff extends BagOStuff { /** * Get the table name for a given shard index + * @param $index int + * @return string */ protected function getTableByShard( $index ) { if ( $this->shards > 1 ) { @@ -115,61 +157,103 @@ class SqlBagOStuff extends BagOStuff { } } + /** + * @param $key string + * @return mixed + */ public function get( $key ) { - # expire old entries if any - $this->garbageCollect(); - $db = $this->getDB(); - $tableName = $this->getTableByKey( $key ); - $row = $db->selectRow( $tableName, array( 'value', 'exptime' ), - array( 'keyname' => $key ), __METHOD__ ); + $values = $this->getMulti( array( $key ) ); + return array_key_exists( $key, $values ) ? $values[$key] : false; + } - if ( !$row ) { - $this->debug( 'get: no matching rows' ); - return false; - } + /** + * @param $keys array + * @return Array + */ + public function getMulti( array $keys ) { + $values = array(); // array of (key => value) - $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); + try { + $db = $this->getDB(); + $keysByTableName = array(); + foreach ( $keys as $key ) { + $tableName = $this->getTableByKey( $key ); + if ( !isset( $keysByTableName[$tableName] ) ) { + $keysByTableName[$tableName] = array(); + } + $keysByTableName[$tableName][] = $key; + } - if ( $this->isExpired( $row->exptime ) ) { - $this->debug( "get: key has expired, deleting" ); - try { - $db->begin( __METHOD__ ); - # Put the expiry time in the WHERE condition to avoid deleting a - # newly-inserted value - $db->delete( $tableName, - array( - 'keyname' => $key, - 'exptime' => $row->exptime - ), __METHOD__ ); - $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { - $this->handleWriteError( $e ); + $this->garbageCollect(); // expire old entries if any + + $dataRows = array(); + foreach ( $keysByTableName as $tableName => $tableKeys ) { + $res = $db->select( $tableName, + array( 'keyname', 'value', 'exptime' ), + array( 'keyname' => $tableKeys ), + __METHOD__ ); + foreach ( $res as $row ) { + $dataRows[$row->keyname] = $row; + } } - return false; - } + foreach ( $keys as $key ) { + if ( isset( $dataRows[$key] ) ) { // HIT? + $row = $dataRows[$key]; + $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); + if ( $this->isExpired( $row->exptime ) ) { // MISS + $this->debug( "get: key has expired, deleting" ); + try { + $db->begin( __METHOD__ ); + # Put the expiry time in the WHERE condition to avoid deleting a + # newly-inserted value + $db->delete( $this->getTableByKey( $key ), + array( 'keyname' => $key, 'exptime' => $row->exptime ), + __METHOD__ ); + $db->commit( __METHOD__ ); + } catch ( DBQueryError $e ) { + $this->handleWriteError( $e ); + } + $values[$key] = false; + } else { // HIT + $values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) ); + } + } else { // MISS + $values[$key] = false; + $this->debug( 'get: no matching rows' ); + } + } + } catch ( DBError $e ) { + $this->handleReadError( $e ); + }; - return $this->unserialize( $db->decodeBlob( $row->value ) ); + return $values; } + /** + * @param $key string + * @param $value mixed + * @param $exptime int + * @return bool + */ public function set( $key, $value, $exptime = 0 ) { - $db = $this->getDB(); - $exptime = intval( $exptime ); - - if ( $exptime < 0 ) { - $exptime = 0; - } + try { + $db = $this->getDB(); + $exptime = intval( $exptime ); - if ( $exptime == 0 ) { - $encExpiry = $this->getMaxDateTime(); - } else { - if ( $exptime < 3.16e8 ) { # ~10 years - $exptime += time(); + if ( $exptime < 0 ) { + $exptime = 0; } - $encExpiry = $db->timestamp( $exptime ); - } - try { + if ( $exptime == 0 ) { + $encExpiry = $this->getMaxDateTime(); + } else { + if ( $exptime < 3.16e8 ) { # ~10 years + $exptime += time(); + } + + $encExpiry = $db->timestamp( $exptime ); + } $db->begin( __METHOD__ ); // (bug 24425) use a replace if the db supports it instead of // delete/insert to avoid clashes with conflicting keynames @@ -182,40 +266,46 @@ class SqlBagOStuff extends BagOStuff { 'exptime' => $encExpiry ), __METHOD__ ); $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return false; } return true; } + /** + * @param $key string + * @param $time int + * @return bool + */ public function delete( $key, $time = 0 ) { - $db = $this->getDB(); - try { + $db = $this->getDB(); $db->begin( __METHOD__ ); $db->delete( $this->getTableByKey( $key ), array( 'keyname' => $key ), __METHOD__ ); $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return false; } return true; } + /** + * @param $key string + * @param $step int + * @return int|null + */ public function incr( $key, $step = 1 ) { - $db = $this->getDB(); - $tableName = $this->getTableByKey( $key ); - $step = intval( $step ); - try { + $db = $this->getDB(); + $tableName = $this->getTableByKey( $key ); + $step = intval( $step ); $db->begin( __METHOD__ ); $row = $db->selectRow( $tableName, @@ -251,34 +341,47 @@ class SqlBagOStuff extends BagOStuff { $newValue = null; } $db->commit( __METHOD__ ); - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); - return null; } return $newValue; } + /** + * @return Array + */ public function keys() { - $db = $this->getDB(); $result = array(); - for ( $i = 0; $i < $this->shards; $i++ ) { - $res = $db->select( $this->getTableByShard( $i ), - array( 'keyname' ), false, __METHOD__ ); - foreach ( $res as $row ) { - $result[] = $row->keyname; + try { + $db = $this->getDB(); + for ( $i = 0; $i < $this->shards; $i++ ) { + $res = $db->select( $this->getTableByShard( $i ), + array( 'keyname' ), false, __METHOD__ ); + foreach ( $res as $row ) { + $result[] = $row->keyname; + } } + } catch ( DBError $e ) { + $this->handleReadError( $e ); } return $result; } + /** + * @param $exptime string + * @return bool + */ protected function isExpired( $exptime ) { return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time(); } + /** + * @return string + */ protected function getMaxDateTime() { if ( time() > 0x7fffffff ) { return $this->getDB()->timestamp( 1 << 62 ); @@ -310,14 +413,16 @@ class SqlBagOStuff extends BagOStuff { /** * Delete objects from the database which expire before a certain date. + * @param $timestamp string + * @param $progressCallback bool|callback + * @return bool */ public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) { - $db = $this->getDB(); - $dbTimestamp = $db->timestamp( $timestamp ); - $totalSeconds = false; - $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) ); - try { + $db = $this->getDB(); + $dbTimestamp = $db->timestamp( $timestamp ); + $totalSeconds = false; + $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) ); for ( $i = 0; $i < $this->shards; $i++ ) { $maxExpTime = false; while ( true ) { @@ -325,7 +430,7 @@ class SqlBagOStuff extends BagOStuff { if ( $maxExpTime !== false ) { $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime ); } - $rows = $db->select( + $rows = $db->select( $this->getTableByShard( $i ), array( 'keyname', 'exptime' ), $conds, @@ -349,7 +454,7 @@ class SqlBagOStuff extends BagOStuff { $db->begin( __METHOD__ ); $db->delete( $this->getTableByShard( $i ), - array( + array( 'exptime >= ' . $db->addQuotes( $minExpTime ), 'exptime < ' . $db->addQuotes( $dbTimestamp ), 'keyname' => $keys @@ -361,36 +466,40 @@ class SqlBagOStuff extends BagOStuff { if ( intval( $totalSeconds ) === 0 ) { $percent = 0; } else { - $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp ) + $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp ) - wfTimestamp( TS_UNIX, $maxExpTime ); if ( $remainingSeconds > $totalSeconds ) { $totalSeconds = $remainingSeconds; } - $percent = ( $i + $remainingSeconds / $totalSeconds ) + $percent = ( $i + $remainingSeconds / $totalSeconds ) / $this->shards * 100; } call_user_func( $progressCallback, $percent ); } } } - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); + return false; } + return true; } public function deleteAll() { - $db = $this->getDB(); - try { + $db = $this->getDB(); for ( $i = 0; $i < $this->shards; $i++ ) { $db->begin( __METHOD__ ); $db->delete( $this->getTableByShard( $i ), '*', __METHOD__ ); $db->commit( __METHOD__ ); } - } catch ( DBQueryError $e ) { + } catch ( DBError $e ) { $this->handleWriteError( $e ); + return false; } + + return true; } /** @@ -433,23 +542,40 @@ class SqlBagOStuff extends BagOStuff { } /** - * Handle a DBQueryError which occurred during a write operation. - * Ignore errors which are due to a read-only database, rethrow others. + * Handle a DBError which occurred during a read operation. */ - protected function handleWriteError( $exception ) { - $db = $this->getDB(); - - if ( !$db->wasReadOnlyError() ) { - throw $exception; + protected function handleReadError( DBError $exception ) { + if ( $exception instanceof DBConnectionError ) { + $this->connFailureTime = time(); + $this->connFailureError = $exception; } - - try { - $db->rollback(); - } catch ( DBQueryError $e ) { + wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" ); + if ( $this->db ) { + wfDebug( __METHOD__ . ": ignoring query error\n" ); + } else { + wfDebug( __METHOD__ . ": ignoring connection error\n" ); } + } - wfDebug( __METHOD__ . ": ignoring query error\n" ); - $db->ignoreErrors( false ); + /** + * Handle a DBQueryError which occurred during a write operation. + */ + protected function handleWriteError( DBError $exception ) { + if ( $exception instanceof DBConnectionError ) { + $this->connFailureTime = time(); + $this->connFailureError = $exception; + } + if ( $this->db && $this->db->wasReadOnlyError() ) { + try { + $this->db->rollback( __METHOD__ ); + } catch ( DBError $e ) {} + } + wfDebugLog( 'SQLBagOStuff', "DBError: {$exception->getMessage()}" ); + if ( $this->db ) { + wfDebug( __METHOD__ . ": ignoring query error\n" ); + } else { + wfDebug( __METHOD__ . ": ignoring connection error\n" ); + } } /** diff --git a/includes/objectcache/WinCacheBagOStuff.php b/includes/objectcache/WinCacheBagOStuff.php index 7f464946..21aa39e7 100644 --- a/includes/objectcache/WinCacheBagOStuff.php +++ b/includes/objectcache/WinCacheBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using WinCache. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * Wrapper for WinCache object caching functions; identical interface @@ -53,6 +74,9 @@ class WinCacheBagOStuff extends BagOStuff { return true; } + /** + * @return Array + */ public function keys() { $info = wincache_ucache_info(); $list = $info['ucache_entries']; diff --git a/includes/objectcache/XCacheBagOStuff.php b/includes/objectcache/XCacheBagOStuff.php index 0ddf1245..bc68b596 100644 --- a/includes/objectcache/XCacheBagOStuff.php +++ b/includes/objectcache/XCacheBagOStuff.php @@ -1,4 +1,25 @@ <?php +/** + * Object caching using XCache. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Cache + */ /** * Wrapper for XCache object caching functions; identical interface @@ -17,7 +38,13 @@ class XCacheBagOStuff extends BagOStuff { $val = xcache_get( $key ); if ( is_string( $val ) ) { - $val = unserialize( $val ); + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } + } elseif ( is_null( $val ) ) { + return false; } return $val; @@ -32,7 +59,11 @@ class XCacheBagOStuff extends BagOStuff { * @return bool */ public function set( $key, $value, $expire = 0 ) { - xcache_set( $key, serialize( $value ), $expire ); + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + xcache_set( $key, $value, $expire ); return true; } @@ -47,5 +78,12 @@ class XCacheBagOStuff extends BagOStuff { xcache_unset( $key ); return true; } -} + public function incr( $key, $value = 1 ) { + return xcache_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return xcache_dec( $key, $value ); + } +} |