wiki.techinc.nl/includes/cache/bloom/BloomCache.php
Aaron Schulz 140a4f9788 Fixed BloomCache handling of network partitions
* As documented, it should return true on error, so that DB is
  checked if the filter is down.

Change-Id: I883fafc9f5f3a84f85207de6e916f1630c78d1a4
2014-11-20 09:11:09 +00:00

323 lines
9.3 KiB
PHP

<?php
/**
* 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
* @author Aaron Schulz
*/
/**
* Persistent bloom filter used to avoid expensive lookups
*
* @since 1.24
*/
abstract class BloomCache {
/** @var string Unique ID for key namespacing */
protected $cacheID;
/** @var array Map of (id => BloomCache) */
protected static $instances = array();
/**
* @param string $id
* @return BloomCache
*/
final public static function get( $id ) {
global $wgBloomFilterStores;
if ( !isset( self::$instances[$id] ) ) {
if ( isset( $wgBloomFilterStores[$id] ) ) {
$class = $wgBloomFilterStores[$id]['class'];
self::$instances[$id] = new $class( $wgBloomFilterStores[$id] );
} else {
wfDebug( "No bloom filter store '$id'; using EmptyBloomCache." );
return new EmptyBloomCache( array() );
}
}
return self::$instances[$id];
}
/**
* Create a new bloom cache instance from configuration.
* This should only be called from within BloomCache.
*
* @param array $config Parameters include:
* - cacheID : Prefix to all bloom filter names that is unique to this cache.
* It should only consist of alphanumberic, '-', and '_' characters.
* This ID is what avoids collisions if multiple logical caches
* use the same storage system, so this should be set carefully.
*/
public function __construct( array $config ) {
$this->cacheID = $config['cacheId'];
if ( !preg_match( '!^[a-zA-Z0-9-_]{1,32}$!', $this->cacheID ) ) {
throw new MWException( "Cache ID '{$this->cacheID}' is invalid." );
}
}
/**
* Check if a member is set in the bloom filter
*
* A member being set means that it *might* have been added.
* A member not being set means it *could not* have been added.
*
* This abstracts over isHit() to deal with filter updates and readiness.
* A class must exist with the name BloomFilter<type> and a static public
* mergeAndCheck() method. The later takes the following arguments:
* (BloomCache $bcache, $domain, $virtualKey, array $status)
* The method should return a bool indicating whether to use the filter.
*
* The 'shared' bloom key must be used for any updates and will be used
* for the membership check if the method returns true. Since the key is shared,
* the method should never use delete(). The filter cannot be used in cases where
* membership in the filter needs to be taken away. In such cases, code *cannot*
* use this method - instead, it can directly use the other BloomCache methods
* to manage custom filters with their own keys (e.g. not 'shared').
*
* @param string $domain
* @param string $type
* @param string $member
* @return bool True if set, false if not (also returns true on error)
*/
final public function check( $domain, $type, $member ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) {
try {
$virtualKey = "$domain:$type";
$status = $this->getStatus( $virtualKey );
if ( $status == false ) {
wfDebug( "Could not query virtual bloom filter '$virtualKey'." );
return true;
}
$useFilter = call_user_func_array(
array( "BloomFilter{$type}", 'mergeAndCheck' ),
array( $this, $domain, $virtualKey, $status )
);
if ( $useFilter ) {
return ( $this->isHit( 'shared', "$virtualKey:$member" ) !== false );
}
} catch ( MWException $e ) {
MWExceptionHandler::logException( $e );
return true;
}
}
return true;
}
/**
* Inform the bloom filter of a new member in order to keep it up to date
*
* @param string $domain
* @param string $type
* @param string|array $members
* @return bool Success
*/
final public function insert( $domain, $type, $members ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) {
try {
$virtualKey = "$domain:$type";
$prefixedMembers = array();
foreach ( (array)$members as $member ) {
$prefixedMembers[] = "$virtualKey:$member";
}
return $this->add( 'shared', $prefixedMembers );
} catch ( MWException $e ) {
MWExceptionHandler::logException( $e );
return false;
}
}
return true;
}
/**
* Create a new bloom filter at $key (if one does not exist yet)
*
* @param string $key
* @param integer $size Bit length [default: 1000000]
* @param float $precision [default: .001]
* @return bool Success
*/
final public function init( $key, $size = 1000000, $precision = .001 ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doInit( "{$this->cacheID}:$key", $size, min( .1, $precision ) );
}
/**
* Add a member to the bloom filter at $key
*
* @param string $key
* @param string|array $members
* @return bool Success
*/
final public function add( $key, $members ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doAdd( "{$this->cacheID}:$key", (array)$members );
}
/**
* Check if a member is set in the bloom filter.
*
* A member being set means that it *might* have been added.
* A member not being set means it *could not* have been added.
*
* If this returns true, then the caller usually should do the
* expensive check (whatever that may be). It can be avoided otherwise.
*
* @param string $key
* @param string $member
* @return bool|null True if set, false if not, null on error
*/
final public function isHit( $key, $member ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doIsHit( "{$this->cacheID}:$key", $member );
}
/**
* Destroy a bloom filter at $key
*
* @param string $key
* @return bool Success
*/
final public function delete( $key ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doDelete( "{$this->cacheID}:$key" );
}
/**
* Set the status map of the virtual bloom filter at $key
*
* @param string $virtualKey
* @param array $values Map including some of (lastID, asOfTime, epoch)
* @return bool Success
*/
final public function setStatus( $virtualKey, array $values ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doSetStatus( "{$this->cacheID}:$virtualKey", $values );
}
/**
* Get the status map of the virtual bloom filter at $key
*
* The map includes:
* - lastID : the highest ID of the items merged in
* - asOfTime : UNIX timestamp that the filter is up-to-date as of
* - epoch : UNIX timestamp that filter started being populated
* Unset fields will have a null value.
*
* @param string $virtualKey
* @return array|bool False on failure
*/
final public function getStatus( $virtualKey ) {
$section = new ProfileSection( get_class( $this ) . '::' . __FUNCTION__ );
return $this->doGetStatus( "{$this->cacheID}:$virtualKey" );
}
/**
* Get an exclusive lock on a filter for updates
*
* @param string $virtualKey
* @return ScopedCallback|ScopedLock|null Returns null if acquisition failed
*/
public function getScopedLock( $virtualKey ) {
return null;
}
/**
* @param string $key
* @param integer $size Bit length
* @param float $precision
* @return bool Success
*/
abstract protected function doInit( $key, $size, $precision );
/**
* @param string $key
* @param array $members
* @return bool Success
*/
abstract protected function doAdd( $key, array $members );
/**
* @param string $key
* @param string $member
* @return bool|null
*/
abstract protected function doIsHit( $key, $member );
/**
* @param string $key
* @return bool Success
*/
abstract protected function doDelete( $key );
/**
* @param string $virtualKey
* @param array $values
* @return bool Success
*/
abstract protected function doSetStatus( $virtualKey, array $values );
/**
* @param string $key
* @return array|bool
*/
abstract protected function doGetStatus( $key );
}
class EmptyBloomCache extends BloomCache {
public function __construct( array $config ) {
parent::__construct( array( 'cacheId' => 'none' ) );
}
protected function doInit( $key, $size, $precision ) {
return true;
}
protected function doAdd( $key, array $members ) {
return true;
}
protected function doIsHit( $key, $member ) {
return true;
}
protected function doDelete( $key ) {
return true;
}
protected function doSetStatus( $virtualKey, array $values ) {
return true;
}
protected function doGetStatus( $virtualKey ) {
return array( 'lastID' => null, 'asOfTime' => null, 'epoch' => null );
}
}