MediaWiki  REL1_21
EhcacheBagOStuff.php
Go to the documentation of this file.
00001 <?php
00030 class EhcacheBagOStuff extends BagOStuff {
00031         var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions,
00032                 $requestData, $requestDataPos;
00033 
00034         var $curls = array();
00035 
00040         function __construct( $params ) {
00041                 if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) {
00042                         throw new MWException( __CLASS__ . ' requires curl version 7.16.2 or later.' );
00043                 }
00044                 if ( !extension_loaded( 'zlib' ) ) {
00045                         throw new MWException( __CLASS__ . ' requires the zlib extension' );
00046                 }
00047                 if ( !isset( $params['servers'] ) ) {
00048                         throw new MWException( __METHOD__ . ': servers parameter is required' );
00049                 }
00050                 $this->servers = $params['servers'];
00051                 $this->cacheName = isset( $params['cache'] ) ? $params['cache'] : 'mw';
00052                 $this->connectTimeout = isset( $params['connectTimeout'] )
00053                         ? $params['connectTimeout'] : 1;
00054                 $this->timeout = isset( $params['timeout'] ) ? $params['timeout'] : 1;
00055                 $this->curlOptions = array(
00056                         CURLOPT_CONNECTTIMEOUT_MS => intval( $this->connectTimeout * 1000 ),
00057                         CURLOPT_TIMEOUT_MS => intval( $this->timeout * 1000 ),
00058                         CURLOPT_RETURNTRANSFER => 1,
00059                         CURLOPT_CUSTOMREQUEST => 'GET',
00060                         CURLOPT_POST => 0,
00061                         CURLOPT_POSTFIELDS => '',
00062                         CURLOPT_HTTPHEADER => array(),
00063                 );
00064         }
00065 
00071         public function get( $key, &$casToken = null ) {
00072                 wfProfileIn( __METHOD__ );
00073                 $response = $this->doItemRequest( $key );
00074                 if ( !$response || $response['http_code'] == 404 ) {
00075                         wfProfileOut( __METHOD__ );
00076                         return false;
00077                 }
00078                 if ( $response['http_code'] >= 300 ) {
00079                         wfDebug( __METHOD__ . ": GET failure, got HTTP {$response['http_code']}\n" );
00080                         wfProfileOut( __METHOD__ );
00081                         return false;
00082                 }
00083                 $body = $response['body'];
00084                 $type = $response['content_type'];
00085                 if ( $type == 'application/vnd.php.serialized+deflate' ) {
00086                         $body = gzinflate( $body );
00087                         if ( !$body ) {
00088                                 wfDebug( __METHOD__ . ": error inflating $key\n" );
00089                                 wfProfileOut( __METHOD__ );
00090                                 return false;
00091                         }
00092                         $data = unserialize( $body );
00093                 } elseif ( $type == 'application/vnd.php.serialized' ) {
00094                         $data = unserialize( $body );
00095                 } else {
00096                         wfDebug( __METHOD__ . ": unknown content type \"$type\"\n" );
00097                         wfProfileOut( __METHOD__ );
00098                         return false;
00099                 }
00100 
00101                 $casToken = $body;
00102 
00103                 wfProfileOut( __METHOD__ );
00104                 return $data;
00105         }
00106 
00113         public function set( $key, $value, $expiry = 0 ) {
00114                 wfProfileIn( __METHOD__ );
00115                 $expiry = $this->convertExpiry( $expiry );
00116                 $ttl = $expiry ? $expiry - time() : 2147483647;
00117                 $blob = serialize( $value );
00118                 if ( strlen( $blob ) > 100 ) {
00119                         $blob = gzdeflate( $blob );
00120                         $contentType = 'application/vnd.php.serialized+deflate';
00121                 } else {
00122                         $contentType = 'application/vnd.php.serialized';
00123                 }
00124 
00125                 $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
00126 
00127                 if ( $code == 404 ) {
00128                         // Maybe the cache does not exist yet, let's try creating it
00129                         if ( !$this->createCache( $key ) ) {
00130                                 wfDebug( __METHOD__ . ": cache creation failed\n" );
00131                                 wfProfileOut( __METHOD__ );
00132                                 return false;
00133                         }
00134                         $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
00135                 }
00136 
00137                 $result = false;
00138                 if ( !$code ) {
00139                         wfDebug( __METHOD__ . ": PUT failure for key $key\n" );
00140                 } elseif ( $code >= 300 ) {
00141                         wfDebug( __METHOD__ . ": PUT failure for key $key: HTTP $code\n" );
00142                 } else {
00143                         $result = true;
00144                 }
00145 
00146                 wfProfileOut( __METHOD__ );
00147                 return $result;
00148         }
00149 
00157         public function cas( $casToken, $key, $value, $exptime = 0 ) {
00158                 // Not sure if we can implement CAS for ehcache. There appears to be CAS-support per
00159                 // http://ehcache.org/documentation/get-started/consistency-options#cas-cache-operations,
00160                 // but I can't find any docs for our current implementation.
00161                 throw new MWException( "CAS is not implemented in " . __CLASS__ );
00162         }
00163 
00169         public function delete( $key, $time = 0 ) {
00170                 wfProfileIn( __METHOD__ );
00171                 $response = $this->doItemRequest( $key,
00172                         array( CURLOPT_CUSTOMREQUEST => 'DELETE' ) );
00173                 $code = isset( $response['http_code'] ) ? $response['http_code'] : 0;
00174                 if ( !$response || ( $code != 404 && $code >= 300 ) ) {
00175                         wfDebug( __METHOD__ . ": DELETE failure for key $key\n" );
00176                         $result = false;
00177                 } else {
00178                         $result = true;
00179                 }
00180                 wfProfileOut( __METHOD__ );
00181                 return $result;
00182         }
00183 
00188         public function merge( $key, closure $callback, $exptime = 0, $attempts = 10 ) {
00189                 return $this->mergeViaLock( $key, $callback, $exptime, $attempts );
00190         }
00191 
00196         protected function getCacheUrl( $key ) {
00197                 if ( count( $this->servers ) == 1 ) {
00198                         $server = reset( $this->servers );
00199                 } else {
00200                         // Use consistent hashing
00201                         $hashes = array();
00202                         foreach ( $this->servers as $server ) {
00203                                 $hashes[$server] = md5( $server . '/' . $key );
00204                         }
00205                         asort( $hashes );
00206                         reset( $hashes );
00207                         $server = key( $hashes );
00208                 }
00209                 return "http://$server/ehcache/rest/{$this->cacheName}";
00210         }
00211 
00216         protected function getCurl( $cacheUrl ) {
00217                 if ( !isset( $this->curls[$cacheUrl] ) ) {
00218                         $this->curls[$cacheUrl] = curl_init();
00219                 }
00220                 return $this->curls[$cacheUrl];
00221         }
00222 
00230         protected function attemptPut( $key, $data, $type, $ttl ) {
00231                 // In initial benchmarking, it was 30 times faster to use CURLOPT_POST
00232                 // than CURLOPT_UPLOAD with CURLOPT_READFUNCTION. This was because
00233                 // CURLOPT_UPLOAD was pushing the request headers first, then waiting
00234                 // for an ACK packet, then sending the data, whereas CURLOPT_POST just
00235                 // sends the headers and the data in a single send().
00236                 $response = $this->doItemRequest( $key,
00237                         array(
00238                                 CURLOPT_POST => 1,
00239                                 CURLOPT_CUSTOMREQUEST => 'PUT',
00240                                 CURLOPT_POSTFIELDS => $data,
00241                                 CURLOPT_HTTPHEADER => array(
00242                                         'Content-Type: ' . $type,
00243                                         'ehcacheTimeToLiveSeconds: ' . $ttl
00244                                 )
00245                         )
00246                 );
00247                 if ( !$response ) {
00248                         return 0;
00249                 } else {
00250                         return $response['http_code'];
00251                 }
00252         }
00253 
00258         protected function createCache( $key ) {
00259                 wfDebug( __METHOD__ . ": creating cache for $key\n" );
00260                 $response = $this->doCacheRequest( $key,
00261                         array(
00262                                 CURLOPT_POST => 1,
00263                                 CURLOPT_CUSTOMREQUEST => 'PUT',
00264                                 CURLOPT_POSTFIELDS => '',
00265                         ) );
00266                 if ( !$response ) {
00267                         wfDebug( __CLASS__ . ": failed to create cache for $key\n" );
00268                         return false;
00269                 }
00270                 return ( $response['http_code'] == 201 /* created */
00271                         || $response['http_code'] == 409 /* already there */ );
00272         }
00273 
00279         protected function doCacheRequest( $key, $curlOptions = array() ) {
00280                 $cacheUrl = $this->getCacheUrl( $key );
00281                 $curl = $this->getCurl( $cacheUrl );
00282                 return $this->doRequest( $curl, $cacheUrl, $curlOptions );
00283         }
00284 
00290         protected function doItemRequest( $key, $curlOptions = array() ) {
00291                 $cacheUrl = $this->getCacheUrl( $key );
00292                 $curl = $this->getCurl( $cacheUrl );
00293                 $url = $cacheUrl . '/' . rawurlencode( $key );
00294                 return $this->doRequest( $curl, $url, $curlOptions );
00295         }
00296 
00304         protected function doRequest( $curl, $url, $curlOptions = array() ) {
00305                 if ( array_diff_key( $curlOptions, $this->curlOptions ) ) {
00306                         // var_dump( array_diff_key( $curlOptions, $this->curlOptions ) );
00307                         throw new MWException( __METHOD__ . ": to prevent options set in one doRequest() " .
00308                                 "call from affecting subsequent doRequest() calls, only options listed " .
00309                                 "in \$this->curlOptions may be specified in the \$curlOptions parameter." );
00310                 }
00311                 $curlOptions += $this->curlOptions;
00312                 $curlOptions[CURLOPT_URL] = $url;
00313 
00314                 curl_setopt_array( $curl, $curlOptions );
00315                 $result = curl_exec( $curl );
00316                 if ( $result === false ) {
00317                         wfDebug( __CLASS__ . ": curl error: " . curl_error( $curl ) . "\n" );
00318                         return false;
00319                 }
00320                 $info = curl_getinfo( $curl );
00321                 $info['body'] = $result;
00322                 return $info;
00323         }
00324 }