Merge "Migrate block log to new log system"
[mediawiki.git] / includes / cache / bloom / BloomCache.php
blob6ecaacb8d47f7a58a878190ad18ced92c7c67cbc
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
19 * @author Aaron Schulz
22 /**
23 * Persistent bloom filter used to avoid expensive lookups
25 * @since 1.24
27 abstract class BloomCache {
28 /** @var string Unique ID for key namespacing */
29 protected $cacheID;
31 /** @var array Map of (id => BloomCache) */
32 protected static $instances = array();
34 /**
35 * @param string $id
36 * @return BloomCache
38 final public static function get( $id ) {
39 global $wgBloomFilterStores;
41 if ( !isset( self::$instances[$id] ) ) {
42 if ( isset( $wgBloomFilterStores[$id] ) ) {
43 $class = $wgBloomFilterStores[$id]['class'];
44 self::$instances[$id] = new $class( $wgBloomFilterStores[$id] );
45 } else {
46 wfDebug( "No bloom filter store '$id'; using EmptyBloomCache." );
47 return new EmptyBloomCache( array() );
51 return self::$instances[$id];
54 /**
55 * Create a new bloom cache instance from configuration.
56 * This should only be called from within BloomCache.
58 * @param array $config Parameters include:
59 * - cacheID : Prefix to all bloom filter names that is unique to this cache.
60 * It should only consist of alphanumberic, '-', and '_' characters.
61 * This ID is what avoids collisions if multiple logical caches
62 * use the same storage system, so this should be set carefully.
63 * @throws MWException
65 public function __construct( array $config ) {
66 $this->cacheID = $config['cacheId'];
67 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,32}$!', $this->cacheID ) ) {
68 throw new MWException( "Cache ID '{$this->cacheID}' is invalid." );
72 /**
73 * Check if a member is set in the bloom filter
75 * A member being set means that it *might* have been added.
76 * A member not being set means it *could not* have been added.
78 * This abstracts over isHit() to deal with filter updates and readiness.
79 * A class must exist with the name BloomFilter<type> and a static public
80 * mergeAndCheck() method. The later takes the following arguments:
81 * (BloomCache $bcache, $domain, $virtualKey, array $status)
82 * The method should return a bool indicating whether to use the filter.
84 * The 'shared' bloom key must be used for any updates and will be used
85 * for the membership check if the method returns true. Since the key is shared,
86 * the method should never use delete(). The filter cannot be used in cases where
87 * membership in the filter needs to be taken away. In such cases, code *cannot*
88 * use this method - instead, it can directly use the other BloomCache methods
89 * to manage custom filters with their own keys (e.g. not 'shared').
91 * @param string $domain
92 * @param string $type
93 * @param string $member
94 * @return bool True if set, false if not (also returns true on error)
96 final public function check( $domain, $type, $member ) {
97 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
99 if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) {
100 try {
101 $virtualKey = "$domain:$type";
103 $status = $this->getStatus( $virtualKey );
104 if ( $status == false ) {
105 wfDebug( "Could not query virtual bloom filter '$virtualKey'." );
106 return true;
109 $useFilter = call_user_func_array(
110 array( "BloomFilter{$type}", 'mergeAndCheck' ),
111 array( $this, $domain, $virtualKey, $status )
114 if ( $useFilter ) {
115 return ( $this->isHit( 'shared', "$virtualKey:$member" ) !== false );
117 } catch ( Exception $e ) {
118 MWExceptionHandler::logException( $e );
119 return true;
123 return true;
127 * Inform the bloom filter of a new member in order to keep it up to date
129 * @param string $domain
130 * @param string $type
131 * @param string|array $members
132 * @return bool Success
134 final public function insert( $domain, $type, $members ) {
135 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
137 if ( method_exists( "BloomFilter{$type}", 'mergeAndCheck' ) ) {
138 try {
139 $virtualKey = "$domain:$type";
140 $prefixedMembers = array();
141 foreach ( (array)$members as $member ) {
142 $prefixedMembers[] = "$virtualKey:$member";
145 return $this->add( 'shared', $prefixedMembers );
146 } catch ( Exception $e ) {
147 MWExceptionHandler::logException( $e );
148 return false;
152 return true;
156 * Create a new bloom filter at $key (if one does not exist yet)
158 * @param string $key
159 * @param integer $size Bit length [default: 1000000]
160 * @param float $precision [default: .001]
161 * @return bool Success
163 final public function init( $key, $size = 1000000, $precision = .001 ) {
164 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
166 return $this->doInit( "{$this->cacheID}:$key", $size, min( .1, $precision ) );
170 * Add a member to the bloom filter at $key
172 * @param string $key
173 * @param string|array $members
174 * @return bool Success
176 final public function add( $key, $members ) {
177 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
179 return $this->doAdd( "{$this->cacheID}:$key", (array)$members );
183 * Check if a member is set in the bloom filter.
185 * A member being set means that it *might* have been added.
186 * A member not being set means it *could not* have been added.
188 * If this returns true, then the caller usually should do the
189 * expensive check (whatever that may be). It can be avoided otherwise.
191 * @param string $key
192 * @param string $member
193 * @return bool|null True if set, false if not, null on error
195 final public function isHit( $key, $member ) {
196 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
198 return $this->doIsHit( "{$this->cacheID}:$key", $member );
202 * Destroy a bloom filter at $key
204 * @param string $key
205 * @return bool Success
207 final public function delete( $key ) {
208 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
210 return $this->doDelete( "{$this->cacheID}:$key" );
214 * Set the status map of the virtual bloom filter at $key
216 * @param string $virtualKey
217 * @param array $values Map including some of (lastID, asOfTime, epoch)
218 * @return bool Success
220 final public function setStatus( $virtualKey, array $values ) {
221 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
223 return $this->doSetStatus( "{$this->cacheID}:$virtualKey", $values );
227 * Get the status map of the virtual bloom filter at $key
229 * The map includes:
230 * - lastID : the highest ID of the items merged in
231 * - asOfTime : UNIX timestamp that the filter is up-to-date as of
232 * - epoch : UNIX timestamp that filter started being populated
233 * Unset fields will have a null value.
235 * @param string $virtualKey
236 * @return array|bool False on failure
238 final public function getStatus( $virtualKey ) {
239 $ps = Profiler::instance()->scopedProfileIn( get_class( $this ) . '::' . __FUNCTION__ );
241 return $this->doGetStatus( "{$this->cacheID}:$virtualKey" );
245 * Get an exclusive lock on a filter for updates
247 * @param string $virtualKey
248 * @return ScopedCallback|ScopedLock|null Returns null if acquisition failed
250 public function getScopedLock( $virtualKey ) {
251 return null;
255 * @param string $key
256 * @param integer $size Bit length
257 * @param float $precision
258 * @return bool Success
260 abstract protected function doInit( $key, $size, $precision );
263 * @param string $key
264 * @param array $members
265 * @return bool Success
267 abstract protected function doAdd( $key, array $members );
270 * @param string $key
271 * @param string $member
272 * @return bool|null
274 abstract protected function doIsHit( $key, $member );
277 * @param string $key
278 * @return bool Success
280 abstract protected function doDelete( $key );
283 * @param string $virtualKey
284 * @param array $values
285 * @return bool Success
287 abstract protected function doSetStatus( $virtualKey, array $values );
290 * @param string $key
291 * @return array|bool
293 abstract protected function doGetStatus( $key );
296 class EmptyBloomCache extends BloomCache {
297 public function __construct( array $config ) {
298 parent::__construct( array( 'cacheId' => 'none' ) );
301 protected function doInit( $key, $size, $precision ) {
302 return true;
305 protected function doAdd( $key, array $members ) {
306 return true;
309 protected function doIsHit( $key, $member ) {
310 return true;
313 protected function doDelete( $key ) {
314 return true;
317 protected function doSetStatus( $virtualKey, array $values ) {
318 return true;
321 protected function doGetStatus( $virtualKey ) {
322 return array( 'lastID' => null, 'asOfTime' => null, 'epoch' => null );