MediaWiki  REL1_24
BloomCacheRedis.php
Go to the documentation of this file.
00001 <?php
00034 class BloomCacheRedis extends BloomCache {
00036     protected $redisPool;
00038     protected $lockMgr;
00040     protected $servers;
00042     protected $segments = 128;
00043 
00051     public function __construct( array $config ) {
00052         parent::__construct( $config );
00053 
00054         $redisConf = $config['redisConfig'];
00055         $redisConf['serializer'] = 'none'; // manage that in this class
00056         $this->redisPool = RedisConnectionPool::singleton( $redisConf );
00057         $this->servers = $config['redisServers'];
00058         $this->lockMgr = new RedisLockManager( array(
00059             'lockServers'  => array( 'srv1' => $this->servers[0] ),
00060             'srvsByBucket' => array( 0 => array( 'srv1' ) ),
00061             'redisConfig'  => $config['redisConfig']
00062         ) );
00063     }
00064 
00065     protected function doInit( $key, $size, $precision ) {
00066         $conn = $this->getConnection( 'master' );
00067         if ( !$conn ) {
00068             return false;
00069         }
00070 
00071         // 80000000 items at p = .001 take up 500MB and fit into one value.
00072         // Do not hit the 512MB redis value limit by reducing the demands.
00073         $size = min( $size, 80000000 * $this->segments );
00074         $precision = max( round( $precision, 3 ), .001 );
00075         $epoch = microtime( true );
00076 
00077         static $script =
00078 <<<LUA
00079         local kMetadata, kData = unpack(KEYS)
00080         local aEntries, aPrec, aEpoch = unpack(ARGV)
00081         if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then
00082             redis.call('DEL',kMetadata)
00083             redis.call('HSET',kMetadata,'entries',aEntries)
00084             redis.call('HSET',kMetadata,'precision',aPrec)
00085             redis.call('HSET',kMetadata,'epoch',aEpoch)
00086             redis.call('SET',kData,'')
00087             return 1
00088         end
00089         return 0
00090 LUA;
00091 
00092         $res = false;
00093         try {
00094             $conn->script( 'load', $script );
00095             $conn->multi( Redis::MULTI );
00096             for ( $i = 0; $i < $this->segments; ++$i ) {
00097                 $res = $conn->luaEval( $script,
00098                     array(
00099                         "$key:$i:bloom-metadata", # KEYS[1]
00100                         "$key:$i:bloom-data", # KEYS[2]
00101                         ceil( $size / $this->segments ), # ARGV[1]
00102                         $precision, # ARGV[2]
00103                         $epoch # ARGV[3]
00104                     ),
00105                     2 # number of first argument(s) that are keys
00106                 );
00107             }
00108             $results = $conn->exec();
00109             $res = $results && !in_array( false, $results, true );
00110         } catch ( RedisException $e ) {
00111             $this->handleException( $conn, $e );
00112         }
00113 
00114         return ( $res !== false );
00115     }
00116 
00117     protected function doAdd( $key, array $members ) {
00118         $conn = $this->getConnection( 'master' );
00119         if ( !$conn ) {
00120             return false;
00121         }
00122 
00123         static $script =
00124 <<<LUA
00125         local kMetadata, kData = unpack(KEYS)
00126         local aMember = unpack(ARGV)
00127 
00128         -- Check if the filter was initialized
00129         if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then
00130             return false
00131         end
00132 
00133         -- Initial expected entries and desired precision
00134         local entries = 1*redis.call('HGET',kMetadata,'entries')
00135         local precision = 1*redis.call('HGET',kMetadata,'precision')
00136         local hash = redis.sha1hex(aMember)
00137 
00138         -- Based on the math from: http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives
00139         -- 0.480453013 = ln(2)^2
00140         local bits = math.ceil((entries * math.log(precision)) / -0.480453013)
00141 
00142         -- 0.693147180 = ln(2)
00143         local k = math.floor(0.693147180 * bits / entries)
00144 
00145         -- This uses a variation on:
00146         -- 'Less Hashing, Same Performance: Building a Better Bloom Filter'
00147         -- http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf
00148         local h = { }
00149         h[0] = tonumber(string.sub(hash, 1, 8 ), 16)
00150         h[1] = tonumber(string.sub(hash, 9, 16), 16)
00151         h[2] = tonumber(string.sub(hash, 17, 24), 16)
00152         h[3] = tonumber(string.sub(hash, 25, 32), 16)
00153 
00154         for i=1, k do
00155             local pos = (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % bits
00156             redis.call('SETBIT', kData, pos, 1)
00157         end
00158 
00159         return 1
00160 LUA;
00161 
00162         $res = false;
00163         try {
00164             $conn->script( 'load', $script );
00165             $conn->multi( Redis::PIPELINE );
00166             foreach ( $members as $member ) {
00167                 $i = $this->getSegment( $member );
00168                 $conn->luaEval( $script,
00169                     array(
00170                         "$key:$i:bloom-metadata", # KEYS[1],
00171                         "$key:$i:bloom-data", # KEYS[2]
00172                         $member # ARGV[1]
00173                     ),
00174                     2 # number of first argument(s) that are keys
00175                 );
00176             }
00177             $results = $conn->exec();
00178             $res = $results && !in_array( false, $results, true );
00179         } catch ( RedisException $e ) {
00180             $this->handleException( $conn, $e );
00181         }
00182 
00183         if ( $res === false ) {
00184             wfDebug( "Could not add to the '$key' bloom filter; it may be missing." );
00185         }
00186 
00187         return ( $res !== false );
00188     }
00189 
00190     protected function doSetStatus( $virtualKey, array $values ) {
00191         $conn = $this->getConnection( 'master' );
00192         if ( !$conn ) {
00193             return null;
00194         }
00195 
00196         $res = false;
00197         try {
00198             $res = $conn->hMSet( "$virtualKey:filter-metadata", $values );
00199         } catch ( RedisException $e ) {
00200             $this->handleException( $conn, $e );
00201         }
00202 
00203         return ( $res !== false );
00204     }
00205 
00206     protected function doGetStatus( $virtualKey ) {
00207         $conn = $this->getConnection( 'slave' );
00208         if ( !$conn ) {
00209             return false;
00210         }
00211 
00212         $res = false;
00213         try {
00214             $res = $conn->hGetAll( "$virtualKey:filter-metadata" );
00215         } catch ( RedisException $e ) {
00216             $this->handleException( $conn, $e );
00217         }
00218 
00219         if ( is_array( $res ) ) {
00220             $res['lastID'] = isset( $res['lastID'] ) ? $res['lastID'] : null;
00221             $res['asOfTime'] = isset( $res['asOfTime'] ) ? $res['asOfTime'] : null;
00222             $res['epoch'] = isset( $res['epoch'] ) ? $res['epoch'] : null;
00223         }
00224 
00225         return $res;
00226     }
00227 
00228     protected function doIsHit( $key, $member ) {
00229         $conn = $this->getConnection( 'slave' );
00230         if ( !$conn ) {
00231             return null;
00232         }
00233 
00234         static $script =
00235 <<<LUA
00236         local kMetadata, kData = unpack(KEYS)
00237         local aMember = unpack(ARGV)
00238 
00239         -- Check if the filter was initialized
00240         if redis.call('EXISTS',kMetadata) == 0 or redis.call('EXISTS',kData) == 0 then
00241             return false
00242         end
00243 
00244         -- Initial expected entries and desired precision.
00245         -- This determines the size of the first and subsequent filters.
00246         local entries = redis.call('HGET',kMetadata,'entries')
00247         local precision = redis.call('HGET',kMetadata,'precision')
00248         local hash = redis.sha1hex(aMember)
00249 
00250         -- This uses a variation on:
00251         -- 'Less Hashing, Same Performance: Building a Better Bloom Filter'
00252         -- http://www.eecs.harvard.edu/~kirsch/pubs/bbbf/esa06.pdf
00253         local h = { }
00254         h[0] = tonumber(string.sub(hash, 1, 8 ), 16)
00255         h[1] = tonumber(string.sub(hash, 9, 16), 16)
00256         h[2] = tonumber(string.sub(hash, 17, 24), 16)
00257         h[3] = tonumber(string.sub(hash, 25, 32), 16)
00258 
00259         -- 0.480453013 = ln(2)^2
00260         local bits = math.ceil((entries * math.log(precision)) / -0.480453013)
00261 
00262         -- 0.693147180 = ln(2)
00263         local k = math.floor(0.693147180 * bits / entries)
00264 
00265         local found = 1
00266         for i=1, k do
00267             local pos = (h[i % 2] + i * h[2 + (((i + (i % 2)) % 4) / 2)]) % bits
00268             if redis.call('GETBIT', kData, pos) == 0 then
00269                 found = 0
00270                 break
00271             end
00272         end
00273 
00274         return found
00275 LUA;
00276 
00277         $res = null;
00278         try {
00279             $i = $this->getSegment( $member );
00280             $res = $conn->luaEval( $script,
00281                 array(
00282                     "$key:$i:bloom-metadata", # KEYS[1],
00283                     "$key:$i:bloom-data", # KEYS[2]
00284                     $member # ARGV[1]
00285                 ),
00286                 2 # number of first argument(s) that are keys
00287             );
00288         } catch ( RedisException $e ) {
00289             $this->handleException( $conn, $e );
00290         }
00291 
00292         return is_int( $res ) ? (bool)$res : null;
00293     }
00294 
00295     protected function doDelete( $key ) {
00296         $conn = $this->getConnection( 'master' );
00297         if ( !$conn ) {
00298             return false;
00299         }
00300 
00301         $res = false;
00302         try {
00303             $keys = array();
00304             for ( $i = 0; $i < $this->segments; ++$i ) {
00305                 $keys[] = "$key:$i:bloom-metadata";
00306                 $keys[] = "$key:$i:bloom-data";
00307             }
00308             $res = $conn->delete( $keys );
00309         } catch ( RedisException $e ) {
00310             $this->handleException( $conn, $e );
00311         }
00312 
00313         return ( $res !== false );
00314     }
00315 
00316     public function getScopedLock( $virtualKey ) {
00317         $status = Status::newGood();
00318         return ScopedLock::factory( $this->lockMgr,
00319             array( $virtualKey ), LockManager::LOCK_EX, $status );
00320     }
00321 
00326     protected function getSegment( $member ) {
00327         return hexdec( substr( md5( $member ), 0, 2 ) ) % $this->segments;
00328     }
00329 
00334     protected function getConnection( $to ) {
00335         if ( $to === 'master' ) {
00336             $conn = $this->redisPool->getConnection( $this->servers[0] );
00337         } else {
00338             static $lastServer = null;
00339 
00340             $conn = false;
00341             if ( $lastServer ) {
00342                 $conn = $this->redisPool->getConnection( $lastServer );
00343                 if ( $conn ) {
00344                     return $conn; // reuse connection
00345                 }
00346             }
00347             $servers = $this->servers;
00348             $attempts = min( 3, count( $servers ) );
00349             for ( $i = 1; $i <= $attempts; ++$i ) {
00350                 $index = mt_rand( 0, count( $servers ) - 1 );
00351                 $conn = $this->redisPool->getConnection( $servers[$index] );
00352                 if ( $conn ) {
00353                     $lastServer = $servers[$index];
00354                     return $conn;
00355                 }
00356                 unset( $servers[$index] ); // skip next time
00357             }
00358         }
00359 
00360         return $conn;
00361     }
00362 
00367     protected function handleException( RedisConnRef $conn, $e ) {
00368         $this->redisPool->handleError( $conn, $e );
00369     }
00370 }