* @covers WANObjectCache::getWarmupKeyMisses
* @covers WANObjectCache::prefixCacheKeys
* @covers WANObjectCache::getProcessCache
- * @covers WANObjectCache::getNonProcessCachedKeys
+ * @covers WANObjectCache::getNonProcessCachedMultiKeys
* @covers WANObjectCache::getRawKeysForWarmup
* @covers WANObjectCache::getInterimValue
* @covers WANObjectCache::setInterimValue
$this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
}
- public function testProcessCache() {
+ /**
+ * @covers WANObjectCache::getWithSetCallback
+ */
+ public function testProcessCacheLruAndDelete() {
+ $cache = $this->cache;
$mockWallClock = 1549343530.2053;
- $this->cache->setMockTime( $mockWallClock );
+ $cache->setMockTime( $mockWallClock );
$hit = 0;
- $callback = function () use ( &$hit ) {
+ $fn = function () use ( &$hit ) {
++$hit;
return 42;
};
- $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
- $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
+ $keysA = [ wfRandomString(), wfRandomString(), wfRandomString() ];
+ $keysB = [ wfRandomString(), wfRandomString(), wfRandomString() ];
+ $pcg = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
- foreach ( $keys as $i => $key ) {
- $this->cache->getWithSetCallback(
- $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+ foreach ( $keysA as $i => $key ) {
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
}
- $this->assertEquals( 3, $hit );
+ $this->assertEquals( 3, $hit, "Values not cached yet" );
- foreach ( $keys as $i => $key ) {
- $this->cache->getWithSetCallback(
- $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+ foreach ( $keysA as $i => $key ) {
+ // Should not evict from process cache
+ $cache->delete( $key );
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
}
- $this->assertEquals( 3, $hit, "Values cached" );
+ $this->assertEquals( 3, $hit, "Values cached; not cleared by delete()" );
- foreach ( $keys as $i => $key ) {
- $this->cache->getWithSetCallback(
- "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+ foreach ( $keysB as $i => $key ) {
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
}
- $this->assertEquals( 6, $hit );
+ $this->assertEquals( 6, $hit, "New values not cached yet" );
- foreach ( $keys as $i => $key ) {
- $this->cache->getWithSetCallback(
- "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+ foreach ( $keysB as $i => $key ) {
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
}
$this->assertEquals( 6, $hit, "New values cached" );
- foreach ( $keys as $i => $key ) {
- // Should not evict from process cache
- $this->cache->delete( $key );
+ foreach ( $keysA as $i => $key ) {
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
+ }
+ $this->assertEquals( 9, $hit, "Prior values evicted by new values" );
+ }
+
+ /**
+ * @covers WANObjectCache::getWithSetCallback
+ */
+ public function testProcessCacheInterimKeys() {
+ $cache = $this->cache;
+ $mockWallClock = 1549343530.2053;
+ $cache->setMockTime( $mockWallClock );
+
+ $hit = 0;
+ $fn = function () use ( &$hit ) {
+ ++$hit;
+ return 42;
+ };
+ $keysA = [ wfRandomString(), wfRandomString(), wfRandomString() ];
+ $pcg = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
+
+ foreach ( $keysA as $i => $key ) {
+ $cache->delete( $key ); // tombstone key
$mockWallClock += 0.001; // cached values will be newer than tombstone
- // Get into cache (specific process cache group)
- $this->cache->getWithSetCallback(
- $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
+ // Get into process cache (specific group) and interim cache
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
}
- $this->assertEquals( 9, $hit, "Values evicted by delete()" );
+ $this->assertEquals( 3, $hit );
- // Get into cache (default process cache group)
- $key = reset( $keys );
- $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
- $this->assertEquals( 9, $hit, "Value recently interim-cached" );
+ // Get into process cache (default group)
+ $key = reset( $keysA );
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
+ $this->assertEquals( 3, $hit, "Value recently interim-cached" );
$mockWallClock += 0.2; // interim key not brand new
- $this->cache->clearProcessCache();
- $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
- $this->assertEquals( 10, $hit, "Value calculated (interim key not recent and reset)" );
- $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
- $this->assertEquals( 10, $hit, "Value process cached" );
+ $cache->clearProcessCache();
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
+ $this->assertEquals( 4, $hit, "Value calculated (interim key not recent and reset)" );
+ $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
+ $this->assertEquals( 4, $hit, "Value process cached" );
+ }
- $mockWallClock += 0.2; // interim key not brand new
- $outerCallback = function () use ( &$callback, $key ) {
- $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
+ /**
+ * @covers WANObjectCache::getWithSetCallback
+ */
+ public function testProcessCacheNesting() {
+ $cache = $this->cache;
+ $mockWallClock = 1549343530.2053;
+ $cache->setMockTime( $mockWallClock );
+
+ $keyOuter = "outer-" . wfRandomString();
+ $keyInner = "inner-" . wfRandomString();
+
+ $innerHit = 0;
+ $innerFn = function () use ( &$innerHit ) {
+ ++$innerHit;
+ return 42;
+ };
+
+ $outerHit = 0;
+ $outerFn = function () use ( $keyInner, $innerFn, $cache, &$outerHit ) {
+ ++$outerHit;
+ $v = $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
return 43 + $v;
};
- // Outer key misses and refuses inner key process cache value
- $this->cache->getWithSetCallback( "$key-miss-outer", 100, $outerCallback );
- $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
+
+ $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
+ $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
+
+ $this->assertEquals( 1, $innerHit, "Inner callback value cached" );
+ $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
+ $mockWallClock += 1;
+
+ $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
+ $this->assertEquals( 1, $innerHit, "Inner callback process cached" );
+
+ // Outer key misses and inner key process cache value is refused
+ $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
+
+ $this->assertEquals( 1, $outerHit, "Outer callback value not yet cached" );
+ $this->assertEquals( 2, $innerHit, "Inner callback value process cache skipped" );
+
+ $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
+
+ $this->assertEquals( 1, $outerHit, "Outer callback value cached" );
+
+ $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
+ $cache->delete( $keyOuter, $cache::HOLDOFF_NONE );
+ $mockWallClock += 1;
+ $cache->clearProcessCache();
+ $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
+
+ $this->assertEquals( 2, $outerHit, "Outer callback value not yet cached" );
+ $this->assertEquals( 3, $innerHit, "Inner callback value not yet cached" );
+
+ $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
+ $mockWallClock += 1;
+ $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
+
+ $this->assertEquals( 3, $innerHit, "Inner callback value process cached" );
}
/**
$cache->set( $key2, $value2, 10 );
$curTTLs = [];
- $this->assertEquals(
+ $this->assertSame(
[ $key1 => $value1, $key2 => $value2 ],
$cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
'Result array populated'
$mockWallClock += 1;
$curTTLs = [];
- $this->assertEquals(
+ $this->assertSame(
[ $key1 => $value1, $key2 => $value2 ],
$cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
"Result array populated even with new check keys"
'key2' => $check2,
'key3' => $check3,
] );
- $this->assertEquals(
+ $this->assertSame(
[ 'key1' => $value1, 'key2' => $value2 ],
$result,
'Initial values'
'key2' => $check2,
'key3' => $check3,
] );
- $this->assertEquals(
+ $this->assertSame(
[ 'key1' => $value1, 'key2' => $value2 ],
$result,
'key1 expired by check1, but value still provided'
$this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
}
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ */
+ public function testMakeMultiKeys() {
+ $cache = $this->cache;
+
+ $ids = [ 1, 2, 3, 4, 4, 5, 6, 6, 7, 7 ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeKey( 'key', $id );
+ };
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "local:key:1" => 1,
+ "local:key:2" => 2,
+ "local:key:3" => 3,
+ "local:key:4" => 4,
+ "local:key:5" => 5,
+ "local:key:6" => 6,
+ "local:key:7" => 7
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+
+ $ids = [ '1', '2', '3', '4', '4', '5', '6', '6', '7', '7' ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+ };
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "global:key:1:a:1:b" => '1',
+ "global:key:2:a:2:b" => '2',
+ "global:key:3:a:3:b" => '3',
+ "global:key:4:a:4:b" => '4',
+ "global:key:5:a:5:b" => '5',
+ "global:key:6:a:6:b" => '6',
+ "global:key:7:a:7:b" => '7'
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+ }
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ */
+ public function testMakeMultiKeysIntString() {
+ $cache = $this->cache;
+ $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7, '7' ];
+ $keyCallback = function ( $id, WANObjectCache $cache ) {
+ return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
+ };
+
+ $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
+
+ $expected = [
+ "global:key:1:a:1:b" => 1,
+ "global:key:2:a:2:b" => 2,
+ "global:key:3:a:3:b" => 3,
+ "global:key:4:a:4:b" => 4,
+ "global:key:5:a:5:b" => 5,
+ "global:key:6:a:6:b" => 6,
+ "global:key:7:a:7:b" => 7
+ ];
+ $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
+ }
+
+ /**
+ * @covers WANObjectCache::makeMultiKeys
+ * @expectedException UnexpectedValueException
+ */
+ public function testMakeMultiKeysCollision() {
+ $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7 ];
+
+ $this->cache->makeMultiKeys(
+ $ids,
+ function ( $id ) {
+ return "keymod:" . $id % 3;
+ }
+ );
+ }
+
+ /**
+ * @covers WANObjectCache::multiRemap
+ */
+ public function testMultiRemap() {
+ $a = [ 'a', 'b', 'c' ];
+ $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3 ];
+
+ $this->assertEquals(
+ [ 'a' => 1, 'b' => 2, 'c' => 3 ],
+ $this->cache->multiRemap( $a, $res )
+ );
+
+ $a = [ 'a', 'b', 'c', 'c', 'd' ];
+ $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3, 'keyD' => 4 ];
+
+ $this->assertEquals(
+ [ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 ],
+ $this->cache->multiRemap( $a, $res )
+ );
+ }
+
+ /**
+ * @covers WANObjectCache::hash256
+ */
+ public function testHash256() {
+ $bag = new HashBagOStuff();
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 5 ] );
+ $this->assertEquals(
+ 'f402bce76bfa1136adc705d8d5719911ce1fe61f0ad82ddf79a15f3c4de6ec4c',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 50 ] );
+ $this->assertEquals(
+ 'f79a126722f0a682c4c500509f1b61e836e56c4803f92edc89fc281da5caa54e',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden' ] );
+ $this->assertEquals(
+ '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+ $cache->hash256( 'x' )
+ );
+
+ $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden', 'epoch' => 3 ] );
+ $this->assertEquals(
+ '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
+ $cache->hash256( 'x' )
+ );
+ }
}
class NearExpiringWANObjectCache extends WANObjectCache {