3 use MediaWiki\Deferred\DeferredUpdates
;
4 use MediaWiki\MainConfigNames
;
5 use Wikimedia\LightweightObjectStore\StorageAwareness
;
6 use Wikimedia\ObjectCache\BagOStuff
;
7 use Wikimedia\ObjectCache\HashBagOStuff
;
8 use Wikimedia\ObjectCache\MultiWriteBagOStuff
;
9 use Wikimedia\ScopedCallback
;
10 use Wikimedia\TestingAccessWrapper
;
13 * @author Matthias Mullie <mmullie@wikimedia.org>
15 * @covers \Wikimedia\ObjectCache\BagOStuff
16 * @covers \Wikimedia\ObjectCache\MediumSpecificBagOStuff
18 abstract class BagOStuffTestBase
extends MediaWikiIntegrationTestCase
{
22 protected const TEST_TIME
= 1563892142;
24 protected function setUp(): void
{
28 $this->cache
= $this->newCacheInstance();
29 } catch ( InvalidArgumentException
$e ) {
30 $this->markTestSkipped( "Cannot create cache instance for " . static::class .
31 ': the configuration is presumably missing from $wgObjectCaches' );
33 $this->cache
->deleteMulti( [
34 $this->cache
->makeKey( $this->testKey() ),
35 $this->cache
->makeKey( $this->testKey() ) . ':lock'
39 private function testKey() {
40 return 'test-' . static::class;
46 abstract protected function newCacheInstance();
48 protected function getCacheByClass( $className ) {
49 $caches = $this->getConfVar( MainConfigNames
::ObjectCaches
);
50 foreach ( $caches as $id => $cache ) {
51 if ( ( $cache['class'] ??
'' ) === $className ) {
52 return $this->getServiceContainer()->getObjectCacheFactory()->getInstance( $id );
55 $this->markTestSkipped( "No $className is configured" );
58 public function testMakeKey() {
59 $cache = new HashBagOStuff( [ 'keyspace' => 'local_prefix' ] );
61 $localKey = $cache->makeKey( 'first', 'second', 'third' );
62 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
65 'local_prefix:first:second:third',
67 'Local key interpolates parameters'
71 'global:first:second:third',
73 'Global key interpolates parameters and contains global prefix'
76 $this->assertNotEquals(
79 'Local key and global key with same parameters should not be equal'
82 $this->assertNotEquals(
83 $cache->makeKey( 'a', 'bc:', 'de' ),
84 $cache->makeKey( 'a', 'bc', ':de' )
87 $keyEmptyCollection = $cache->makeKey( '', 'second', 'third' );
89 'local_prefix::second:third',
91 'Local key interpolates empty parameters'
95 public function testKeyIsGlobal() {
96 $cache = new HashBagOStuff();
98 $localKey = $cache->makeKey( 'first', 'second', 'third' );
99 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
101 $this->assertFalse( $cache->isKeyGlobal( $localKey ) );
102 $this->assertTrue( $cache->isKeyGlobal( $globalKey ) );
105 public function testMerge() {
106 $key = $this->cache
->makeKey( $this->testKey() );
109 $casRace = false; // emulate a race
110 $callback = static function ( BagOStuff
$cache, $key, $oldVal, &$expiry ) use ( &$calls, &$casRace ) {
114 $cache->set( $key, 'conflict', 5 );
117 return ( $oldVal === false ) ?
'merged' : $oldVal . 'merged';
120 // merge on non-existing value
121 $merged = $this->cache
->merge( $key, $callback, 5 );
122 $this->assertTrue( $merged );
123 $this->assertEquals( 'merged', $this->cache
->get( $key ) );
125 // merge on existing value
126 $merged = $this->cache
->merge( $key, $callback, 5 );
127 $this->assertTrue( $merged );
128 $this->assertEquals( 'mergedmerged', $this->cache
->get( $key ) );
133 $this->cache
->merge( $key, $callback, 5, 1 ),
134 'Non-blocking merge (CAS)'
137 if ( $this->cache
instanceof MultiWriteBagOStuff
) {
138 $wrapper = TestingAccessWrapper
::newFromObject( $this->cache
);
139 $this->assertEquals( count( $wrapper->caches
), $calls );
141 $this->assertSame( 1, $calls );
145 public function testChangeTTLRenew() {
146 $key = $this->cache
->makeKey( $this->testKey() );
149 $this->cache
->add( $key, $value, 60 );
150 $this->assertEquals( $value, $this->cache
->get( $key ) );
151 $this->assertTrue( $this->cache
->changeTTL( $key, 120 ) );
152 $this->assertTrue( $this->cache
->changeTTL( $key, 120 ) );
153 $this->assertTrue( $this->cache
->changeTTL( $key, 0 ) );
154 $this->assertEquals( $this->cache
->get( $key ), $value );
156 $this->cache
->delete( $key );
157 $this->assertFalse( $this->cache
->changeTTL( $key, 15 ) );
160 public function testChangeTTLExpireRel() {
161 $key = $this->cache
->makeKey( $this->testKey() );
164 $this->cache
->add( $key, $value, 5 );
165 $this->assertSame( $value, $this->cache
->get( $key ) );
166 $this->assertTrue( $this->cache
->changeTTL( $key, -3600 ) );
167 $this->assertFalse( $this->cache
->get( $key ) );
168 $this->assertFalse( $this->cache
->changeTTL( $key, -3600 ) );
171 public function testChangeTTLExpireAbs() {
172 $key = $this->cache
->makeKey( $this->testKey() );
175 $this->cache
->add( $key, $value, 5 );
176 $this->assertSame( $value, $this->cache
->get( $key ) );
178 $now = $this->cache
->getCurrentTime();
179 $this->assertTrue( $this->cache
->changeTTL( $key, (int)$now - 3600 ) );
180 $this->assertFalse( $this->cache
->get( $key ) );
181 $this->assertFalse( $this->cache
->changeTTL( $key, (int)$now - 3600 ) );
184 public function testChangeTTLMulti() {
185 $key1 = $this->cache
->makeKey( 'test-key1' );
186 $key2 = $this->cache
->makeKey( 'test-key2' );
187 $key3 = $this->cache
->makeKey( 'test-key3' );
188 $key4 = $this->cache
->makeKey( 'test-key4' );
191 $this->cache
->deleteMulti( [ $key1, $key2, $key3, $key4 ] );
193 $ok = $this->cache
->changeTTLMulti( [ $key1, $key2, $key3 ], 30 );
194 $this->assertFalse( $ok, "No keys found" );
195 $this->assertFalse( $this->cache
->get( $key1 ) );
196 $this->assertFalse( $this->cache
->get( $key2 ) );
197 $this->assertFalse( $this->cache
->get( $key3 ) );
199 $ok = $this->cache
->setMulti( [ $key1 => 1, $key2 => 2, $key3 => 3 ] );
200 $this->assertTrue( $ok, "setMulti() succeeded" );
201 $this->assertCount( 3, $this->cache
->getMulti( [ $key1, $key2, $key3 ] ),
202 "setMulti() succeeded via getMulti() check" );
204 $ok = $this->cache
->changeTTLMulti( [ $key1, $key2, $key3 ], 300 );
205 $this->assertTrue( $ok, "TTL bumped for all keys" );
206 $this->assertSame( 1, $this->cache
->get( $key1 ) );
207 $this->assertEquals( 2, $this->cache
->get( $key2 ) );
208 $this->assertEquals( 3, $this->cache
->get( $key3 ) );
210 $ok = $this->cache
->changeTTLMulti( [ $key1, $key2, $key3, $key4 ], 300 );
211 $this->assertFalse( $ok, "One key missing" );
212 $this->assertSame( 1, $this->cache
->get( $key1 ), "Key still live" );
214 $ok = $this->cache
->setMulti( [ $key1 => 1, $key2 => 2, $key3 => 3 ] );
215 $this->assertTrue( $ok, "setMulti() succeeded" );
217 $now = $this->cache
->getCurrentTime();
218 $ok = $this->cache
->changeTTLMulti( [ $key1, $key2, $key3 ], (int)$now +
86400 );
219 $this->assertTrue( $ok, "Expiry set for all keys" );
220 $this->assertSame( 1, $this->cache
->get( $key1 ), "Key still live" );
223 $this->cache
->deleteMulti( [ $key1, $key2, $key3, $key4 ] );
226 public function testAdd() {
227 $key = $this->cache
->makeKey( $this->testKey() );
228 $this->assertFalse( $this->cache
->get( $key ) );
229 $this->assertTrue( $this->cache
->add( $key, 'test', 5 ) );
230 $this->assertFalse( $this->cache
->add( $key, 'test', 5 ) );
233 public function testAddBackground() {
234 $key = $this->cache
->makeKey( $this->testKey() );
235 $this->assertFalse( $this->cache
->get( $key ) );
237 $this->cache
->add( $key, 'test', 5, BagOStuff
::WRITE_BACKGROUND
)
239 for ( $i = 0; $i < 100 && $this->cache
->get( $key ) !== 'test'; $i++
) {
242 $this->assertSame( 'test', $this->cache
->get( $key ) );
245 public function testGet() {
246 $value = [ 'this' => 'is', 'a' => 'test' ];
248 $key = $this->cache
->makeKey( $this->testKey() );
249 $this->cache
->add( $key, $value, 5 );
250 $this->assertSame( $this->cache
->get( $key ), $value );
253 public function testGetWithSetCallback() {
254 $now = self
::TEST_TIME
;
255 $cache = new HashBagOStuff( [] );
256 $cache->setMockTime( $now );
257 $key = $cache->makeKey( $this->testKey() );
259 $this->assertFalse( $cache->get( $key ), "No value" );
261 $value = $cache->getWithSetCallback(
264 static function ( &$ttl ) {
267 return 'hello kitty';
271 $this->assertEquals( 'hello kitty', $value );
272 $this->assertEquals( $value, $cache->get( $key ), "Value set" );
276 $this->assertFalse( $cache->get( $key ), "Value expired" );
279 public function testIncrWithInit() {
280 $key = $this->cache
->makeKey( $this->testKey() );
282 $val = $this->cache
->get( $key );
283 $this->assertFalse( $val, "No value yet" );
285 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
286 $this->assertSame( 3, $val, "Correct init value" );
288 $val = $this->cache
->incrWithInit( $key, 0, 1, 3 );
289 $this->assertSame( 4, $val, "Correct incremented value" );
290 $this->cache
->delete( $key );
292 $val = $this->cache
->incrWithInit( $key, 0, 5 );
293 $this->assertSame( 5, $val, "Correct incremented value" );
296 public function testIncrWithInitAsync() {
297 $key = $this->cache
->makeKey( $this->testKey() );
298 $val = $this->cache
->get( $key );
299 $this->assertFalse( $val, "No value yet" );
301 $val = $this->cache
->incrWithInit( $key, 0, 1, 3, BagOStuff
::WRITE_BACKGROUND
);
302 if ( $val === true ) {
303 $val = $this->cache
->get( $key );
304 for ( $i = 0; $i < 1000 && $val !== 3; $i++
) {
306 $val = $this->cache
->get( $key );
309 $this->assertSame( 3, $val );
311 $val = $this->cache
->incrWithInit( $key, 0, 1, 3, BagOStuff
::WRITE_BACKGROUND
);
312 if ( $val === true ) {
313 $val = $this->cache
->get( $key );
314 for ( $i = 0; $i < 1000 && $val !== 4; $i++
) {
316 $val = $this->cache
->get( $key );
319 $this->assertSame( 4, $val );
322 public function testGetMulti() {
323 $value1 = [ 'this' => 'is', 'a' => 'test' ];
324 $value2 = [ 'this' => 'is', 'another' => 'test' ];
325 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
326 $value4 = [ 'another test where chars in key will be encoded' ];
328 $key1 = $this->cache
->makeKey( 'test-1' );
329 $key2 = $this->cache
->makeKey( 'test-2' );
330 // internally, MemcachedBagOStuffs will encode to will-%25-encode
331 $key3 = $this->cache
->makeKey( 'will-%-encode' );
332 $key4 = $this->cache
->makeKey(
333 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
337 $this->cache
->delete( $key1 );
338 $this->cache
->delete( $key2 );
339 $this->cache
->delete( $key3 );
340 $this->cache
->delete( $key4 );
342 $this->cache
->add( $key1, $value1, 5 );
343 $this->cache
->add( $key2, $value2, 5 );
344 $this->cache
->add( $key3, $value3, 5 );
345 $this->cache
->add( $key4, $value4, 5 );
348 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
349 $this->cache
->getMulti( [ $key1, $key2, $key3, $key4 ] )
353 $this->cache
->delete( $key1 );
354 $this->cache
->delete( $key2 );
355 $this->cache
->delete( $key3 );
356 $this->cache
->delete( $key4 );
359 public function testSetDeleteMulti() {
361 $this->cache
->makeKey( 'test-1' ) => 'Siberian',
362 $this->cache
->makeKey( 'test-2' ) => [ 'Huskies' ],
363 $this->cache
->makeKey( 'test-3' ) => [ 'are' => 'the' ],
364 $this->cache
->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ],
365 $this->cache
->makeKey( 'test-5' ) => 4,
366 $this->cache
->makeKey( 'test-6' ) => 'ever'
369 $this->assertTrue( $this->cache
->setMulti( $map ) );
372 $this->cache
->getMulti( array_keys( $map ) )
375 $this->assertTrue( $this->cache
->deleteMulti( array_keys( $map ) ) );
379 $this->cache
->getMulti( array_keys( $map ), BagOStuff
::READ_LATEST
)
383 $this->cache
->getMulti( array_keys( $map ) )
387 public function testDelete() {
388 // Delete of non-existent key should return true
389 $key = $this->cache
->makeKey( 'nonexistent' );
390 $this->assertTrue( $this->cache
->delete( $key ) );
391 $this->assertTrue( $this->cache
->delete( $key, BagOStuff
::WRITE_BACKGROUND
) );
394 public function testSetSegmentable() {
395 $key = $this->cache
->makeKey( $this->testKey() );
397 $small = wfRandomString( 32 );
398 // 64 * 8 * 32768 = 16777216 bytes
399 $big = str_repeat( wfRandomString( 32 ) . '-' . wfRandomString( 32 ), 32768 );
401 $callback = static function ( $cache, $key, $oldValue ) {
402 return $oldValue . '!';
405 $cases = [ 'tiny' => $tiny, 'small' => $small, 'big' => $big ];
406 foreach ( $cases as $case => $value ) {
407 $this->cache
->set( $key, $value, 10, BagOStuff
::WRITE_ALLOW_SEGMENTS
);
408 $this->assertEquals( $value, $this->cache
->get( $key ), "get $case" );
409 $this->assertEquals( [ $key => $value ], $this->cache
->getMulti( [ $key ] ), "get $case" );
412 $this->cache
->merge( $key, $callback, 5, 1, BagOStuff
::WRITE_ALLOW_SEGMENTS
),
417 $this->cache
->get( $key ),
422 $this->cache
->getMulti( [ $key ] )[$key],
426 $this->assertTrue( $this->cache
->deleteMulti( [ $key ] ), "delete $case" );
427 $this->assertFalse( $this->cache
->get( $key ), "deleted $case" );
428 $this->assertEquals( [], $this->cache
->getMulti( [ $key ] ), "deletd $case" );
430 $this->cache
->set( $key, "@$value", 10, BagOStuff
::WRITE_ALLOW_SEGMENTS
);
431 $this->assertEquals( "@$value", $this->cache
->get( $key ), "get $case" );
433 $this->cache
->delete( $key, BagOStuff
::WRITE_ALLOW_SEGMENTS
),
436 $this->assertFalse( $this->cache
->get( $key ), "pruned $case" );
437 $this->assertEquals( [], $this->cache
->getMulti( [ $key ] ), "pruned $case" );
440 $this->cache
->set( $key, 666, 10, BagOStuff
::WRITE_ALLOW_SEGMENTS
);
442 $this->assertEquals( 666, $this->cache
->get( $key ) );
443 $this->assertEquals( 667, $this->cache
->incrWithInit( $key, 10 ) );
444 $this->assertEquals( 667, $this->cache
->get( $key ) );
446 $this->assertTrue( $this->cache
->delete( $key ) );
447 $this->assertFalse( $this->cache
->get( $key ) );
450 public function testSetBackground() {
451 $key = $this->cache
->makeKey( $this->testKey() );
453 $this->cache
->set( $key, 'background', BagOStuff
::WRITE_BACKGROUND
) );
456 public function testGetScopedLock() {
457 $key = $this->cache
->makeKey( $this->testKey() );
458 $value1 = $this->cache
->getScopedLock( $key, 0 );
459 $value2 = $this->cache
->getScopedLock( $key, 0 );
461 $this->assertInstanceOf( ScopedCallback
::class, $value1, 'First call returned lock' );
462 $this->assertNull( $value2, 'Duplicate call returned no lock' );
466 $value3 = $this->cache
->getScopedLock( $key, 0 );
467 $this->assertInstanceOf( ScopedCallback
::class, $value3, 'Lock returned callback after release' );
470 $value1 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
471 $value2 = $this->cache
->getScopedLock( $key, 0, 5, 'reentry' );
473 $this->assertInstanceOf( ScopedCallback
::class, $value1, 'First reentrant call returned lock' );
474 $this->assertInstanceOf( ScopedCallback
::class, $value2, 'Second reentrant call returned lock' );
477 public function testReportDupes() {
478 $logger = $this->createMock( Psr\Log\NullLogger
::class );
479 $logger->expects( $this->once() )
480 ->method( 'warning' )
481 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
486 $cache = new HashBagOStuff( [
487 'reportDupes' => true,
488 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
491 $cache->get( 'foo' );
492 $cache->get( 'bar' );
493 $cache->get( 'foo' );
495 DeferredUpdates
::doUpdates();
498 public function testLocking() {
499 $key = $this->cache
->makeKey( $this->testKey() );
500 $this->assertTrue( $this->cache
->lock( $key ) );
501 $this->assertFalse( $this->cache
->lock( $key ) );
502 $this->assertTrue( $this->cache
->unlock( $key ) );
503 $this->assertFalse( $this->cache
->unlock( $key ) );
505 $this->assertTrue( $this->cache
->lock( $key, 5, 5, 'rclass' ) );
506 $this->assertTrue( $this->cache
->lock( $key, 5, 5, 'rclass' ) );
507 $this->assertTrue( $this->cache
->unlock( $key ) );
508 $this->assertTrue( $this->cache
->unlock( $key ) );
511 public function testErrorHandling() {
512 $key = $this->cache
->makeKey( $this->testKey() );
513 $wrapper = TestingAccessWrapper
::newFromObject( $this->cache
);
515 $wp = $this->cache
->watchErrors();
516 $this->cache
->get( $key );
517 $this->assertSame( StorageAwareness
::ERR_NONE
, $this->cache
->getLastError() );
518 $this->assertSame( StorageAwareness
::ERR_NONE
, $this->cache
->getLastError( $wp ) );
520 $wrapper->setLastError( StorageAwareness
::ERR_UNREACHABLE
);
521 $this->assertSame( StorageAwareness
::ERR_UNREACHABLE
, $this->cache
->getLastError() );
522 $this->assertSame( StorageAwareness
::ERR_UNREACHABLE
, $this->cache
->getLastError( $wp ) );
524 $wp = $this->cache
->watchErrors();
525 $wrapper->setLastError( StorageAwareness
::ERR_UNEXPECTED
);
526 $wp2 = $this->cache
->watchErrors();
527 $this->assertSame( StorageAwareness
::ERR_UNEXPECTED
, $this->cache
->getLastError() );
528 $this->assertSame( StorageAwareness
::ERR_UNEXPECTED
, $this->cache
->getLastError( $wp ) );
529 $this->assertSame( StorageAwareness
::ERR_NONE
, $this->cache
->getLastError( $wp2 ) );
531 $this->cache
->get( $key );
532 $this->assertSame( StorageAwareness
::ERR_UNEXPECTED
, $this->cache
->getLastError() );
533 $this->assertSame( StorageAwareness
::ERR_UNEXPECTED
, $this->cache
->getLastError( $wp ) );
534 $this->assertSame( StorageAwareness
::ERR_NONE
, $this->cache
->getLastError( $wp2 ) );