* Added an Ehcache client.
authorTim Starling <tstarling@users.mediawiki.org>
Fri, 4 Mar 2011 06:01:30 +0000 (06:01 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Fri, 4 Mar 2011 06:01:30 +0000 (06:01 +0000)
* Fixed encoding of spaces in memcached keys, in r83140 they would have been encoded as "+".

includes/AutoLoader.php
includes/objectcache/EhcacheBagOStuff.php [new file with mode: 0644]
includes/objectcache/MemcachedPhpBagOStuff.php

index 961f180..1fe21fd 100644 (file)
@@ -505,6 +505,7 @@ $wgAutoloadLocalClasses = array(
        'BagOStuff' => 'includes/objectcache/BagOStuff.php',
        'DBABagOStuff' => 'includes/objectcache/DBABagOStuff.php',
        'eAccelBagOStuff' => 'includes/objectcache/eAccelBagOStuff.php',
+       'EhcacheBagOStuff' => 'includes/objectcache/EhcacheBagOStuff.php',
        'EmptyBagOStuff' => 'includes/objectcache/EmptyBagOStuff.php',
        'FakeMemCachedClient' => 'includes/objectcache/EmptyBagOStuff.php',
        'HashBagOStuff' => 'includes/objectcache/HashBagOStuff.php',
diff --git a/includes/objectcache/EhcacheBagOStuff.php b/includes/objectcache/EhcacheBagOStuff.php
new file mode 100644 (file)
index 0000000..2ac013a
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+
+class EhcacheBagOStuff extends BagOStuff {
+       var $servers, $cacheName, $connectTimeout, $timeout, $curlOptions, 
+               $requestData, $requestDataPos;
+       
+       var $curls = array();
+
+       function __construct( $params ) {
+               if ( !defined( 'CURLOPT_TIMEOUT_MS' ) ) {
+                       throw new MWException( __CLASS__.' requires curl version 7.16.2 or later.' );
+               }
+               if ( !extension_loaded( 'zlib' ) ) {
+                       throw new MWException( __CLASS__.' requires the zlib extension' );
+               }
+               if ( !isset( $params['servers'] ) ) {
+                       throw new MWException( __METHOD__.': servers parameter is required' );
+               }
+               $this->servers = $params['servers'];
+               $this->cacheName = isset( $params['cache'] ) ? $params['cache'] : 'mw';
+               $this->connectTimeout = isset( $params['connectTimeout'] ) 
+                       ? $params['connectTimeout'] : 1;
+               $this->timeout = isset( $params['timeout'] ) ? $params['timeout'] : 1;
+               $this->curlOptions = array(
+                       CURLOPT_CONNECTTIMEOUT_MS => intval( $this->connectTimeout * 1000 ),
+                       CURLOPT_TIMEOUT_MS => intval( $this->timeout * 1000 ),
+                       CURLOPT_RETURNTRANSFER => 1,
+                       CURLOPT_CUSTOMREQUEST => 'GET',
+                       CURLOPT_POST => 0,
+                       CURLOPT_POSTFIELDS => '',
+                       CURLOPT_HTTPHEADER => array(),
+               );
+       }
+
+       public function get( $key ) {
+               wfProfileIn( __METHOD__ );
+               $response = $this->doItemRequest( $key );
+               if ( !$response || $response['http_code'] == 404 ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               }
+               if ( $response['http_code'] >= 300 ) {
+                       wfDebug( __METHOD__.": GET failure, got HTTP {$response['http_code']}\n" );
+                       wfProfileOut( __METHOD__ );
+                       return false;                   
+               }
+               $body = $response['body'];
+               $type = $response['content_type'];
+               if ( $type == 'application/vnd.php.serialized+deflate' ) {
+                       $body = gzinflate( $body );
+                       if ( !$body ) {
+                               wfDebug( __METHOD__.": error inflating $key\n" );
+                               wfProfileOut( __METHOD__ );
+                               return false;
+                       }
+                       $data = unserialize( $body );
+               } elseif ( $type == 'application/vnd.php.serialized' ) {
+                       $data = unserialize( $body );
+               } else {
+                       wfDebug( __METHOD__.": unknown content type \"$type\"\n" );
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $data;
+       }
+
+       public function set( $key, $value, $expiry = 0 ) {
+               wfProfileIn( __METHOD__ );
+               $expiry = $this->convertExpiry( $expiry );
+               $ttl = $expiry ? $expiry - time() : 2147483647;
+               $blob = serialize( $value );
+               if ( strlen( $blob ) > 100 ) {
+                       $blob = gzdeflate( $blob );
+                       $contentType = 'application/vnd.php.serialized+deflate';
+               } else {
+                       $contentType = 'application/vnd.php.serialized';
+               }
+
+               $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
+
+               if ( $code == 404 ) {
+                       // Maybe the cache does not exist yet, let's try creating it
+                       if ( !$this->createCache( $key ) ) {
+                               wfDebug( __METHOD__.": cache creation failed\n" );
+                               wfProfileOut( __METHOD__ );
+                               return false;
+                       }
+                       $code = $this->attemptPut( $key, $blob, $contentType, $ttl );
+               }
+
+               $result = false;
+               if ( !$code ) {
+                       wfDebug( __METHOD__.": PUT failure for key $key\n" );
+               } elseif ( $code >= 300 ) {
+                       wfDebug( __METHOD__.": PUT failure for key $key: HTTP $code\n" );
+               } else {
+                       $result = true;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $result;
+       }
+
+       public function delete( $key, $time = 0 ) {
+               wfProfileIn( __METHOD__ );
+               $response = $this->doItemRequest( $key,
+                       array( CURLOPT_CUSTOMREQUEST => 'DELETE' ) );
+               $code = isset( $response['http_code'] ) ? $response['http_code'] : 0;
+               if ( !$response || ( $code != 404 && $code >= 300 ) ) {
+                       wfDebug( __METHOD__.": DELETE failure for key $key\n" );
+                       $result = false;
+               } else {
+                       $result = true;
+               }
+               wfProfileOut( __METHOD__ );
+               return $result;
+       }
+
+       protected function getCacheUrl( $key ) {
+               if ( count( $this->servers ) == 1 ) {
+                       $server = reset( $this->servers );
+               } else {
+                       // Use consistent hashing
+                       $hashes = array();
+                       foreach ( $this->servers as $server ) {
+                               $hashes[$server] = md5( $server . '/' . $key );
+                       }
+                       asort( $hashes );
+                       reset( $hashes );
+                       $server = key( $hashes );
+               }
+               return "http://$server/ehcache/rest/{$this->cacheName}";
+       }
+
+       /**
+        * Get a cURL handle for the given cache URL.
+        * We cache the handles to allow keepalive.
+        */
+       protected function getCurl( $cacheUrl ) {
+               if ( !isset( $this->curls[$cacheUrl] ) ) {
+                       $this->curls[$cacheUrl] = curl_init();
+               }
+               return $this->curls[$cacheUrl];
+       }
+
+       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
+               // CURLOPT_UPLOAD was pushing the request headers first, then waiting 
+               // for an ACK packet, then sending the data, whereas CURLOPT_POST just
+               // sends the headers and the data in a single send().
+               $response = $this->doItemRequest( $key,
+                       array(
+                               CURLOPT_POST => 1,
+                               CURLOPT_CUSTOMREQUEST => 'PUT',
+                               CURLOPT_POSTFIELDS => $data,
+                               CURLOPT_HTTPHEADER => array(
+                                       'Content-Type: ' . $type,
+                                       'ehcacheTimeToLiveSeconds: ' . $ttl
+                               )
+                       )
+               );
+               if ( !$response ) {
+                       return 0;
+               } else {
+                       return $response['http_code'];
+               }
+       }
+
+       protected function createCache( $key ) {
+               wfDebug( __METHOD__.": creating cache for $key\n" );
+               $response = $this->doCacheRequest( $key, 
+                       array(
+                               CURLOPT_POST => 1,
+                               CURLOPT_CUSTOMREQUEST => 'PUT',
+                               CURLOPT_POSTFIELDS => '',
+                       ) );
+               if ( !$response ) {
+                       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;
+               }                       
+       }
+
+       protected function doCacheRequest( $key, $curlOptions = array() ) {
+               $cacheUrl = $this->getCacheUrl( $key );
+               $curl = $this->getCurl( $cacheUrl );
+               return $this->doRequest( $curl, $cacheUrl, $curlOptions );
+       }
+
+       protected function doItemRequest( $key, $curlOptions = array() ) {
+               $cacheUrl = $this->getCacheUrl( $key );
+               $curl = $this->getCurl( $cacheUrl );
+               $url = $cacheUrl . '/' . rawurlencode( $key );
+               return $this->doRequest( $curl, $url, $curlOptions );
+       }
+
+       protected function doRequest( $curl, $url, $curlOptions = array() ) {
+               if ( array_diff_key( $curlOptions, $this->curlOptions ) ) {
+                       var_dump( array_diff_key( $curlOptions, $this->curlOptions ) );
+                       throw new MWException( __METHOD__.": to prevent options set in one doRequest() " .
+                               "call from affecting subsequent doRequest() calls, only options listed " . 
+                               "in \$this->curlOptions may be specified in the \$curlOptions parameter." );
+               }
+               $curlOptions += $this->curlOptions;
+               $curlOptions[CURLOPT_URL] = $url;
+
+               curl_setopt_array( $curl, $curlOptions );
+               $result = curl_exec( $curl );
+               if ( $result === false ) {
+                       wfDebug( __CLASS__.": curl error: " . curl_error( $curl ) . "\n" );
+                       return false;
+               }
+               $info = curl_getinfo( $curl );
+               $info['body'] = $result;
+               return $info;
+       }
+}
index 4538bb0..7bac319 100644 (file)
@@ -102,7 +102,7 @@ class MemcachedPhpBagOStuff extends BagOStuff {
        }
 
        protected function encodeKeyCallback( $m ) {
-               return urlencode( $m[0] );
+               return rawurlencode( $m[0] );
        }
 
        /**