3 use Wikimedia\TestingAccessWrapper
;
6 * @covers WANObjectCache::wrap
7 * @covers WANObjectCache::unwrap
8 * @covers WANObjectCache::worthRefreshExpiring
9 * @covers WANObjectCache::worthRefreshPopular
10 * @covers WANObjectCache::isValid
11 * @covers WANObjectCache::getWarmupKeyMisses
12 * @covers WANObjectCache::prefixCacheKeys
13 * @covers WANObjectCache::getProcessCache
14 * @covers WANObjectCache::getNonProcessCachedKeys
15 * @covers WANObjectCache::getRawKeysForWarmup
16 * @covers WANObjectCache::getInterimValue
17 * @covers WANObjectCache::setInterimValue
19 class WANObjectCacheTest
extends PHPUnit\Framework\TestCase
{
21 use MediaWikiCoversValidator
;
22 use PHPUnit4And6Compat
;
24 /** @var WANObjectCache */
27 private $internalCache;
29 protected function setUp() {
32 $this->cache
= new WANObjectCache( [
33 'cache' => new HashBagOStuff(),
34 'pool' => 'testcache-hash',
35 'relayer' => new EventRelayerNull( [] )
38 $wanCache = TestingAccessWrapper
::newFromObject( $this->cache
);
39 /** @noinspection PhpUndefinedFieldInspection */
40 $this->internalCache
= $wanCache->cache
;
44 * @dataProvider provideSetAndGet
45 * @covers WANObjectCache::set()
46 * @covers WANObjectCache::get()
47 * @covers WANObjectCache::makeKey()
51 public function testSetAndGet( $value, $ttl ) {
54 $key = $this->cache
->makeKey( 'x', wfRandomString() );
56 $this->cache
->get( $key, $curTTL, [], $asOf );
57 $this->assertNull( $curTTL, "Current TTL is null" );
58 $this->assertNull( $asOf, "Current as-of-time is infinite" );
60 $t = microtime( true );
61 $this->cache
->set( $key, $value, $ttl );
63 $this->assertEquals( $value, $this->cache
->get( $key, $curTTL, [], $asOf ) );
64 if ( is_infinite( $ttl ) ||
$ttl == 0 ) {
65 $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
67 $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
68 $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
70 $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
71 $this->assertLessThanOrEqual( $t +
1, $asOf, "As-of-time in range of set() time" );
74 public static function provideSetAndGet() {
81 [ (object)[ 'meow' ], 3 ],
89 * @covers WANObjectCache::get()
90 * @covers WANObjectCache::makeGlobalKey()
92 public function testGetNotExists() {
93 $key = $this->cache
->makeGlobalKey( 'y', wfRandomString(), 'p' );
95 $value = $this->cache
->get( $key, $curTTL );
97 $this->assertFalse( $value, "Non-existing key has false value" );
98 $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
102 * @covers WANObjectCache::set()
104 public function testSetOver() {
105 $key = wfRandomString();
106 for ( $i = 0; $i < 3; ++
$i ) {
107 $value = wfRandomString();
108 $this->cache
->set( $key, $value, 3 );
110 $this->assertEquals( $this->cache
->get( $key ), $value );
115 * @covers WANObjectCache::set()
117 public function testStaleSet() {
118 $key = wfRandomString();
119 $value = wfRandomString();
120 $this->cache
->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
122 $this->assertFalse( $this->cache
->get( $key ), "Stale set() value ignored" );
125 public function testProcessCache() {
127 $callback = function () use ( &$hit ) {
131 $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
132 $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
134 foreach ( $keys as $i => $key ) {
135 $this->cache
->getWithSetCallback(
136 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
138 $this->assertEquals( 3, $hit );
140 foreach ( $keys as $i => $key ) {
141 $this->cache
->getWithSetCallback(
142 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
144 $this->assertEquals( 3, $hit, "Values cached" );
146 foreach ( $keys as $i => $key ) {
147 $this->cache
->getWithSetCallback(
148 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
150 $this->assertEquals( 6, $hit );
152 foreach ( $keys as $i => $key ) {
153 $this->cache
->getWithSetCallback(
154 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
156 $this->assertEquals( 6, $hit, "New values cached" );
158 foreach ( $keys as $i => $key ) {
159 $this->cache
->delete( $key );
160 $this->cache
->getWithSetCallback(
161 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
163 $this->assertEquals( 9, $hit, "Values evicted" );
165 $key = reset( $keys );
166 // Get into cache (default process cache group)
167 $this->cache
->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
168 $this->assertEquals( 10, $hit, "Value calculated" );
169 $this->cache
->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
170 $this->assertEquals( 10, $hit, "Value cached" );
171 $outerCallback = function () use ( &$callback, $key ) {
172 $v = $this->cache
->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
176 // Outer key misses and refuses inner key process cache value
177 $this->cache
->getWithSetCallback( "$key-miss-outer", 100, $outerCallback );
178 $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
182 * @dataProvider getWithSetCallback_provider
183 * @covers WANObjectCache::getWithSetCallback()
184 * @covers WANObjectCache::doGetWithSetCallback()
185 * @param array $extOpts
186 * @param bool $versioned
188 public function testGetWithSetCallback( array $extOpts, $versioned ) {
189 $cache = $this->cache
;
191 $key = wfRandomString();
192 $value = wfRandomString();
193 $cKey1 = wfRandomString();
194 $cKey2 = wfRandomString();
199 $func = function ( $old, &$ttl, &$opts, $asOf )
200 use ( &$wasSet, &$priorValue, &$priorAsOf, $value ) {
204 $ttl = 20; // override with another value
208 $mockWallClock = 1549343530.2053;
209 $priorTime = $mockWallClock; // reference time
210 $cache->setMockTime( $mockWallClock );
213 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] +
$extOpts );
214 $this->assertEquals( $value, $v, "Value returned" );
215 $this->assertEquals( 1, $wasSet, "Value regenerated" );
216 $this->assertFalse( $priorValue, "No prior value" );
217 $this->assertNull( $priorAsOf, "No prior value" );
220 $cache->get( $key, $curTTL );
221 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
222 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
225 $v = $cache->getWithSetCallback(
226 $key, 30, $func, [ 'lowTTL' => 0, 'lockTSE' => 5 ] +
$extOpts );
227 $this->assertEquals( $value, $v, "Value returned" );
228 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
233 $v = $cache->getWithSetCallback(
234 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
236 $this->assertEquals( $value, $v, "Value returned" );
237 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
238 $this->assertEquals( $value, $priorValue, "Has prior value" );
239 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
240 $t1 = $cache->getCheckKeyTime( $cKey1 );
241 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
242 $t2 = $cache->getCheckKeyTime( $cKey2 );
243 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
245 $mockWallClock +
= 0.01;
246 $priorTime = $mockWallClock; // reference time
248 $v = $cache->getWithSetCallback(
249 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
251 $this->assertEquals( $value, $v, "Value returned" );
252 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
253 $t1 = $cache->getCheckKeyTime( $cKey1 );
254 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
255 $t2 = $cache->getCheckKeyTime( $cKey2 );
256 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
259 $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
261 $this->assertEquals( $value, $v[$cache::VFLD_DATA
], "Value returned" );
263 $this->assertEquals( $value, $v, "Value returned" );
265 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
268 $key = wfRandomString();
269 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] +
$extOpts );
270 $this->assertEquals( $value, $v, "Value returned" );
271 $cache->delete( $key );
272 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] +
$extOpts );
273 $this->assertEquals( $value, $v, "Value still returned after deleted" );
274 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
276 $oldValReceived = -1;
277 $oldAsOfReceived = -1;
278 $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
279 use ( &$oldValReceived, &$oldAsOfReceived, &$wasSet ) {
281 $oldValReceived = $oldVal;
282 $oldAsOfReceived = $oldAsOf;
284 return 'xxx' . $wasSet;
287 $mockWallClock = 1549343530.2053;
288 $priorTime = $mockWallClock; // reference time
291 $key = wfRandomString();
292 $v = $cache->getWithSetCallback(
293 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] +
$extOpts );
294 $this->assertEquals( 'xxx1', $v, "Value returned" );
295 $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
296 $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
298 $mockWallClock +
= 40;
299 $v = $cache->getWithSetCallback(
300 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] +
$extOpts );
301 $this->assertEquals( 'xxx2', $v, "Value still returned after expired" );
302 $this->assertEquals( 2, $wasSet, "Value recalculated while expired" );
303 $this->assertEquals( 'xxx1', $oldValReceived, "Callback got stale value" );
304 $this->assertNotEquals( null, $oldAsOfReceived, "Callback got stale value" );
306 $mockWallClock +
= 260;
307 $v = $cache->getWithSetCallback(
308 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] +
$extOpts );
309 $this->assertEquals( 'xxx3', $v, "Value still returned after expired" );
310 $this->assertEquals( 3, $wasSet, "Value recalculated while expired" );
311 $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
312 $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
314 $mockWallClock = ( $priorTime - $cache::HOLDOFF_TTL
- 1 );
316 $key = wfRandomString();
317 $checkKey = $cache->makeKey( 'template', 'X' );
318 $cache->touchCheckKey( $checkKey ); // init check key
319 $mockWallClock = $priorTime;
320 $v = $cache->getWithSetCallback(
322 $cache::TTL_INDEFINITE
,
324 [ 'graceTTL' => $cache::TTL_WEEK
, 'checkKeys' => [ $checkKey ] ] +
$extOpts
326 $this->assertEquals( 'xxx1', $v, "Value returned" );
327 $this->assertEquals( 1, $wasSet, "Value computed" );
328 $this->assertEquals( false, $oldValReceived, "Callback got no stale value" );
329 $this->assertEquals( null, $oldAsOfReceived, "Callback got no stale value" );
331 $mockWallClock +
= $cache::TTL_HOUR
; // some time passes
332 $v = $cache->getWithSetCallback(
334 $cache::TTL_INDEFINITE
,
336 [ 'graceTTL' => $cache::TTL_WEEK
, 'checkKeys' => [ $checkKey ] ] +
$extOpts
338 $this->assertEquals( 'xxx1', $v, "Cached value returned" );
339 $this->assertEquals( 1, $wasSet, "Cached value returned" );
341 $cache->touchCheckKey( $checkKey ); // make key stale
342 $mockWallClock +
= 0.01; // ~1 week left of grace (barely stale to avoid refreshes)
344 $v = $cache->getWithSetCallback(
346 $cache::TTL_INDEFINITE
,
348 [ 'graceTTL' => $cache::TTL_WEEK
, 'checkKeys' => [ $checkKey ] ] +
$extOpts
350 $this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" );
351 $this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" );
353 // Chance of refresh increase to unity as staleness approaches graceTTL
354 $mockWallClock +
= $cache::TTL_WEEK
; // 8 days of being stale
355 $v = $cache->getWithSetCallback(
357 $cache::TTL_INDEFINITE
,
359 [ 'graceTTL' => $cache::TTL_WEEK
, 'checkKeys' => [ $checkKey ] ] +
$extOpts
361 $this->assertEquals( 'xxx2', $v, "Value was recomputed (past grace)" );
362 $this->assertEquals( 2, $wasSet, "Value was recomputed (past grace)" );
363 $this->assertEquals( 'xxx1', $oldValReceived, "Callback got post-grace stale value" );
364 $this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" );
368 * @dataProvider getWithSetCallback_provider
369 * @covers WANObjectCache::getWithSetCallback()
370 * @covers WANObjectCache::doGetWithSetCallback()
371 * @param array $extOpts
372 * @param bool $versioned
374 function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
375 $cache = $this->cache
;
377 $mockWallClock = 1549343530.2053;
378 $cache->setMockTime( $mockWallClock );
380 $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
384 return 'xxx' . $wasSet;
387 $key = wfRandomString();
390 $touchedCallback = function () use ( &$touched ) {
393 $v = $cache->getWithSetCallback(
395 $cache::TTL_INDEFINITE
,
397 [ 'touchedCallback' => $touchedCallback ] +
$extOpts
399 $mockWallClock +
= 60;
400 $v = $cache->getWithSetCallback(
402 $cache::TTL_INDEFINITE
,
404 [ 'touchedCallback' => $touchedCallback ] +
$extOpts
406 $this->assertEquals( 'xxx1', $v, "Value was computed once" );
407 $this->assertEquals( 1, $wasSet, "Value was computed once" );
409 $touched = $mockWallClock - 10;
410 $v = $cache->getWithSetCallback(
412 $cache::TTL_INDEFINITE
,
414 [ 'touchedCallback' => $touchedCallback ] +
$extOpts
416 $v = $cache->getWithSetCallback(
418 $cache::TTL_INDEFINITE
,
420 [ 'touchedCallback' => $touchedCallback ] +
$extOpts
422 $this->assertEquals( 'xxx2', $v, "Value was recomputed once" );
423 $this->assertEquals( 2, $wasSet, "Value was recomputed once" );
426 public static function getWithSetCallback_provider() {
429 [ [ 'version' => 1 ], true ]
433 public function testPreemtiveRefresh() {
436 $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, &$value )
442 $cache = new NearExpiringWANObjectCache( [
443 'cache' => new HashBagOStuff(),
448 $key = wfRandomString();
449 $opts = [ 'lowTTL' => 30 ];
450 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
451 $this->assertEquals( $value, $v, "Value returned" );
452 $this->assertEquals( 1, $wasSet, "Value calculated" );
453 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
454 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
457 $key = wfRandomString();
458 $opts = [ 'lowTTL' => 1 ];
459 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
460 $this->assertEquals( $value, $v, "Value returned" );
461 $this->assertEquals( 1, $wasSet, "Value calculated" );
462 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
463 $this->assertEquals( 1, $wasSet, "Value cached" );
466 $asyncHandler = function ( $callback ) use ( &$asycList ) {
467 $asycList[] = $callback;
469 $cache = new NearExpiringWANObjectCache( [
470 'cache' => new HashBagOStuff(),
472 'asyncHandler' => $asyncHandler
475 $mockWallClock = 1549343530.2053;
476 $priorTime = $mockWallClock; // reference time
477 $cache->setMockTime( $mockWallClock );
480 $key = wfRandomString();
481 $opts = [ 'lowTTL' => 100 ];
482 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
483 $this->assertEquals( $value, $v, "Value returned" );
484 $this->assertEquals( 1, $wasSet, "Value calculated" );
485 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
486 $this->assertEquals( 1, $wasSet, "Cached value used" );
487 $this->assertEquals( $v, $value, "Value cached" );
489 $mockWallClock +
= 250;
490 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
491 $this->assertEquals( $value, $v, "Value returned" );
492 $this->assertEquals( 1, $wasSet, "Stale value used" );
493 $this->assertEquals( 1, count( $asycList ), "Refresh deferred." );
494 $value = 'NewCatsInTown'; // change callback return value
495 $asycList[0](); // run the refresh callback
497 $this->assertEquals( 2, $wasSet, "Value calculated at later time" );
498 $this->assertEquals( 0, count( $asycList ), "No deferred refreshes added." );
499 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
500 $this->assertEquals( $value, $v, "New value stored" );
502 $cache = new PopularityRefreshingWANObjectCache( [
503 'cache' => new HashBagOStuff(),
507 $mockWallClock = $priorTime;
508 $cache->setMockTime( $mockWallClock );
511 $key = wfRandomString();
512 $opts = [ 'hotTTR' => 900 ];
513 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
514 $this->assertEquals( $value, $v, "Value returned" );
515 $this->assertEquals( 1, $wasSet, "Value calculated" );
517 $mockWallClock +
= 30;
519 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
520 $this->assertEquals( 1, $wasSet, "Value cached" );
522 $mockWallClock = $priorTime;
524 $key = wfRandomString();
525 $opts = [ 'hotTTR' => 10 ];
526 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
527 $this->assertEquals( $value, $v, "Value returned" );
528 $this->assertEquals( 1, $wasSet, "Value calculated" );
530 $mockWallClock +
= 30;
532 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
533 $this->assertEquals( $value, $v, "Value returned" );
534 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
538 * @covers WANObjectCache::getWithSetCallback()
539 * @covers WANObjectCache::doGetWithSetCallback()
541 public function testGetWithSetCallback_invalidCallback() {
542 $this->setExpectedException( InvalidArgumentException
::class );
543 $this->cache
->getWithSetCallback( 'key', 30, 'invalid callback' );
547 * @dataProvider getMultiWithSetCallback_provider
548 * @covers WANObjectCache::getMultiWithSetCallback
549 * @covers WANObjectCache::makeMultiKeys
550 * @covers WANObjectCache::getMulti
551 * @param array $extOpts
552 * @param bool $versioned
554 public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
555 $cache = $this->cache
;
557 $keyA = wfRandomString();
558 $keyB = wfRandomString();
559 $keyC = wfRandomString();
560 $cKey1 = wfRandomString();
561 $cKey2 = wfRandomString();
566 $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
567 &$wasSet, &$priorValue, &$priorAsOf
572 $ttl = 20; // override with another value
576 $mockWallClock = 1549343530.2053;
577 $priorTime = $mockWallClock; // reference time
578 $cache->setMockTime( $mockWallClock );
581 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
583 $v = $cache->getMultiWithSetCallback(
584 $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] +
$extOpts );
585 $this->assertEquals( $value, $v[$keyA], "Value returned" );
586 $this->assertEquals( 1, $wasSet, "Value regenerated" );
587 $this->assertFalse( $priorValue, "No prior value" );
588 $this->assertNull( $priorAsOf, "No prior value" );
591 $cache->get( $keyA, $curTTL );
592 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
593 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
597 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
598 $v = $cache->getMultiWithSetCallback(
599 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] +
$extOpts );
600 $this->assertEquals( $value, $v[$keyB], "Value returned" );
601 $this->assertEquals( 1, $wasSet, "Value regenerated" );
602 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
603 $v = $cache->getMultiWithSetCallback(
604 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] +
$extOpts );
605 $this->assertEquals( $value, $v[$keyB], "Value returned" );
606 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
607 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
612 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
613 $v = $cache->getMultiWithSetCallback(
614 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
616 $this->assertEquals( $value, $v[$keyB], "Value returned" );
617 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
618 $this->assertEquals( $value, $priorValue, "Has prior value" );
619 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
620 $t1 = $cache->getCheckKeyTime( $cKey1 );
621 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
622 $t2 = $cache->getCheckKeyTime( $cKey2 );
623 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
625 $mockWallClock +
= 0.01;
626 $priorTime = $mockWallClock;
629 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
630 $v = $cache->getMultiWithSetCallback(
631 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
633 $this->assertEquals( $value, $v[$keyC], "Value returned" );
634 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
635 $t1 = $cache->getCheckKeyTime( $cKey1 );
636 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
637 $t2 = $cache->getCheckKeyTime( $cKey2 );
638 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
641 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
643 $this->assertEquals( $value, $v[$cache::VFLD_DATA
], "Value returned" );
645 $this->assertEquals( $value, $v, "Value returned" );
647 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
650 $key = wfRandomString();
651 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
652 $v = $cache->getMultiWithSetCallback(
653 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] +
$extOpts );
654 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
655 $cache->delete( $key );
656 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
657 $v = $cache->getMultiWithSetCallback(
658 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] +
$extOpts );
659 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
660 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
663 $ids = [ 1, 2, 3, 4, 5, 6 ];
664 $keyFunc = function ( $id, WANObjectCache
$wanCache ) {
665 return $wanCache->makeKey( 'test', $id );
667 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
668 $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
673 $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
676 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
677 array_values( $values ),
678 "Correct values in correct order"
681 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache
) ),
682 array_keys( $values ),
683 "Correct keys in correct order"
685 $this->assertEquals( count( $ids ), $calls );
687 $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
688 $this->assertEquals( count( $ids ), $calls, "Values cached" );
690 // Mock the BagOStuff to assure only one getMulti() call given process caching
691 $localBag = $this->getMockBuilder( HashBagOStuff
::class )
692 ->setMethods( [ 'getMulti' ] )->getMock();
693 $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
694 WANObjectCache
::VALUE_KEY_PREFIX
. 'k1' => 'val-id1',
695 WANObjectCache
::VALUE_KEY_PREFIX
. 'k2' => 'val-id2'
697 $wanCache = new WANObjectCache( [ 'cache' => $localBag, 'pool' => 'testcache-hash' ] );
699 // Warm the process cache
700 $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] );
702 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
703 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
705 // Use the process cache
707 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
708 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
712 public static function getMultiWithSetCallback_provider() {
715 [ [ 'version' => 1 ], true ]
720 * @dataProvider getMultiWithUnionSetCallback_provider
721 * @covers WANObjectCache::getMultiWithUnionSetCallback()
722 * @covers WANObjectCache::makeMultiKeys()
723 * @param array $extOpts
724 * @param bool $versioned
726 public function testGetMultiWithUnionSetCallback( array $extOpts, $versioned ) {
727 $cache = $this->cache
;
729 $keyA = wfRandomString();
730 $keyB = wfRandomString();
731 $keyC = wfRandomString();
732 $cKey1 = wfRandomString();
733 $cKey2 = wfRandomString();
736 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use (
737 &$wasSet, &$priorValue, &$priorAsOf
740 foreach ( $ids as $id ) {
742 $newValues[$id] = "@$id$";
743 $ttls[$id] = 20; // override with another value
749 $mockWallClock = 1549343530.2053;
750 $priorTime = $mockWallClock; // reference time
751 $cache->setMockTime( $mockWallClock );
754 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
756 $v = $cache->getMultiWithUnionSetCallback(
757 $keyedIds, 30, $genFunc, $extOpts );
758 $this->assertEquals( $value, $v[$keyA], "Value returned" );
759 $this->assertEquals( 1, $wasSet, "Value regenerated" );
762 $cache->get( $keyA, $curTTL );
763 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
764 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
768 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
769 $v = $cache->getMultiWithUnionSetCallback(
770 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] +
$extOpts );
771 $this->assertEquals( $value, $v[$keyB], "Value returned" );
772 $this->assertEquals( 1, $wasSet, "Value regenerated" );
773 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
774 $v = $cache->getMultiWithUnionSetCallback(
775 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] +
$extOpts );
776 $this->assertEquals( $value, $v[$keyB], "Value returned" );
777 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
778 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
783 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
784 $v = $cache->getMultiWithUnionSetCallback(
785 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
787 $this->assertEquals( $value, $v[$keyB], "Value returned" );
788 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
789 $t1 = $cache->getCheckKeyTime( $cKey1 );
790 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
791 $t2 = $cache->getCheckKeyTime( $cKey2 );
792 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
794 $mockWallClock +
= 0.01;
795 $priorTime = $mockWallClock;
798 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
799 $v = $cache->getMultiWithUnionSetCallback(
800 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] +
$extOpts
802 $this->assertEquals( $value, $v[$keyC], "Value returned" );
803 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
804 $t1 = $cache->getCheckKeyTime( $cKey1 );
805 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
806 $t2 = $cache->getCheckKeyTime( $cKey2 );
807 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
810 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
812 $this->assertEquals( $value, $v[$cache::VFLD_DATA
], "Value returned" );
814 $this->assertEquals( $value, $v, "Value returned" );
816 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
819 $key = wfRandomString();
820 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
821 $v = $cache->getMultiWithUnionSetCallback(
822 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] +
$extOpts );
823 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
824 $cache->delete( $key );
825 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
826 $v = $cache->getMultiWithUnionSetCallback(
827 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] +
$extOpts );
828 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
829 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
832 $ids = [ 1, 2, 3, 4, 5, 6 ];
833 $keyFunc = function ( $id, WANObjectCache
$wanCache ) {
834 return $wanCache->makeKey( 'test', $id );
836 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
837 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use ( &$calls ) {
839 foreach ( $ids as $id ) {
841 $newValues[$id] = "val-{$id}";
846 $values = $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
849 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
850 array_values( $values ),
851 "Correct values in correct order"
854 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache
) ),
855 array_keys( $values ),
856 "Correct keys in correct order"
858 $this->assertEquals( count( $ids ), $calls );
860 $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
861 $this->assertEquals( count( $ids ), $calls, "Values cached" );
864 public static function getMultiWithUnionSetCallback_provider() {
867 [ [ 'version' => 1 ], true ]
872 * @covers WANObjectCache::getWithSetCallback()
873 * @covers WANObjectCache::doGetWithSetCallback()
875 public function testLockTSE() {
876 $cache = $this->cache
;
877 $key = wfRandomString();
878 $value = wfRandomString();
881 $func = function () use ( &$calls, $value, $cache, $key ) {
886 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
887 $this->assertEquals( $value, $ret );
888 $this->assertEquals( 1, $calls, 'Value was populated' );
890 // Acquire the mutex to verify that getWithSetCallback uses lockTSE properly
891 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
893 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
894 $ret = $cache->getWithSetCallback( $key, 30, $func,
895 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
896 $this->assertEquals( $value, $ret, 'Old value used' );
897 $this->assertEquals( 1, $calls, 'Callback was not used' );
899 $cache->delete( $key );
900 $ret = $cache->getWithSetCallback( $key, 30, $func,
901 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
902 $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
903 $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
905 $ret = $cache->getWithSetCallback( $key, 30, $func,
906 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
907 $this->assertEquals( $value, $ret, 'Callback was not used; used interim (mutex failed)' );
908 $this->assertEquals( 2, $calls, 'Callback was not used; used interim (mutex failed)' );
912 * @covers WANObjectCache::getWithSetCallback()
913 * @covers WANObjectCache::doGetWithSetCallback()
914 * @covers WANObjectCache::set()
916 public function testLockTSESlow() {
917 $cache = $this->cache
;
918 $key = wfRandomString();
919 $value = wfRandomString();
922 $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
924 $setOpts['since'] = microtime( true ) - 10;
925 // Immediately kill any mutex rather than waiting a second
926 $cache->delete( $cache::MUTEX_KEY_PREFIX
. $key );
930 // Value should be marked as stale due to snapshot lag
932 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
933 $this->assertEquals( $value, $ret );
934 $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
935 $this->assertLessThan( 0, $curTTL, 'Value has negative curTTL' );
936 $this->assertEquals( 1, $calls, 'Value was generated' );
938 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
939 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
940 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
941 $this->assertEquals( $value, $ret );
942 $this->assertEquals( 1, $calls, 'Callback was not used' );
946 * @covers WANObjectCache::getWithSetCallback()
947 * @covers WANObjectCache::doGetWithSetCallback()
949 public function testBusyValue() {
950 $cache = $this->cache
;
951 $key = wfRandomString();
952 $value = wfRandomString();
953 $busyValue = wfRandomString();
956 $func = function () use ( &$calls, $value, $cache, $key ) {
958 // Immediately kill any mutex rather than waiting a second
959 $cache->delete( $cache::MUTEX_KEY_PREFIX
. $key );
963 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
964 $this->assertEquals( $value, $ret );
965 $this->assertEquals( 1, $calls, 'Value was populated' );
967 // Acquire a lock to verify that getWithSetCallback uses busyValue properly
968 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
970 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
971 $ret = $cache->getWithSetCallback( $key, 30, $func,
972 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
973 $this->assertEquals( $value, $ret, 'Callback used' );
974 $this->assertEquals( 2, $calls, 'Callback used' );
976 $ret = $cache->getWithSetCallback( $key, 30, $func,
977 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
978 $this->assertEquals( $value, $ret, 'Old value used' );
979 $this->assertEquals( 2, $calls, 'Callback was not used' );
981 $cache->delete( $key ); // no value at all anymore and still locked
982 $ret = $cache->getWithSetCallback( $key, 30, $func,
983 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
984 $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
985 $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
987 $this->internalCache
->delete( $cache::MUTEX_KEY_PREFIX
. $key );
988 $ret = $cache->getWithSetCallback( $key, 30, $func,
989 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
990 $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
991 $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
993 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
994 $ret = $cache->getWithSetCallback( $key, 30, $func,
995 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
996 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
997 $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
1001 * @covers WANObjectCache::getMulti()
1003 public function testGetMulti() {
1004 $cache = $this->cache
;
1006 $value1 = [ 'this' => 'is', 'a' => 'test' ];
1007 $value2 = [ 'this' => 'is', 'another' => 'test' ];
1009 $key1 = wfRandomString();
1010 $key2 = wfRandomString();
1011 $key3 = wfRandomString();
1013 $mockWallClock = 1549343530.2053;
1014 $priorTime = $mockWallClock; // reference time
1015 $cache->setMockTime( $mockWallClock );
1017 $cache->set( $key1, $value1, 5 );
1018 $cache->set( $key2, $value2, 10 );
1021 $this->assertEquals(
1022 [ $key1 => $value1, $key2 => $value2 ],
1023 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
1024 'Result array populated'
1027 $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
1028 $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
1029 $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
1031 $cKey1 = wfRandomString();
1032 $cKey2 = wfRandomString();
1034 $mockWallClock +
= 1;
1037 $this->assertEquals(
1038 [ $key1 => $value1, $key2 => $value2 ],
1039 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
1040 "Result array populated even with new check keys"
1042 $t1 = $cache->getCheckKeyTime( $cKey1 );
1043 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
1044 $t2 = $cache->getCheckKeyTime( $cKey2 );
1045 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
1046 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
1047 $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
1048 $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
1050 $mockWallClock +
= 1;
1053 $this->assertEquals(
1054 [ $key1 => $value1, $key2 => $value2 ],
1055 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
1056 "Result array still populated even with new check keys"
1058 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
1059 $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
1060 $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
1064 * @covers WANObjectCache::getMulti()
1065 * @covers WANObjectCache::processCheckKeys()
1067 public function testGetMultiCheckKeys() {
1068 $cache = $this->cache
;
1070 $checkAll = wfRandomString();
1071 $check1 = wfRandomString();
1072 $check2 = wfRandomString();
1073 $check3 = wfRandomString();
1074 $value1 = wfRandomString();
1075 $value2 = wfRandomString();
1077 $mockWallClock = 1549343530.2053;
1078 $cache->setMockTime( $mockWallClock );
1080 // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
1081 // several seconds during the test to assert the behaviour.
1082 foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
1083 $cache->touchCheckKey( $checkKey, WANObjectCache
::HOLDOFF_NONE
);
1086 $mockWallClock +
= 0.100;
1088 $cache->set( 'key1', $value1, 10 );
1089 $cache->set( 'key2', $value2, 10 );
1092 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1098 $this->assertEquals(
1099 [ 'key1' => $value1, 'key2' => $value2 ],
1103 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
1104 $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
1105 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
1106 $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
1108 $mockWallClock +
= 0.100;
1109 $cache->touchCheckKey( $check1 );
1112 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1118 $this->assertEquals(
1119 [ 'key1' => $value1, 'key2' => $value2 ],
1121 'key1 expired by check1, but value still provided'
1123 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
1124 $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
1126 $cache->touchCheckKey( $checkAll );
1129 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1135 $this->assertEquals(
1136 [ 'key1' => $value1, 'key2' => $value2 ],
1138 'All keys expired by checkAll, but value still provided'
1140 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
1141 $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
1145 * @covers WANObjectCache::get()
1146 * @covers WANObjectCache::processCheckKeys()
1148 public function testCheckKeyInitHoldoff() {
1149 $cache = $this->cache
;
1151 for ( $i = 0; $i < 500; ++
$i ) {
1152 $key = wfRandomString();
1153 $checkKey = wfRandomString();
1155 $cache->get( $key, $curTTL, [ $checkKey ] );
1156 $cache->set( $key, 'val', 10 );
1158 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
1160 $this->assertEquals( 'val', $v );
1161 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
1164 for ( $i = 0; $i < 500; ++
$i ) {
1165 $key = wfRandomString();
1166 $checkKey = wfRandomString();
1168 $cache->set( $key, 'val', 10 );
1170 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
1172 $this->assertEquals( 'val', $v );
1173 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
1178 * @covers WANObjectCache::delete
1179 * @covers WANObjectCache::relayDelete
1180 * @covers WANObjectCache::relayPurge
1182 public function testDelete() {
1183 $key = wfRandomString();
1184 $value = wfRandomString();
1185 $this->cache
->set( $key, $value );
1188 $v = $this->cache
->get( $key, $curTTL );
1189 $this->assertEquals( $value, $v, "Key was created with value" );
1190 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
1192 $this->cache
->delete( $key );
1195 $v = $this->cache
->get( $key, $curTTL );
1196 $this->assertFalse( $v, "Deleted key has false value" );
1197 $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
1199 $this->cache
->set( $key, $value . 'more' );
1200 $v = $this->cache
->get( $key, $curTTL );
1201 $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
1202 $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
1204 $this->cache
->set( $key, $value );
1205 $this->cache
->delete( $key, WANObjectCache
::HOLDOFF_NONE
);
1208 $v = $this->cache
->get( $key, $curTTL );
1209 $this->assertFalse( $v, "Deleted key has false value" );
1210 $this->assertNull( $curTTL, "Deleted key has null current TTL" );
1212 $this->cache
->set( $key, $value );
1213 $v = $this->cache
->get( $key, $curTTL );
1214 $this->assertEquals( $value, $v, "Key was created with value" );
1215 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
1219 * @dataProvider getWithSetCallback_versions_provider
1220 * @covers WANObjectCache::getWithSetCallback()
1221 * @covers WANObjectCache::doGetWithSetCallback()
1222 * @param array $extOpts
1223 * @param bool $versioned
1225 public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
1226 $cache = $this->cache
;
1228 $key = wfRandomString();
1229 $valueV1 = wfRandomString();
1230 $valueV2 = [ wfRandomString() ];
1233 $funcV1 = function () use ( &$wasSet, $valueV1 ) {
1239 $priorValue = false;
1241 $funcV2 = function ( $oldValue, &$ttl, $setOpts, $oldAsOf )
1242 use ( &$wasSet, $valueV2, &$priorValue, &$priorAsOf ) {
1243 $priorValue = $oldValue;
1244 $priorAsOf = $oldAsOf;
1247 return $valueV2; // new array format
1250 // Set the main key (version N if versioned)
1252 $v = $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
1253 $this->assertEquals( $valueV1, $v, "Value returned" );
1254 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1255 $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
1256 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
1257 $this->assertEquals( $valueV1, $v, "Value not regenerated" );
1260 // Set the key for version N+1 format
1261 $verOpts = [ 'version' => $extOpts['version'] +
1 ];
1263 // Start versioning now with the unversioned key still there
1264 $verOpts = [ 'version' => 1 ];
1267 // Value goes to secondary key since V1 already used $key
1269 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts +
$extOpts );
1270 $this->assertEquals( $valueV2, $v, "Value returned" );
1271 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1272 $this->assertEquals( false, $priorValue, "Old value not given due to old format" );
1273 $this->assertEquals( null, $priorAsOf, "Old value not given due to old format" );
1276 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts +
$extOpts );
1277 $this->assertEquals( $valueV2, $v, "Value not regenerated (secondary key)" );
1278 $this->assertEquals( 0, $wasSet, "Value not regenerated (secondary key)" );
1280 // Clear out the older or unversioned key
1281 $cache->delete( $key, 0 );
1283 // Set the key for next/first versioned format
1285 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts +
$extOpts );
1286 $this->assertEquals( $valueV2, $v, "Value returned" );
1287 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1289 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts +
$extOpts );
1290 $this->assertEquals( $valueV2, $v, "Value not regenerated (main key)" );
1291 $this->assertEquals( 1, $wasSet, "Value not regenerated (main key)" );
1294 public static function getWithSetCallback_versions_provider() {
1297 [ [ 'version' => 1 ], true ]
1302 * @covers WANObjectCache::useInterimHoldOffCaching
1303 * @covers WANObjectCache::getInterimValue
1305 public function testInterimHoldOffCaching() {
1306 $cache = $this->cache
;
1308 $value = 'CRL-40-940';
1310 $func = function () use ( &$wasCalled, $value ) {
1316 $cache->useInterimHoldOffCaching( true );
1318 $key = wfRandomString( 32 );
1319 $v = $cache->getWithSetCallback( $key, 60, $func );
1320 $v = $cache->getWithSetCallback( $key, 60, $func );
1321 $this->assertEquals( 1, $wasCalled, 'Value cached' );
1322 $cache->delete( $key );
1323 $v = $cache->getWithSetCallback( $key, 60, $func );
1324 $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
1325 $v = $cache->getWithSetCallback( $key, 60, $func );
1326 $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
1327 // Lock up the mutex so interim cache is used
1328 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
1329 $v = $cache->getWithSetCallback( $key, 60, $func );
1330 $this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' );
1331 $this->internalCache
->delete( $cache::MUTEX_KEY_PREFIX
. $key );
1333 $cache->useInterimHoldOffCaching( false );
1336 $key = wfRandomString( 32 );
1337 $v = $cache->getWithSetCallback( $key, 60, $func );
1338 $v = $cache->getWithSetCallback( $key, 60, $func );
1339 $this->assertEquals( 1, $wasCalled, 'Value cached' );
1340 $cache->delete( $key );
1341 $v = $cache->getWithSetCallback( $key, 60, $func );
1342 $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' );
1343 $v = $cache->getWithSetCallback( $key, 60, $func );
1344 $this->assertEquals( 3, $wasCalled, 'Value still regenerated (got mutex)' );
1345 $v = $cache->getWithSetCallback( $key, 60, $func );
1346 $this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' );
1347 // Lock up the mutex so interim cache is used
1348 $this->internalCache
->add( $cache::MUTEX_KEY_PREFIX
. $key, 1, 0 );
1349 $v = $cache->getWithSetCallback( $key, 60, $func );
1350 $this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' );
1354 * @covers WANObjectCache::touchCheckKey
1355 * @covers WANObjectCache::resetCheckKey
1356 * @covers WANObjectCache::getCheckKeyTime
1357 * @covers WANObjectCache::getMultiCheckKeyTime
1358 * @covers WANObjectCache::makePurgeValue
1359 * @covers WANObjectCache::parsePurgeValue
1361 public function testTouchKeys() {
1362 $cache = $this->cache
;
1363 $key = wfRandomString();
1365 $mockWallClock = 1549343530.2053;
1366 $priorTime = $mockWallClock; // reference time
1367 $cache->setMockTime( $mockWallClock );
1369 $mockWallClock +
= 0.100;
1370 $t0 = $cache->getCheckKeyTime( $key );
1371 $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
1373 $priorTime = $mockWallClock;
1374 $mockWallClock +
= 0.100;
1375 $cache->touchCheckKey( $key );
1376 $t1 = $cache->getCheckKeyTime( $key );
1377 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
1379 $t2 = $cache->getCheckKeyTime( $key );
1380 $this->assertEquals( $t1, $t2, 'Check key time did not change' );
1382 $mockWallClock +
= 0.100;
1383 $cache->touchCheckKey( $key );
1384 $t3 = $cache->getCheckKeyTime( $key );
1385 $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
1387 $t4 = $cache->getCheckKeyTime( $key );
1388 $this->assertEquals( $t3, $t4, 'Check key time did not change' );
1390 $mockWallClock +
= 0.100;
1391 $cache->resetCheckKey( $key );
1392 $t5 = $cache->getCheckKeyTime( $key );
1393 $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
1395 $t6 = $cache->getCheckKeyTime( $key );
1396 $this->assertEquals( $t5, $t6, 'Check key time did not change' );
1400 * @covers WANObjectCache::getMulti()
1402 public function testGetWithSeveralCheckKeys() {
1403 $key = wfRandomString();
1404 $tKey1 = wfRandomString();
1405 $tKey2 = wfRandomString();
1408 $mockWallClock = 1549343530.2053;
1409 $priorTime = $mockWallClock; // reference time
1410 $this->cache
->setMockTime( $mockWallClock );
1412 // Two check keys are newer (given hold-off) than $key, another is older
1413 $this->internalCache
->set(
1414 WANObjectCache
::TIME_KEY_PREFIX
. $tKey2,
1415 WANObjectCache
::PURGE_VAL_PREFIX
. ( $priorTime - 3 )
1417 $this->internalCache
->set(
1418 WANObjectCache
::TIME_KEY_PREFIX
. $tKey2,
1419 WANObjectCache
::PURGE_VAL_PREFIX
. ( $priorTime - 5 )
1421 $this->internalCache
->set(
1422 WANObjectCache
::TIME_KEY_PREFIX
. $tKey1,
1423 WANObjectCache
::PURGE_VAL_PREFIX
. ( $priorTime - 30 )
1425 $this->cache
->set( $key, $value, 30 );
1428 $v = $this->cache
->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
1429 $this->assertEquals( $value, $v, "Value matches" );
1430 $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
1431 $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
1435 * @covers WANObjectCache::reap()
1436 * @covers WANObjectCache::reapCheckKey()
1438 public function testReap() {
1439 $vKey1 = wfRandomString();
1440 $vKey2 = wfRandomString();
1441 $tKey1 = wfRandomString();
1442 $tKey2 = wfRandomString();
1445 $knownPurge = time() - 60;
1446 $goodTime = microtime( true ) - 5;
1447 $badTime = microtime( true ) - 300;
1449 $this->internalCache
->set(
1450 WANObjectCache
::VALUE_KEY_PREFIX
. $vKey1,
1452 WANObjectCache
::FLD_VERSION
=> WANObjectCache
::VERSION
,
1453 WANObjectCache
::FLD_VALUE
=> $value,
1454 WANObjectCache
::FLD_TTL
=> 3600,
1455 WANObjectCache
::FLD_TIME
=> $goodTime
1458 $this->internalCache
->set(
1459 WANObjectCache
::VALUE_KEY_PREFIX
. $vKey2,
1461 WANObjectCache
::FLD_VERSION
=> WANObjectCache
::VERSION
,
1462 WANObjectCache
::FLD_VALUE
=> $value,
1463 WANObjectCache
::FLD_TTL
=> 3600,
1464 WANObjectCache
::FLD_TIME
=> $badTime
1467 $this->internalCache
->set(
1468 WANObjectCache
::TIME_KEY_PREFIX
. $tKey1,
1469 WANObjectCache
::PURGE_VAL_PREFIX
. $goodTime
1471 $this->internalCache
->set(
1472 WANObjectCache
::TIME_KEY_PREFIX
. $tKey2,
1473 WANObjectCache
::PURGE_VAL_PREFIX
. $badTime
1476 $this->assertEquals( $value, $this->cache
->get( $vKey1 ) );
1477 $this->assertEquals( $value, $this->cache
->get( $vKey2 ) );
1478 $this->cache
->reap( $vKey1, $knownPurge, $bad1 );
1479 $this->cache
->reap( $vKey2, $knownPurge, $bad2 );
1481 $this->assertFalse( $bad1 );
1482 $this->assertTrue( $bad2 );
1484 $this->cache
->reapCheckKey( $tKey1, $knownPurge, $tBad1 );
1485 $this->cache
->reapCheckKey( $tKey2, $knownPurge, $tBad2 );
1486 $this->assertFalse( $tBad1 );
1487 $this->assertTrue( $tBad2 );
1491 * @covers WANObjectCache::reap()
1493 public function testReap_fail() {
1494 $backend = $this->getMockBuilder( EmptyBagOStuff
::class )
1495 ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
1496 $backend->expects( $this->once() )->method( 'get' )
1498 WANObjectCache
::FLD_VERSION
=> WANObjectCache
::VERSION
,
1499 WANObjectCache
::FLD_VALUE
=> 'value',
1500 WANObjectCache
::FLD_TTL
=> 3600,
1501 WANObjectCache
::FLD_TIME
=> 300,
1503 $backend->expects( $this->once() )->method( 'changeTTL' )
1504 ->willReturn( false );
1506 $wanCache = new WANObjectCache( [
1507 'cache' => $backend,
1508 'pool' => 'testcache-hash',
1509 'relayer' => new EventRelayerNull( [] )
1513 $ret = $wanCache->reap( 'key', 360, $isStale );
1514 $this->assertTrue( $isStale, 'value was stale' );
1515 $this->assertFalse( $ret, 'changeTTL failed' );
1519 * @covers WANObjectCache::set()
1521 public function testSetWithLag() {
1524 $key = wfRandomString();
1525 $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
1526 $this->cache
->set( $key, $value, 30, $opts );
1527 $this->assertEquals( $value, $this->cache
->get( $key ), "Rep-lagged value written." );
1529 $key = wfRandomString();
1530 $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
1531 $this->cache
->set( $key, $value, 30, $opts );
1532 $this->assertEquals( false, $this->cache
->get( $key ), "Trx-lagged value not written." );
1534 $key = wfRandomString();
1535 $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
1536 $this->cache
->set( $key, $value, 30, $opts );
1537 $this->assertEquals( false, $this->cache
->get( $key ), "Lagged value not written." );
1541 * @covers WANObjectCache::set()
1543 public function testWritePending() {
1546 $key = wfRandomString();
1547 $opts = [ 'pending' => true ];
1548 $this->cache
->set( $key, $value, 30, $opts );
1549 $this->assertEquals( false, $this->cache
->get( $key ), "Pending value not written." );
1552 public function testMcRouterSupport() {
1553 $localBag = $this->getMockBuilder( EmptyBagOStuff
::class )
1554 ->setMethods( [ 'set', 'delete' ] )->getMock();
1555 $localBag->expects( $this->never() )->method( 'set' );
1556 $localBag->expects( $this->never() )->method( 'delete' );
1557 $wanCache = new WANObjectCache( [
1558 'cache' => $localBag,
1559 'pool' => 'testcache-hash',
1560 'relayer' => new EventRelayerNull( [] ),
1561 'mcrouterAware' => true,
1562 'region' => 'pmtpa',
1563 'cluster' => 'mw-wan'
1565 $valFunc = function () {
1569 // None of these should use broadcasting commands (e.g. SET, DELETE)
1570 $wanCache->get( 'x' );
1571 $wanCache->get( 'x', $ctl, [ 'check1' ] );
1572 $wanCache->getMulti( [ 'x', 'y' ] );
1573 $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
1574 $wanCache->getWithSetCallback( 'p', 30, $valFunc );
1575 $wanCache->getCheckKeyTime( 'zzz' );
1576 $wanCache->reap( 'x', time() - 300 );
1577 $wanCache->reap( 'zzz', time() - 300 );
1580 public function testMcRouterSupportBroadcastDelete() {
1581 $localBag = $this->getMockBuilder( EmptyBagOStuff
::class )
1582 ->setMethods( [ 'set' ] )->getMock();
1583 $wanCache = new WANObjectCache( [
1584 'cache' => $localBag,
1585 'pool' => 'testcache-hash',
1586 'relayer' => new EventRelayerNull( [] ),
1587 'mcrouterAware' => true,
1588 'region' => 'pmtpa',
1589 'cluster' => 'mw-wan'
1592 $localBag->expects( $this->once() )->method( 'set' )
1593 ->with( "/*/mw-wan/" . $wanCache::VALUE_KEY_PREFIX
. "test" );
1595 $wanCache->delete( 'test' );
1598 public function testMcRouterSupportBroadcastTouchCK() {
1599 $localBag = $this->getMockBuilder( EmptyBagOStuff
::class )
1600 ->setMethods( [ 'set' ] )->getMock();
1601 $wanCache = new WANObjectCache( [
1602 'cache' => $localBag,
1603 'pool' => 'testcache-hash',
1604 'relayer' => new EventRelayerNull( [] ),
1605 'mcrouterAware' => true,
1606 'region' => 'pmtpa',
1607 'cluster' => 'mw-wan'
1610 $localBag->expects( $this->once() )->method( 'set' )
1611 ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX
. "test" );
1613 $wanCache->touchCheckKey( 'test' );
1616 public function testMcRouterSupportBroadcastResetCK() {
1617 $localBag = $this->getMockBuilder( EmptyBagOStuff
::class )
1618 ->setMethods( [ 'delete' ] )->getMock();
1619 $wanCache = new WANObjectCache( [
1620 'cache' => $localBag,
1621 'pool' => 'testcache-hash',
1622 'relayer' => new EventRelayerNull( [] ),
1623 'mcrouterAware' => true,
1624 'region' => 'pmtpa',
1625 'cluster' => 'mw-wan'
1628 $localBag->expects( $this->once() )->method( 'delete' )
1629 ->with( "/*/mw-wan/" . $wanCache::TIME_KEY_PREFIX
. "test" );
1631 $wanCache->resetCheckKey( 'test' );
1634 public function testEpoch() {
1635 $bag = new HashBagOStuff();
1636 $cache = new WANObjectCache( [ 'cache' => $bag, 'pool' => 'testcache-hash' ] );
1637 $key = $cache->makeGlobalKey( 'The whole of the Law' );
1639 $now = microtime( true );
1640 $cache->setMockTime( $now );
1642 $cache->set( $key, 'Do what thou Wilt' );
1643 $cache->touchCheckKey( $key );
1647 $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
1648 $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key init', 0.01 );
1650 $cache = new WANObjectCache( [
1652 'pool' => 'testcache-hash',
1653 'epoch' => $now - 3600
1655 $cache->setMockTime( $now );
1657 $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
1658 $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key kept', 0.01 );
1661 $cache = new WANObjectCache( [
1663 'pool' => 'testcache-hash',
1664 'epoch' => $now +
3600
1666 $cache->setMockTime( $now );
1668 $this->assertFalse( $cache->get( $key ), 'Key rejected due to epoch' );
1669 $this->assertEquals( $now, $cache->getCheckKeyTime( $key ), 'Check key reset', 0.01 );
1673 * @dataProvider provideAdaptiveTTL
1674 * @covers WANObjectCache::adaptiveTTL()
1675 * @param float|int $ago
1676 * @param int $maxTTL
1677 * @param int $minTTL
1678 * @param float $factor
1679 * @param int $adaptiveTTL
1681 public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
1682 $mtime = $ago ?
time() - $ago : $ago;
1684 $ttl = $this->cache
->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
1686 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1687 $this->assertLessThanOrEqual( $adaptiveTTL +
$margin, $ttl );
1689 $ttl = $this->cache
->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
1691 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1692 $this->assertLessThanOrEqual( $adaptiveTTL +
$margin, $ttl );
1695 public static function provideAdaptiveTTL() {
1697 [ 3600, 900, 30, 0.2, 720 ],
1698 [ 3600, 500, 30, 0.2, 500 ],
1699 [ 3600, 86400, 800, 0.2, 800 ],
1700 [ false, 86400, 800, 0.2, 800 ],
1701 [ null, 86400, 800, 0.2, 800 ]
1706 * @covers WANObjectCache::__construct
1707 * @covers WANObjectCache::newEmpty
1709 public function testNewEmpty() {
1710 $this->assertInstanceOf(
1711 WANObjectCache
::class,
1712 WANObjectCache
::newEmpty()
1717 * @covers WANObjectCache::setLogger
1719 public function testSetLogger() {
1720 $this->assertSame( null, $this->cache
->setLogger( new Psr\Log\NullLogger
) );
1724 * @covers WANObjectCache::getQoS
1726 public function testGetQoS() {
1727 $backend = $this->getMockBuilder( HashBagOStuff
::class )
1728 ->setMethods( [ 'getQoS' ] )->getMock();
1729 $backend->expects( $this->once() )->method( 'getQoS' )
1730 ->willReturn( BagOStuff
::QOS_UNKNOWN
);
1731 $wanCache = new WANObjectCache( [ 'cache' => $backend ] );
1734 $wanCache::QOS_UNKNOWN
,
1735 $wanCache->getQoS( $wanCache::ATTR_EMULATION
)
1740 * @covers WANObjectCache::makeKey
1742 public function testMakeKey() {
1743 $backend = $this->getMockBuilder( HashBagOStuff
::class )
1744 ->setMethods( [ 'makeKey' ] )->getMock();
1745 $backend->expects( $this->once() )->method( 'makeKey' )
1746 ->willReturn( 'special' );
1748 $wanCache = new WANObjectCache( [
1749 'cache' => $backend,
1750 'pool' => 'testcache-hash',
1751 'relayer' => new EventRelayerNull( [] )
1754 $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) );
1758 * @covers WANObjectCache::makeGlobalKey
1760 public function testMakeGlobalKey() {
1761 $backend = $this->getMockBuilder( HashBagOStuff
::class )
1762 ->setMethods( [ 'makeGlobalKey' ] )->getMock();
1763 $backend->expects( $this->once() )->method( 'makeGlobalKey' )
1764 ->willReturn( 'special' );
1766 $wanCache = new WANObjectCache( [
1767 'cache' => $backend,
1768 'pool' => 'testcache-hash',
1769 'relayer' => new EventRelayerNull( [] )
1772 $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) );
1775 public static function statsKeyProvider() {
1777 [ 'domain:page:5', 'page' ],
1778 [ 'domain:main-key', 'main-key' ],
1779 [ 'domain:page:history', 'page' ],
1780 [ 'missingdomainkey', 'missingdomainkey' ]
1785 * @dataProvider statsKeyProvider
1786 * @covers WANObjectCache::determineKeyClass
1788 public function testStatsKeyClass( $key, $class ) {
1789 $wanCache = TestingAccessWrapper
::newFromObject( new WANObjectCache( [
1790 'cache' => new HashBagOStuff
,
1791 'pool' => 'testcache-hash',
1792 'relayer' => new EventRelayerNull( [] )
1795 $this->assertEquals( $class, $wanCache->determineKeyClass( $key ) );
1799 class NearExpiringWANObjectCache
extends WANObjectCache
{
1800 const CLOCK_SKEW
= 1;
1802 protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
1803 return ( $curTTL > 0 && ( $curTTL + self
::CLOCK_SKEW
) < $lowTTL );
1807 class PopularityRefreshingWANObjectCache
extends WANObjectCache
{
1808 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
1809 return ( ( $now - $asOf ) > $timeTillRefresh );