From: Aaron Schulz Date: Sun, 16 Mar 2014 19:28:18 +0000 (-0700) Subject: Moved ProcessCacheLRU to /libs X-Git-Tag: 1.31.0-rc.0~16584^2 X-Git-Url: http://git.cyclocoop.org/%24self?a=commitdiff_plain;h=400e4848ec058332b7d48637e1d0f794c1d4281d;p=lhc%2Fweb%2Fwiklou.git Moved ProcessCacheLRU to /libs Change-Id: I7052d04d9847f0310c1e62bd66365c813fddeab5 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b290e8d51f..c627da16fe 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -386,7 +386,6 @@ $wgAutoloadLocalClasses = array( 'MapCacheLRU' => 'includes/cache/MapCacheLRU.php', 'MessageCache' => 'includes/cache/MessageCache.php', 'ObjectFileCache' => 'includes/cache/ObjectFileCache.php', - 'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php', 'ResourceFileCache' => 'includes/cache/ResourceFileCache.php', # includes/changes @@ -709,6 +708,7 @@ $wgAutoloadLocalClasses = array( 'JSTokenizer' => 'includes/libs/jsminplus.php', 'MultiHttpClient' => 'includes/libs/MultiHttpClient.php', 'MWMessagePack' => 'includes/libs/MWMessagePack.php', + 'ProcessCacheLRU' => 'includes/libs/ProcessCacheLRU.php', 'RunningStat' => 'includes/libs/RunningStat.php', 'ScopedCallback' => 'includes/libs/ScopedCallback.php', 'ScopedPHPTimeout' => 'includes/libs/ScopedPHPTimeout.php', diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php deleted file mode 100644 index 786d74acec..0000000000 --- a/includes/cache/ProcessCacheLRU.php +++ /dev/null @@ -1,132 +0,0 @@ - prop => value) - /** @var Array */ - protected $cacheTimes = array(); // (key => prop => UNIX timestamp) - - protected $maxCacheKeys; // integer; max entries - - /** - * @param $maxKeys integer Maximum number of entries allowed (min 1). - * @throws MWException When $maxCacheKeys is not an int or =< 0. - */ - public function __construct( $maxKeys ) { - if ( !is_int( $maxKeys ) || $maxKeys < 1 ) { - throw new MWException( __METHOD__ . " must be given an integer and >= 1" ); - } - $this->maxCacheKeys = $maxKeys; - } - - /** - * Set a property field for a cache entry. - * This will prune the cache if it gets too large based on LRU. - * If the item is already set, it will be pushed to the top of the cache. - * - * @param $key string - * @param $prop string - * @param $value mixed - * @return void - */ - public function set( $key, $prop, $value ) { - if ( isset( $this->cache[$key] ) ) { - $this->ping( $key ); // push to top - } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) { - reset( $this->cache ); - $evictKey = key( $this->cache ); - unset( $this->cache[$evictKey] ); - unset( $this->cacheTimes[$evictKey] ); - } - $this->cache[$key][$prop] = $value; - $this->cacheTimes[$key][$prop] = time(); - } - - /** - * Check if a property field exists for a cache entry. - * - * @param $key string - * @param $prop string - * @param $maxAge integer Ignore items older than this many seconds (since 1.21) - * @return bool - */ - public function has( $key, $prop, $maxAge = 0 ) { - if ( isset( $this->cache[$key][$prop] ) ) { - return ( $maxAge <= 0 || ( time() - $this->cacheTimes[$key][$prop] ) <= $maxAge ); - } - - return false; - } - - /** - * Get a property field for a cache entry. - * This returns null if the property is not set. - * If the item is already set, it will be pushed to the top of the cache. - * - * @param $key string - * @param $prop string - * @return mixed - */ - public function get( $key, $prop ) { - if ( isset( $this->cache[$key][$prop] ) ) { - $this->ping( $key ); // push to top - return $this->cache[$key][$prop]; - } else { - return null; - } - } - - /** - * Clear one or several cache entries, or all cache entries - * - * @param $keys string|Array - * @return void - */ - public function clear( $keys = null ) { - if ( $keys === null ) { - $this->cache = array(); - $this->cacheTimes = array(); - } else { - foreach ( (array)$keys as $key ) { - unset( $this->cache[$key] ); - unset( $this->cacheTimes[$key] ); - } - } - } - - /** - * Push an entry to the top of the cache - * - * @param $key string - */ - protected function ping( $key ) { - $item = $this->cache[$key]; - unset( $this->cache[$key] ); - $this->cache[$key] = $item; - } -} diff --git a/includes/libs/ProcessCacheLRU.php b/includes/libs/ProcessCacheLRU.php new file mode 100644 index 0000000000..f2d9f42aba --- /dev/null +++ b/includes/libs/ProcessCacheLRU.php @@ -0,0 +1,132 @@ + prop => value) + /** @var Array */ + protected $cacheTimes = array(); // (key => prop => UNIX timestamp) + + protected $maxCacheKeys; // integer; max entries + + /** + * @param $maxKeys integer Maximum number of entries allowed (min 1). + * @throws UnexpectedValueException When $maxCacheKeys is not an int or =< 0. + */ + public function __construct( $maxKeys ) { + if ( !is_int( $maxKeys ) || $maxKeys < 1 ) { + throw new UnexpectedValueException( __METHOD__ . " must be given an integer >= 1" ); + } + $this->maxCacheKeys = $maxKeys; + } + + /** + * Set a property field for a cache entry. + * This will prune the cache if it gets too large based on LRU. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param $key string + * @param $prop string + * @param $value mixed + * @return void + */ + public function set( $key, $prop, $value ) { + if ( isset( $this->cache[$key] ) ) { + $this->ping( $key ); // push to top + } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) { + reset( $this->cache ); + $evictKey = key( $this->cache ); + unset( $this->cache[$evictKey] ); + unset( $this->cacheTimes[$evictKey] ); + } + $this->cache[$key][$prop] = $value; + $this->cacheTimes[$key][$prop] = time(); + } + + /** + * Check if a property field exists for a cache entry. + * + * @param $key string + * @param $prop string + * @param $maxAge integer Ignore items older than this many seconds (since 1.21) + * @return bool + */ + public function has( $key, $prop, $maxAge = 0 ) { + if ( isset( $this->cache[$key][$prop] ) ) { + return ( $maxAge <= 0 || ( time() - $this->cacheTimes[$key][$prop] ) <= $maxAge ); + } + + return false; + } + + /** + * Get a property field for a cache entry. + * This returns null if the property is not set. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param $key string + * @param $prop string + * @return mixed + */ + public function get( $key, $prop ) { + if ( isset( $this->cache[$key][$prop] ) ) { + $this->ping( $key ); // push to top + return $this->cache[$key][$prop]; + } else { + return null; + } + } + + /** + * Clear one or several cache entries, or all cache entries + * + * @param $keys string|Array + * @return void + */ + public function clear( $keys = null ) { + if ( $keys === null ) { + $this->cache = array(); + $this->cacheTimes = array(); + } else { + foreach ( (array)$keys as $key ) { + unset( $this->cache[$key] ); + unset( $this->cacheTimes[$key] ); + } + } + } + + /** + * Push an entry to the top of the cache + * + * @param $key string + */ + protected function ping( $key ) { + $item = $this->cache[$key]; + unset( $this->cache[$key] ); + $this->cache[$key] = $item; + } +} diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php deleted file mode 100644 index d3793d8343..0000000000 --- a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php +++ /dev/null @@ -1,237 +0,0 @@ -assertAttributeEquals( array(), 'cache', $cache, $msg ); - } - - /** - * Helper to fill a cache object passed by reference - */ - function fillCache( &$cache, $numEntries ) { - // Fill cache with three values - for ( $i = 1; $i <= $numEntries; $i++ ) { - $cache->set( "cache-key-$i", "prop-$i", "value-$i" ); - } - } - - /** - * Generates an array of what would be expected in cache for a given cache - * size and a number of entries filled in sequentially - */ - function getExpectedCache( $cacheMaxEntries, $entryToFill ) { - $expected = array(); - - if ( $entryToFill === 0 ) { - # The cache is empty! - return array(); - } elseif ( $entryToFill <= $cacheMaxEntries ) { - # Cache is not fully filled - $firstKey = 1; - } else { - # Cache overflowed - $firstKey = 1 + $entryToFill - $cacheMaxEntries; - } - - $lastKey = $entryToFill; - - for ( $i = $firstKey; $i <= $lastKey; $i++ ) { - $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" ); - } - - return $expected; - } - - /** - * Highlight diff between assertEquals and assertNotSame - */ - public function testPhpUnitArrayEquality() { - $one = array( 'A' => 1, 'B' => 2 ); - $two = array( 'B' => 2, 'A' => 1 ); - $this->assertEquals( $one, $two ); // == - $this->assertNotSame( $one, $two ); // === - } - - /** - * @dataProvider provideInvalidConstructorArg - * @expectedException MWException - */ - public function testConstructorGivenInvalidValue( $maxSize ) { - new ProcessCacheLRUTestable( $maxSize ); - } - - /** - * Value which are forbidden by the constructor - */ - public static function provideInvalidConstructorArg() { - return array( - array( null ), - array( array() ), - array( new stdClass() ), - array( 0 ), - array( '5' ), - array( -1 ), - ); - } - - public function testAddAndGetAKey() { - $oneCache = new ProcessCacheLRUTestable( 1 ); - $this->assertCacheEmpty( $oneCache ); - - // First set just one value - $oneCache->set( 'cache-key', 'prop1', 'value1' ); - $this->assertEquals( 1, $oneCache->getEntriesCount() ); - $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) ); - $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) ); - } - - public function testDeleteOldKey() { - $oneCache = new ProcessCacheLRUTestable( 1 ); - $this->assertCacheEmpty( $oneCache ); - - $oneCache->set( 'cache-key', 'prop1', 'value1' ); - $oneCache->set( 'cache-key', 'prop1', 'value2' ); - $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) ); - } - - /** - * This test that we properly overflow when filling a cache with - * a sequence of always different cache-keys. Meant to verify we correclty - * delete the older key. - * - * @dataProvider provideCacheFilling - * @param $cacheMaxEntries Maximum entry the created cache will hold - * @param $entryToFill Number of entries to insert in the created cache. - */ - public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) { - $cache = new ProcessCacheLRUTestable( $cacheMaxEntries ); - $this->fillCache( $cache, $entryToFill ); - - $this->assertSame( - $this->getExpectedCache( $cacheMaxEntries, $entryToFill ), - $cache->getCache(), - "Filling a $cacheMaxEntries entries cache with $entryToFill entries" - ); - } - - /** - * Provider for testFillingCache - */ - public static function provideCacheFilling() { - // ($cacheMaxEntries, $entryToFill, $msg='') - return array( - array( 1, 0 ), - array( 1, 1 ), - array( 1, 2 ), # overflow - array( 5, 33 ), # overflow - ); - } - - /** - * Create a cache with only one remaining entry then update - * the first inserted entry. Should bump it to the top. - */ - public function testReplaceExistingKeyShouldBumpEntryToTop() { - $maxEntries = 3; - - $cache = new ProcessCacheLRUTestable( $maxEntries ); - // Fill cache leaving just one remaining slot - $this->fillCache( $cache, $maxEntries - 1 ); - - // Set an existing cache key - $cache->set( "cache-key-1", "prop-1", "new-value-for-1" ); - - $this->assertSame( - array( - 'cache-key-2' => array( 'prop-2' => 'value-2' ), - 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ), - ), - $cache->getCache() - ); - } - - public function testRecentlyAccessedKeyStickIn() { - $cache = new ProcessCacheLRUTestable( 2 ); - $cache->set( 'first', 'prop1', 'value1' ); - $cache->set( 'second', 'prop2', 'value2' ); - - // Get first - $cache->get( 'first', 'prop1' ); - // Cache a third value, should invalidate the least used one - $cache->set( 'third', 'prop3', 'value3' ); - - $this->assertFalse( $cache->has( 'second', 'prop2' ) ); - } - - /** - * This first create a full cache then update the value for the 2nd - * filled entry. - * Given a cache having 1,2,3 as key, updating 2 should bump 2 to - * the top of the queue with the new value: 1,3,2* (* = updated). - */ - public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() { - $maxEntries = 3; - - $cache = new ProcessCacheLRUTestable( $maxEntries ); - $this->fillCache( $cache, $maxEntries ); - - // Set an existing cache key - $cache->set( "cache-key-2", "prop-2", "new-value-for-2" ); - $this->assertSame( - array( - 'cache-key-1' => array( 'prop-1' => 'value-1' ), - 'cache-key-3' => array( 'prop-3' => 'value-3' ), - 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ), - ), - $cache->getCache() - ); - $this->assertEquals( 'new-value-for-2', - $cache->get( 'cache-key-2', 'prop-2' ) - ); - } - - public function testBumpExistingKeyToTop() { - $cache = new ProcessCacheLRUTestable( 3 ); - $this->fillCache( $cache, 3 ); - - // Set the very first cache key to a new value - $cache->set( "cache-key-1", "prop-1", "new value for 1" ); - $this->assertEquals( - array( - 'cache-key-2' => array( 'prop-2' => 'value-2' ), - 'cache-key-3' => array( 'prop-3' => 'value-3' ), - 'cache-key-1' => array( 'prop-1' => 'new value for 1' ), - ), - $cache->getCache() - ); - } -} - -/** - * Overrides some ProcessCacheLRU methods and properties accessibility. - */ -class ProcessCacheLRUTestable extends ProcessCacheLRU { - public $cache = array(); - - public function getCache() { - return $this->cache; - } - - public function getEntriesCount() { - return count( $this->cache ); - } -} diff --git a/tests/phpunit/includes/libs/ProcessCacheLRUTest.php b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php new file mode 100644 index 0000000000..3eae810c4d --- /dev/null +++ b/tests/phpunit/includes/libs/ProcessCacheLRUTest.php @@ -0,0 +1,237 @@ +assertAttributeEquals( array(), 'cache', $cache, $msg ); + } + + /** + * Helper to fill a cache object passed by reference + */ + function fillCache( &$cache, $numEntries ) { + // Fill cache with three values + for ( $i = 1; $i <= $numEntries; $i++ ) { + $cache->set( "cache-key-$i", "prop-$i", "value-$i" ); + } + } + + /** + * Generates an array of what would be expected in cache for a given cache + * size and a number of entries filled in sequentially + */ + function getExpectedCache( $cacheMaxEntries, $entryToFill ) { + $expected = array(); + + if ( $entryToFill === 0 ) { + # The cache is empty! + return array(); + } elseif ( $entryToFill <= $cacheMaxEntries ) { + # Cache is not fully filled + $firstKey = 1; + } else { + # Cache overflowed + $firstKey = 1 + $entryToFill - $cacheMaxEntries; + } + + $lastKey = $entryToFill; + + for ( $i = $firstKey; $i <= $lastKey; $i++ ) { + $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" ); + } + + return $expected; + } + + /** + * Highlight diff between assertEquals and assertNotSame + */ + public function testPhpUnitArrayEquality() { + $one = array( 'A' => 1, 'B' => 2 ); + $two = array( 'B' => 2, 'A' => 1 ); + $this->assertEquals( $one, $two ); // == + $this->assertNotSame( $one, $two ); // === + } + + /** + * @dataProvider provideInvalidConstructorArg + * @expectedException UnexpectedValueException + */ + public function testConstructorGivenInvalidValue( $maxSize ) { + new ProcessCacheLRUTestable( $maxSize ); + } + + /** + * Value which are forbidden by the constructor + */ + public static function provideInvalidConstructorArg() { + return array( + array( null ), + array( array() ), + array( new stdClass() ), + array( 0 ), + array( '5' ), + array( -1 ), + ); + } + + public function testAddAndGetAKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + // First set just one value + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $this->assertEquals( 1, $oneCache->getEntriesCount() ); + $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) ); + $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + public function testDeleteOldKey() { + $oneCache = new ProcessCacheLRUTestable( 1 ); + $this->assertCacheEmpty( $oneCache ); + + $oneCache->set( 'cache-key', 'prop1', 'value1' ); + $oneCache->set( 'cache-key', 'prop1', 'value2' ); + $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) ); + } + + /** + * This test that we properly overflow when filling a cache with + * a sequence of always different cache-keys. Meant to verify we correclty + * delete the older key. + * + * @dataProvider provideCacheFilling + * @param $cacheMaxEntries Maximum entry the created cache will hold + * @param $entryToFill Number of entries to insert in the created cache. + */ + public function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) { + $cache = new ProcessCacheLRUTestable( $cacheMaxEntries ); + $this->fillCache( $cache, $entryToFill ); + + $this->assertSame( + $this->getExpectedCache( $cacheMaxEntries, $entryToFill ), + $cache->getCache(), + "Filling a $cacheMaxEntries entries cache with $entryToFill entries" + ); + } + + /** + * Provider for testFillingCache + */ + public static function provideCacheFilling() { + // ($cacheMaxEntries, $entryToFill, $msg='') + return array( + array( 1, 0 ), + array( 1, 1 ), + array( 1, 2 ), # overflow + array( 5, 33 ), # overflow + ); + } + + /** + * Create a cache with only one remaining entry then update + * the first inserted entry. Should bump it to the top. + */ + public function testReplaceExistingKeyShouldBumpEntryToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + // Fill cache leaving just one remaining slot + $this->fillCache( $cache, $maxEntries - 1 ); + + // Set an existing cache key + $cache->set( "cache-key-1", "prop-1", "new-value-for-1" ); + + $this->assertSame( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ), + ), + $cache->getCache() + ); + } + + public function testRecentlyAccessedKeyStickIn() { + $cache = new ProcessCacheLRUTestable( 2 ); + $cache->set( 'first', 'prop1', 'value1' ); + $cache->set( 'second', 'prop2', 'value2' ); + + // Get first + $cache->get( 'first', 'prop1' ); + // Cache a third value, should invalidate the least used one + $cache->set( 'third', 'prop3', 'value3' ); + + $this->assertFalse( $cache->has( 'second', 'prop2' ) ); + } + + /** + * This first create a full cache then update the value for the 2nd + * filled entry. + * Given a cache having 1,2,3 as key, updating 2 should bump 2 to + * the top of the queue with the new value: 1,3,2* (* = updated). + */ + public function testReplaceExistingKeyInAFullCacheShouldBumpToTop() { + $maxEntries = 3; + + $cache = new ProcessCacheLRUTestable( $maxEntries ); + $this->fillCache( $cache, $maxEntries ); + + // Set an existing cache key + $cache->set( "cache-key-2", "prop-2", "new-value-for-2" ); + $this->assertSame( + array( + 'cache-key-1' => array( 'prop-1' => 'value-1' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ), + ), + $cache->getCache() + ); + $this->assertEquals( 'new-value-for-2', + $cache->get( 'cache-key-2', 'prop-2' ) + ); + } + + public function testBumpExistingKeyToTop() { + $cache = new ProcessCacheLRUTestable( 3 ); + $this->fillCache( $cache, 3 ); + + // Set the very first cache key to a new value + $cache->set( "cache-key-1", "prop-1", "new value for 1" ); + $this->assertEquals( + array( + 'cache-key-2' => array( 'prop-2' => 'value-2' ), + 'cache-key-3' => array( 'prop-3' => 'value-3' ), + 'cache-key-1' => array( 'prop-1' => 'new value for 1' ), + ), + $cache->getCache() + ); + } +} + +/** + * Overrides some ProcessCacheLRU methods and properties accessibility. + */ +class ProcessCacheLRUTestable extends ProcessCacheLRU { + public $cache = array(); + + public function getCache() { + return $this->cache; + } + + public function getEntriesCount() { + return count( $this->cache ); + } +}