3 use Wikimedia\ScopedCallback
;
6 * @author Matthias Mullie <mmullie@wikimedia.org>
9 class BagOStuffTest
extends MediaWikiTestCase
{
13 const TEST_KEY
= 'test';
15 protected function setUp() {
18 // type defined through parameter
19 if ( $this->getCliArg( 'use-bagostuff' ) ) {
20 $name = $this->getCliArg( 'use-bagostuff' );
22 $this->cache
= ObjectCache
::newFromId( $name );
24 // no type defined - use simple hash
25 $this->cache
= new HashBagOStuff
;
28 $this->cache
->delete( $this->cache
->makeKey( self
::TEST_KEY
) );
29 $this->cache
->delete( $this->cache
->makeKey( self
::TEST_KEY
) . ':lock' );
33 * @covers BagOStuff::makeGlobalKey
34 * @covers BagOStuff::makeKeyInternal
36 public function testMakeKey() {
37 $cache = ObjectCache
::newFromId( 'hash' );
39 $localKey = $cache->makeKey( 'first', 'second', 'third' );
40 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
42 $this->assertStringMatchesFormat(
43 '%Sfirst%Ssecond%Sthird%S',
45 'Local key interpolates parameters'
48 $this->assertStringMatchesFormat(
49 'global%Sfirst%Ssecond%Sthird%S',
51 'Global key interpolates parameters and contains global prefix'
54 $this->assertNotEquals(
57 'Local key and global key with same parameters should not be equal'
60 $this->assertNotEquals(
61 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc:', 'de' ] ),
62 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc', ':de' ] )
67 * @covers BagOStuff::merge
68 * @covers BagOStuff::mergeViaCas
70 public function testMerge() {
71 $key = $this->cache
->makeKey( self
::TEST_KEY
);
74 $casRace = false; // emulate a race
75 $callback = function ( BagOStuff
$cache, $key, $oldVal ) use ( &$calls, &$casRace ) {
79 $cache->set( $key, 'conflict', 5 );
82 return ( $oldVal === false ) ?
'merged' : $oldVal . 'merged';
85 // merge on non-existing value
86 $merged = $this->cache
->merge( $key, $callback, 5 );
87 $this->assertTrue( $merged );
88 $this->assertEquals( 'merged', $this->cache
->get( $key ) );
90 // merge on existing value
91 $merged = $this->cache
->merge( $key, $callback, 5 );
92 $this->assertTrue( $merged );
93 $this->assertEquals( 'mergedmerged', $this->cache
->get( $key ) );
98 $this->cache
->merge( $key, $callback, 5, 1 ),
99 'Non-blocking merge (CAS)'
101 if ( $this->cache
instanceof MultiWriteBagOStuff
) {
102 $wrapper = \Wikimedia\TestingAccessWrapper
::newFromObject( $this->cache
);
103 $n = count( $wrapper->caches
);
107 $this->assertEquals( $n, $calls );
111 * @covers BagOStuff::merge
112 * @dataProvider provideTestMerge_fork
114 public function testMerge_fork( $exists, $childWins, $resCAS ) {
115 $key = $this->cache
->makeKey( self
::TEST_KEY
);
116 $pCallback = function ( BagOStuff
$cache, $key, $oldVal ) {
117 return ( $oldVal === false ) ?
'init-parent' : $oldVal . '-merged-parent';
119 $cCallback = function ( BagOStuff
$cache, $key, $oldVal ) {
120 return ( $oldVal === false ) ?
'init-child' : $oldVal . '-merged-child';
124 $this->cache
->set( $key, 'x', 5 );
128 * Test concurrent merges by forking this process, if:
129 * - not manually called with --use-bagostuff
130 * - pcntl_fork is supported by the system
131 * - cache type will correctly support calls over forks
133 $fork = (bool)$this->getCliArg( 'use-bagostuff' );
134 $fork &= function_exists( 'pcntl_fork' );
135 $fork &= !$this->cache
instanceof HashBagOStuff
;
136 $fork &= !$this->cache
instanceof EmptyBagOStuff
;
137 $fork &= !$this->cache
instanceof MultiWriteBagOStuff
;
140 // Function to start merge(), run another merge() midway through, then finish
141 $func = function ( $cache, $key, $cur ) use ( $pCallback, $cCallback, &$pid ) {
146 pcntl_wait( $status );
148 return $pCallback( $cache, $key, $cur );
150 $this->cache
->merge( $key, $cCallback, 0, 1 );
151 // Bail out of the outer merge() in the child process since it does not
152 // need to attempt to write anything. Success is checked by the parent.
153 parent
::tearDown(); // avoid phpunit notices
158 // attempt a merge - this should fail
159 $merged = $this->cache
->merge( $key, $func, 0, 1 );
162 return; // can't fork, ignore this test...
165 // merge has failed because child process was merging (and we only attempted once)
166 $this->assertEquals( !$childWins, $merged );
167 $this->assertEquals( $this->cache
->get( $key ), $resCAS );
169 $this->markTestSkipped( 'No pcntl methods available' );
173 function provideTestMerge_fork() {
175 // (already exists, child wins CAS, result of CAS)
176 [ false, true, 'init-child' ],
177 [ true, true, 'x-merged-child' ]
182 * @covers BagOStuff::changeTTL
184 public function testChangeTTL() {
185 $key = $this->cache
->makeKey( self
::TEST_KEY
);
188 $this->cache
->add( $key, $value, 5 );
189 $this->assertTrue( $this->cache
->changeTTL( $key, 5 ) );
190 $this->assertEquals( $this->cache
->get( $key ), $value );
191 $this->cache
->delete( $key );
192 $this->assertFalse( $this->cache
->changeTTL( $key, 5 ) );
196 * @covers BagOStuff::add
198 public function testAdd() {
199 $key = $this->cache
->makeKey( self
::TEST_KEY
);
200 $this->assertTrue( $this->cache
->add( $key, 'test', 5 ) );
204 * @covers BagOStuff::get
206 public function testGet() {
207 $value = [ 'this' => 'is', 'a' => 'test' ];
209 $key = $this->cache
->makeKey( self
::TEST_KEY
);
210 $this->cache
->add( $key, $value, 5 );
211 $this->assertEquals( $this->cache
->get( $key ), $value );
215 * @covers BagOStuff::get
216 * @covers BagOStuff::set
217 * @covers BagOStuff::getWithSetCallback
219 public function testGetWithSetCallback() {
220 $key = $this->cache
->makeKey( self
::TEST_KEY
);
221 $value = $this->cache
->getWithSetCallback(
225 return 'hello kitty';
229 $this->assertEquals( 'hello kitty', $value );
230 $this->assertEquals( $value, $this->cache
->get( $key ) );
234 * @covers BagOStuff::incr
236 public function testIncr() {
237 $key = $this->cache
->makeKey( self
::TEST_KEY
);
238 $this->cache
->add( $key, 0, 5 );
239 $this->cache
->incr( $key );
241 $actualValue = $this->cache
->get( $key );
242 $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
246 * @covers BagOStuff::incrWithInit
248 public function testIncrWithInit() {
249 $key = $this->cache
->makeKey( self
::TEST_KEY
);
250 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
251 $this->assertEquals( 3, $val, "Correct init value" );
253 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
254 $this->assertEquals( 4, $val, "Correct init value" );
258 * @covers BagOStuff::getMulti
260 public function testGetMulti() {
261 $value1 = [ 'this' => 'is', 'a' => 'test' ];
262 $value2 = [ 'this' => 'is', 'another' => 'test' ];
263 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
264 $value4 = [ 'another test where chars in key will be encoded' ];
266 $key1 = $this->cache
->makeKey( 'test-1' );
267 $key2 = $this->cache
->makeKey( 'test-2' );
268 // internally, MemcachedBagOStuffs will encode to will-%25-encode
269 $key3 = $this->cache
->makeKey( 'will-%-encode' );
270 $key4 = $this->cache
->makeKey(
271 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
275 $this->cache
->delete( $key1 );
276 $this->cache
->delete( $key2 );
277 $this->cache
->delete( $key3 );
278 $this->cache
->delete( $key4 );
280 $this->cache
->add( $key1, $value1, 5 );
281 $this->cache
->add( $key2, $value2, 5 );
282 $this->cache
->add( $key3, $value3, 5 );
283 $this->cache
->add( $key4, $value4, 5 );
286 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
287 $this->cache
->getMulti( [ $key1, $key2, $key3, $key4 ] )
291 $this->cache
->delete( $key1 );
292 $this->cache
->delete( $key2 );
293 $this->cache
->delete( $key3 );
294 $this->cache
->delete( $key4 );
298 * @covers BagOStuff::setMulti
299 * @covers BagOStuff::deleteMulti
301 public function testSetDeleteMulti() {
303 $this->cache
->makeKey( 'test-1' ) => 'Siberian',
304 $this->cache
->makeKey( 'test-2' ) => [ 'Huskies' ],
305 $this->cache
->makeKey( 'test-3' ) => [ 'are' => 'the' ],
306 $this->cache
->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ],
307 $this->cache
->makeKey( 'test-5' ) => 4,
308 $this->cache
->makeKey( 'test-6' ) => 'ever'
311 $this->cache
->setMulti( $map, 5 );
314 $this->cache
->getMulti( array_keys( $map ) )
317 $this->assertTrue( $this->cache
->deleteMulti( array_keys( $map ), 5 ) );
321 $this->cache
->getMulti( array_keys( $map ) )
326 * @covers BagOStuff::getScopedLock
328 public function testGetScopedLock() {
329 $key = $this->cache
->makeKey( self
::TEST_KEY
);
330 $value1 = $this->cache
->getScopedLock( $key, 0 );
331 $value2 = $this->cache
->getScopedLock( $key, 0 );
333 $this->assertType( ScopedCallback
::class, $value1, 'First call returned lock' );
334 $this->assertNull( $value2, 'Duplicate call returned no lock' );
338 $value3 = $this->cache
->getScopedLock( $key, 0 );
339 $this->assertType( ScopedCallback
::class, $value3, 'Lock returned callback after release' );
342 $value1 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
343 $value2 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
345 $this->assertType( ScopedCallback
::class, $value1, 'First reentrant call returned lock' );
346 $this->assertType( ScopedCallback
::class, $value1, 'Second reentrant call returned lock' );
350 * @covers BagOStuff::__construct
351 * @covers BagOStuff::trackDuplicateKeys
353 public function testReportDupes() {
354 $logger = $this->createMock( Psr\Log\NullLogger
::class );
355 $logger->expects( $this->once() )
356 ->method( 'warning' )
357 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
362 $cache = new HashBagOStuff( [
363 'reportDupes' => true,
364 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
367 $cache->get( 'foo' );
368 $cache->get( 'bar' );
369 $cache->get( 'foo' );
371 DeferredUpdates
::doUpdates();
375 * @covers BagOStuff::lock()
376 * @covers BagOStuff::unlock()
378 public function testLocking() {
380 $this->assertTrue( $this->cache
->lock( $key ) );
381 $this->assertFalse( $this->cache
->lock( $key ) );
382 $this->assertTrue( $this->cache
->unlock( $key ) );
385 $this->assertTrue( $this->cache
->lock( $key2, 5, 5, 'rclass' ) );
386 $this->assertTrue( $this->cache
->lock( $key2, 5, 5, 'rclass' ) );
387 $this->assertTrue( $this->cache
->unlock( $key2 ) );
388 $this->assertTrue( $this->cache
->unlock( $key2 ) );