0c084e058a6472a0f12784794269965e90c7d365
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / objectcache / WANObjectCacheTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 /**
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::getNonProcessCachedMultiKeys
15 * @covers WANObjectCache::getRawKeysForWarmup
16 * @covers WANObjectCache::getInterimValue
17 * @covers WANObjectCache::setInterimValue
18 */
19 class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
20
21 use MediaWikiCoversValidator;
22 use PHPUnit4And6Compat;
23
24 /** @var WANObjectCache */
25 private $cache;
26 /** @var BagOStuff */
27 private $internalCache;
28
29 protected function setUp() {
30 parent::setUp();
31
32 $this->cache = new WANObjectCache( [
33 'cache' => new HashBagOStuff()
34 ] );
35
36 $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
37 /** @noinspection PhpUndefinedFieldInspection */
38 $this->internalCache = $wanCache->cache;
39 }
40
41 /**
42 * @dataProvider provideSetAndGet
43 * @covers WANObjectCache::set()
44 * @covers WANObjectCache::get()
45 * @covers WANObjectCache::makeKey()
46 * @param mixed $value
47 * @param int $ttl
48 */
49 public function testSetAndGet( $value, $ttl ) {
50 $cache = $this->cache;
51
52 $curTTL = null;
53 $asOf = null;
54 $key = $cache->makeKey( 'x', wfRandomString() );
55
56 $cache->get( $key, $curTTL, [], $asOf );
57 $this->assertNull( $curTTL, "Current TTL is null" );
58 $this->assertNull( $asOf, "Current as-of-time is infinite" );
59
60 $t = microtime( true );
61
62 $cache->set( $key, $value, $cache::TTL_UNCACHEABLE );
63 $cache->get( $key, $curTTL, [], $asOf );
64 $this->assertNull( $curTTL, "Current TTL is null (TTL_UNCACHEABLE)" );
65 $this->assertNull( $asOf, "Current as-of-time is infinite (TTL_UNCACHEABLE)" );
66
67 $cache->set( $key, $value, $ttl );
68
69 $this->assertEquals( $value, $cache->get( $key, $curTTL, [], $asOf ) );
70 if ( is_infinite( $ttl ) || $ttl == 0 ) {
71 $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
72 } else {
73 $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
74 $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
75 }
76 $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
77 $this->assertLessThanOrEqual( $t + 1, $asOf, "As-of-time in range of set() time" );
78 }
79
80 public static function provideSetAndGet() {
81 return [
82 [ 14141, 3 ],
83 [ 3535.666, 3 ],
84 [ [], 3 ],
85 [ null, 3 ],
86 [ '0', 3 ],
87 [ (object)[ 'meow' ], 3 ],
88 [ INF, 3 ],
89 [ '', 3 ],
90 [ 'pizzacat', INF ],
91 ];
92 }
93
94 /**
95 * @covers WANObjectCache::get()
96 * @covers WANObjectCache::makeGlobalKey()
97 */
98 public function testGetNotExists() {
99 $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
100 $curTTL = null;
101 $value = $this->cache->get( $key, $curTTL );
102
103 $this->assertFalse( $value, "Non-existing key has false value" );
104 $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
105 }
106
107 /**
108 * @covers WANObjectCache::set()
109 */
110 public function testSetOver() {
111 $key = wfRandomString();
112 for ( $i = 0; $i < 3; ++$i ) {
113 $value = wfRandomString();
114 $this->cache->set( $key, $value, 3 );
115
116 $this->assertEquals( $this->cache->get( $key ), $value );
117 }
118 }
119
120 /**
121 * @covers WANObjectCache::set()
122 */
123 public function testStaleSet() {
124 $key = wfRandomString();
125 $value = wfRandomString();
126 $this->cache->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
127
128 $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
129 }
130
131 /**
132 * @covers WANObjectCache::getWithSetCallback
133 */
134 public function testProcessCacheTTL() {
135 $cache = $this->cache;
136 $mockWallClock = 1549343530.2053;
137 $cache->setMockTime( $mockWallClock );
138
139 $key = "mykey-" . wfRandomString();
140
141 $hits = 0;
142 $callback = function ( $oldValue, &$ttl, &$setOpts ) use ( &$hits ) {
143 ++$hits;
144 return 42;
145 };
146
147 $cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
148 $cache->delete( $key, $cache::HOLDOFF_NONE ); // clear persistent cache
149 $cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
150 $this->assertEquals( 1, $hits, "Value process cached" );
151
152 $mockWallClock += 6;
153 $cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
154 $this->assertEquals( 2, $hits, "Value expired in process cache" );
155 }
156
157 /**
158 * @covers WANObjectCache::getWithSetCallback
159 */
160 public function testProcessCacheLruAndDelete() {
161 $cache = $this->cache;
162 $mockWallClock = 1549343530.2053;
163 $cache->setMockTime( $mockWallClock );
164
165 $hit = 0;
166 $fn = function () use ( &$hit ) {
167 ++$hit;
168 return 42;
169 };
170 $keysA = [ wfRandomString(), wfRandomString(), wfRandomString() ];
171 $keysB = [ wfRandomString(), wfRandomString(), wfRandomString() ];
172 $pcg = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
173
174 foreach ( $keysA as $i => $key ) {
175 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
176 }
177 $this->assertEquals( 3, $hit, "Values not cached yet" );
178
179 foreach ( $keysA as $i => $key ) {
180 // Should not evict from process cache
181 $cache->delete( $key );
182 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
183 }
184 $this->assertEquals( 3, $hit, "Values cached; not cleared by delete()" );
185
186 foreach ( $keysB as $i => $key ) {
187 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
188 }
189 $this->assertEquals( 6, $hit, "New values not cached yet" );
190
191 foreach ( $keysB as $i => $key ) {
192 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
193 }
194 $this->assertEquals( 6, $hit, "New values cached" );
195
196 foreach ( $keysA as $i => $key ) {
197 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
198 }
199 $this->assertEquals( 9, $hit, "Prior values evicted by new values" );
200 }
201
202 /**
203 * @covers WANObjectCache::getWithSetCallback
204 */
205 public function testProcessCacheInterimKeys() {
206 $cache = $this->cache;
207 $mockWallClock = 1549343530.2053;
208 $cache->setMockTime( $mockWallClock );
209
210 $hit = 0;
211 $fn = function () use ( &$hit ) {
212 ++$hit;
213 return 42;
214 };
215 $keysA = [ wfRandomString(), wfRandomString(), wfRandomString() ];
216 $pcg = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
217
218 foreach ( $keysA as $i => $key ) {
219 $cache->delete( $key ); // tombstone key
220 $mockWallClock += 0.001; // cached values will be newer than tombstone
221 // Get into process cache (specific group) and interim cache
222 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5, 'pcGroup' => $pcg[$i] ] );
223 }
224 $this->assertEquals( 3, $hit );
225
226 // Get into process cache (default group)
227 $key = reset( $keysA );
228 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
229 $this->assertEquals( 3, $hit, "Value recently interim-cached" );
230
231 $mockWallClock += 0.2; // interim key not brand new
232 $cache->clearProcessCache();
233 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
234 $this->assertEquals( 4, $hit, "Value calculated (interim key not recent and reset)" );
235 $cache->getWithSetCallback( $key, 100, $fn, [ 'pcTTL' => 5 ] );
236 $this->assertEquals( 4, $hit, "Value process cached" );
237 }
238
239 /**
240 * @covers WANObjectCache::getWithSetCallback
241 */
242 public function testProcessCacheNesting() {
243 $cache = $this->cache;
244 $mockWallClock = 1549343530.2053;
245 $cache->setMockTime( $mockWallClock );
246
247 $keyOuter = "outer-" . wfRandomString();
248 $keyInner = "inner-" . wfRandomString();
249
250 $innerHit = 0;
251 $innerFn = function () use ( &$innerHit ) {
252 ++$innerHit;
253 return 42;
254 };
255
256 $outerHit = 0;
257 $outerFn = function () use ( $keyInner, $innerFn, $cache, &$outerHit ) {
258 ++$outerHit;
259 $v = $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
260
261 return 43 + $v;
262 };
263
264 $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
265 $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
266
267 $this->assertEquals( 1, $innerHit, "Inner callback value cached" );
268 $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
269 $mockWallClock += 1;
270
271 $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
272 $this->assertEquals( 1, $innerHit, "Inner callback process cached" );
273
274 // Outer key misses and inner key process cache value is refused
275 $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
276
277 $this->assertEquals( 1, $outerHit, "Outer callback value not yet cached" );
278 $this->assertEquals( 2, $innerHit, "Inner callback value process cache skipped" );
279
280 $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
281
282 $this->assertEquals( 1, $outerHit, "Outer callback value cached" );
283
284 $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
285 $cache->delete( $keyOuter, $cache::HOLDOFF_NONE );
286 $mockWallClock += 1;
287 $cache->clearProcessCache();
288 $cache->getWithSetCallback( $keyOuter, 100, $outerFn );
289
290 $this->assertEquals( 2, $outerHit, "Outer callback value not yet cached" );
291 $this->assertEquals( 3, $innerHit, "Inner callback value not yet cached" );
292
293 $cache->delete( $keyInner, $cache::HOLDOFF_NONE );
294 $mockWallClock += 1;
295 $cache->getWithSetCallback( $keyInner, 100, $innerFn, [ 'pcTTL' => 5 ] );
296
297 $this->assertEquals( 3, $innerHit, "Inner callback value process cached" );
298 }
299
300 /**
301 * @dataProvider getWithSetCallback_provider
302 * @covers WANObjectCache::getWithSetCallback()
303 * @covers WANObjectCache::fetchOrRegenerate()
304 * @param array $extOpts
305 * @param bool $versioned
306 */
307 public function testGetWithSetCallback( array $extOpts, $versioned ) {
308 $cache = $this->cache;
309
310 $key = wfRandomString();
311 $value = wfRandomString();
312 $cKey1 = wfRandomString();
313 $cKey2 = wfRandomString();
314
315 $priorValue = null;
316 $priorAsOf = null;
317 $wasSet = 0;
318 $func = function ( $old, &$ttl, &$opts, $asOf )
319 use ( &$wasSet, &$priorValue, &$priorAsOf, $value ) {
320 ++$wasSet;
321 $priorValue = $old;
322 $priorAsOf = $asOf;
323 $ttl = 20; // override with another value
324 return $value;
325 };
326
327 $mockWallClock = 1549343530.2053;
328 $priorTime = $mockWallClock; // reference time
329 $cache->setMockTime( $mockWallClock );
330
331 $wasSet = 0;
332 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
333 $this->assertEquals( $value, $v, "Value returned" );
334 $this->assertEquals( 1, $wasSet, "Value regenerated" );
335 $this->assertFalse( $priorValue, "No prior value" );
336 $this->assertNull( $priorAsOf, "No prior value" );
337
338 $curTTL = null;
339 $cache->get( $key, $curTTL );
340 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
341 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
342
343 $wasSet = 0;
344 $v = $cache->getWithSetCallback(
345 $key, 30, $func, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
346 $this->assertEquals( $value, $v, "Value returned" );
347 $this->assertSame( 0, $wasSet, "Value not regenerated" );
348
349 $mockWallClock += 1;
350
351 $wasSet = 0;
352 $v = $cache->getWithSetCallback(
353 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
354 );
355 $this->assertEquals( $value, $v, "Value returned" );
356 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
357 $this->assertEquals( $value, $priorValue, "Has prior value" );
358 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
359 $t1 = $cache->getCheckKeyTime( $cKey1 );
360 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
361 $t2 = $cache->getCheckKeyTime( $cKey2 );
362 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
363
364 $mockWallClock += 0.2; // interim key is not brand new and check keys have past values
365 $priorTime = $mockWallClock; // reference time
366 $wasSet = 0;
367 $v = $cache->getWithSetCallback(
368 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
369 );
370 $this->assertEquals( $value, $v, "Value returned" );
371 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
372 $t1 = $cache->getCheckKeyTime( $cKey1 );
373 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
374 $t2 = $cache->getCheckKeyTime( $cKey2 );
375 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
376
377 $curTTL = null;
378 $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
379 $this->assertEquals( $value, $v, "Value returned" );
380 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
381
382 $wasSet = 0;
383 $key = wfRandomString();
384 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
385 $this->assertEquals( $value, $v, "Value returned" );
386 $cache->delete( $key );
387 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
388 $this->assertEquals( $value, $v, "Value still returned after deleted" );
389 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
390
391 $oldValReceived = -1;
392 $oldAsOfReceived = -1;
393 $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
394 use ( &$oldValReceived, &$oldAsOfReceived, &$wasSet ) {
395 ++$wasSet;
396 $oldValReceived = $oldVal;
397 $oldAsOfReceived = $oldAsOf;
398
399 return 'xxx' . $wasSet;
400 };
401
402 $mockWallClock = 1549343530.2053;
403 $priorTime = $mockWallClock; // reference time
404
405 $wasSet = 0;
406 $key = wfRandomString();
407 $v = $cache->getWithSetCallback(
408 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
409 $this->assertEquals( 'xxx1', $v, "Value returned" );
410 $this->assertFalse( $oldValReceived, "Callback got no stale value" );
411 $this->assertNull( $oldAsOfReceived, "Callback got no stale value" );
412
413 $mockWallClock += 40;
414 $v = $cache->getWithSetCallback(
415 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
416 $this->assertEquals( 'xxx2', $v, "Value still returned after expired" );
417 $this->assertEquals( 2, $wasSet, "Value recalculated while expired" );
418 $this->assertEquals( 'xxx1', $oldValReceived, "Callback got stale value" );
419 $this->assertNotEquals( null, $oldAsOfReceived, "Callback got stale value" );
420
421 $mockWallClock += 260;
422 $v = $cache->getWithSetCallback(
423 $key, 30, $checkFunc, [ 'staleTTL' => 50 ] + $extOpts );
424 $this->assertEquals( 'xxx3', $v, "Value still returned after expired" );
425 $this->assertEquals( 3, $wasSet, "Value recalculated while expired" );
426 $this->assertFalse( $oldValReceived, "Callback got no stale value" );
427 $this->assertNull( $oldAsOfReceived, "Callback got no stale value" );
428
429 $mockWallClock = ( $priorTime - $cache::HOLDOFF_TTL - 1 );
430 $wasSet = 0;
431 $key = wfRandomString();
432 $checkKey = $cache->makeKey( 'template', 'X' );
433 $cache->touchCheckKey( $checkKey ); // init check key
434 $mockWallClock = $priorTime;
435 $v = $cache->getWithSetCallback(
436 $key,
437 $cache::TTL_INDEFINITE,
438 $checkFunc,
439 [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
440 );
441 $this->assertEquals( 'xxx1', $v, "Value returned" );
442 $this->assertEquals( 1, $wasSet, "Value computed" );
443 $this->assertFalse( $oldValReceived, "Callback got no stale value" );
444 $this->assertNull( $oldAsOfReceived, "Callback got no stale value" );
445
446 $mockWallClock += $cache::TTL_HOUR; // some time passes
447 $v = $cache->getWithSetCallback(
448 $key,
449 $cache::TTL_INDEFINITE,
450 $checkFunc,
451 [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
452 );
453 $this->assertEquals( 'xxx1', $v, "Cached value returned" );
454 $this->assertEquals( 1, $wasSet, "Cached value returned" );
455
456 $cache->touchCheckKey( $checkKey ); // make key stale
457 $mockWallClock += 0.01; // ~1 week left of grace (barely stale to avoid refreshes)
458
459 $v = $cache->getWithSetCallback(
460 $key,
461 $cache::TTL_INDEFINITE,
462 $checkFunc,
463 [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
464 );
465 $this->assertEquals( 'xxx1', $v, "Value still returned after expired (in grace)" );
466 $this->assertEquals( 1, $wasSet, "Value still returned after expired (in grace)" );
467
468 // Chance of refresh increase to unity as staleness approaches graceTTL
469 $mockWallClock += $cache::TTL_WEEK; // 8 days of being stale
470 $v = $cache->getWithSetCallback(
471 $key,
472 $cache::TTL_INDEFINITE,
473 $checkFunc,
474 [ 'graceTTL' => $cache::TTL_WEEK, 'checkKeys' => [ $checkKey ] ] + $extOpts
475 );
476 $this->assertEquals( 'xxx2', $v, "Value was recomputed (past grace)" );
477 $this->assertEquals( 2, $wasSet, "Value was recomputed (past grace)" );
478 $this->assertEquals( 'xxx1', $oldValReceived, "Callback got post-grace stale value" );
479 $this->assertNotEquals( null, $oldAsOfReceived, "Callback got post-grace stale value" );
480 }
481
482 /**
483 * @dataProvider getWithSetCallback_provider
484 * @covers WANObjectCache::getWithSetCallback()
485 * @covers WANObjectCache::fetchOrRegenerate()
486 * @param array $extOpts
487 * @param bool $versioned
488 */
489 function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
490 $cache = $this->cache;
491
492 $mockWallClock = 1549343530.2053;
493 $cache->setMockTime( $mockWallClock );
494
495 $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
496 use ( &$wasSet ) {
497 ++$wasSet;
498
499 return 'xxx' . $wasSet;
500 };
501
502 $key = wfRandomString();
503 $wasSet = 0;
504 $touched = null;
505 $touchedCallback = function () use ( &$touched ) {
506 return $touched;
507 };
508 $v = $cache->getWithSetCallback(
509 $key,
510 $cache::TTL_INDEFINITE,
511 $checkFunc,
512 [ 'touchedCallback' => $touchedCallback ] + $extOpts
513 );
514 $mockWallClock += 60;
515 $v = $cache->getWithSetCallback(
516 $key,
517 $cache::TTL_INDEFINITE,
518 $checkFunc,
519 [ 'touchedCallback' => $touchedCallback ] + $extOpts
520 );
521 $this->assertEquals( 'xxx1', $v, "Value was computed once" );
522 $this->assertEquals( 1, $wasSet, "Value was computed once" );
523
524 $touched = $mockWallClock - 10;
525 $v = $cache->getWithSetCallback(
526 $key,
527 $cache::TTL_INDEFINITE,
528 $checkFunc,
529 [ 'touchedCallback' => $touchedCallback ] + $extOpts
530 );
531 $v = $cache->getWithSetCallback(
532 $key,
533 $cache::TTL_INDEFINITE,
534 $checkFunc,
535 [ 'touchedCallback' => $touchedCallback ] + $extOpts
536 );
537 $this->assertEquals( 'xxx2', $v, "Value was recomputed once" );
538 $this->assertEquals( 2, $wasSet, "Value was recomputed once" );
539 }
540
541 public static function getWithSetCallback_provider() {
542 return [
543 [ [], false ],
544 [ [ 'version' => 1 ], true ]
545 ];
546 }
547
548 public function testPreemtiveRefresh() {
549 $value = 'KatCafe';
550 $wasSet = 0;
551 $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, &$value )
552 {
553 ++$wasSet;
554 return $value;
555 };
556
557 $cache = new NearExpiringWANObjectCache( [ 'cache' => new HashBagOStuff() ] );
558 $mockWallClock = 1549343530.2053;
559 $cache->setMockTime( $mockWallClock );
560
561 $wasSet = 0;
562 $key = wfRandomString();
563 $opts = [ 'lowTTL' => 30 ];
564 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
565 $this->assertEquals( $value, $v, "Value returned" );
566 $this->assertEquals( 1, $wasSet, "Value calculated" );
567
568 $mockWallClock += 0.2; // interim key is not brand new
569 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
570 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
571
572 $wasSet = 0;
573 $key = wfRandomString();
574 $opts = [ 'lowTTL' => 1 ];
575 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
576 $this->assertEquals( $value, $v, "Value returned" );
577 $this->assertEquals( 1, $wasSet, "Value calculated" );
578 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
579 $this->assertEquals( 1, $wasSet, "Value cached" );
580
581 $asycList = [];
582 $asyncHandler = function ( $callback ) use ( &$asycList ) {
583 $asycList[] = $callback;
584 };
585 $cache = new NearExpiringWANObjectCache( [
586 'cache' => new HashBagOStuff(),
587 'asyncHandler' => $asyncHandler
588 ] );
589
590 $mockWallClock = 1549343530.2053;
591 $priorTime = $mockWallClock; // reference time
592 $cache->setMockTime( $mockWallClock );
593
594 $wasSet = 0;
595 $key = wfRandomString();
596 $opts = [ 'lowTTL' => 100 ];
597 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
598 $this->assertEquals( $value, $v, "Value returned" );
599 $this->assertEquals( 1, $wasSet, "Value calculated" );
600 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
601 $this->assertEquals( 1, $wasSet, "Cached value used" );
602 $this->assertEquals( $v, $value, "Value cached" );
603
604 $mockWallClock += 250;
605 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
606 $this->assertEquals( $value, $v, "Value returned" );
607 $this->assertEquals( 1, $wasSet, "Stale value used" );
608 $this->assertEquals( 1, count( $asycList ), "Refresh deferred." );
609 $value = 'NewCatsInTown'; // change callback return value
610 $asycList[0](); // run the refresh callback
611 $asycList = [];
612 $this->assertEquals( 2, $wasSet, "Value calculated at later time" );
613 $this->assertSame( 0, count( $asycList ), "No deferred refreshes added." );
614 $v = $cache->getWithSetCallback( $key, 300, $func, $opts );
615 $this->assertEquals( $value, $v, "New value stored" );
616
617 $cache = new PopularityRefreshingWANObjectCache( [
618 'cache' => new HashBagOStuff()
619 ] );
620
621 $mockWallClock = $priorTime;
622 $cache->setMockTime( $mockWallClock );
623
624 $wasSet = 0;
625 $key = wfRandomString();
626 $opts = [ 'hotTTR' => 900 ];
627 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
628 $this->assertEquals( $value, $v, "Value returned" );
629 $this->assertEquals( 1, $wasSet, "Value calculated" );
630
631 $mockWallClock += 30;
632
633 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
634 $this->assertEquals( 1, $wasSet, "Value cached" );
635
636 $mockWallClock = $priorTime;
637 $wasSet = 0;
638 $key = wfRandomString();
639 $opts = [ 'hotTTR' => 10 ];
640 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
641 $this->assertEquals( $value, $v, "Value returned" );
642 $this->assertEquals( 1, $wasSet, "Value calculated" );
643
644 $mockWallClock += 30;
645
646 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
647 $this->assertEquals( $value, $v, "Value returned" );
648 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
649 }
650
651 /**
652 * @dataProvider getMultiWithSetCallback_provider
653 * @covers WANObjectCache::getMultiWithSetCallback
654 * @covers WANObjectCache::makeMultiKeys
655 * @covers WANObjectCache::getMulti
656 * @param array $extOpts
657 * @param bool $versioned
658 */
659 public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
660 $cache = $this->cache;
661
662 $keyA = wfRandomString();
663 $keyB = wfRandomString();
664 $keyC = wfRandomString();
665 $cKey1 = wfRandomString();
666 $cKey2 = wfRandomString();
667
668 $priorValue = null;
669 $priorAsOf = null;
670 $wasSet = 0;
671 $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
672 &$wasSet, &$priorValue, &$priorAsOf
673 ) {
674 ++$wasSet;
675 $priorValue = $old;
676 $priorAsOf = $asOf;
677 $ttl = 20; // override with another value
678 return "@$id$";
679 };
680
681 $mockWallClock = 1549343530.2053;
682 $priorTime = $mockWallClock; // reference time
683 $cache->setMockTime( $mockWallClock );
684
685 $wasSet = 0;
686 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
687 $value = "@3353$";
688 $v = $cache->getMultiWithSetCallback(
689 $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
690 $this->assertEquals( $value, $v[$keyA], "Value returned" );
691 $this->assertEquals( 1, $wasSet, "Value regenerated" );
692 $this->assertFalse( $priorValue, "No prior value" );
693 $this->assertNull( $priorAsOf, "No prior value" );
694
695 $curTTL = null;
696 $cache->get( $keyA, $curTTL );
697 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
698 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
699
700 $wasSet = 0;
701 $value = "@efef$";
702 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
703 $v = $cache->getMultiWithSetCallback(
704 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
705 $this->assertEquals( $value, $v[$keyB], "Value returned" );
706 $this->assertEquals( 1, $wasSet, "Value regenerated" );
707 $this->assertSame( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
708
709 $v = $cache->getMultiWithSetCallback(
710 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5 ] + $extOpts );
711 $this->assertEquals( $value, $v[$keyB], "Value returned" );
712 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
713 $this->assertSame( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
714
715 $mockWallClock += 1;
716
717 $wasSet = 0;
718 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
719 $v = $cache->getMultiWithSetCallback(
720 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
721 );
722 $this->assertEquals( $value, $v[$keyB], "Value returned" );
723 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
724 $this->assertEquals( $value, $priorValue, "Has prior value" );
725 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
726 $t1 = $cache->getCheckKeyTime( $cKey1 );
727 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
728 $t2 = $cache->getCheckKeyTime( $cKey2 );
729 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
730
731 $mockWallClock += 0.01;
732 $priorTime = $mockWallClock;
733 $value = "@43636$";
734 $wasSet = 0;
735 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
736 $v = $cache->getMultiWithSetCallback(
737 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
738 );
739 $this->assertEquals( $value, $v[$keyC], "Value returned" );
740 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
741 $t1 = $cache->getCheckKeyTime( $cKey1 );
742 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
743 $t2 = $cache->getCheckKeyTime( $cKey2 );
744 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
745
746 $curTTL = null;
747 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
748 $this->assertEquals( $value, $v, "Value returned" );
749 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
750
751 $wasSet = 0;
752 $key = wfRandomString();
753 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
754 $v = $cache->getMultiWithSetCallback(
755 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
756 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
757 $cache->delete( $key );
758 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
759 $v = $cache->getMultiWithSetCallback(
760 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
761 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
762 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
763
764 $calls = 0;
765 $ids = [ 1, 2, 3, 4, 5, 6 ];
766 $keyFunc = function ( $id, WANObjectCache $wanCache ) {
767 return $wanCache->makeKey( 'test', $id );
768 };
769 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
770 $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
771 ++$calls;
772
773 return "val-{$id}";
774 };
775 $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
776
777 $this->assertEquals(
778 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
779 array_values( $values ),
780 "Correct values in correct order"
781 );
782 $this->assertEquals(
783 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
784 array_keys( $values ),
785 "Correct keys in correct order"
786 );
787 $this->assertEquals( count( $ids ), $calls );
788
789 $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
790 $this->assertEquals( count( $ids ), $calls, "Values cached" );
791
792 // Mock the BagOStuff to assure only one getMulti() call given process caching
793 $localBag = $this->getMockBuilder( HashBagOStuff::class )
794 ->setMethods( [ 'getMulti' ] )->getMock();
795 $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
796 'WANCache:v:' . 'k1' => 'val-id1',
797 'WANCache:v:' . 'k2' => 'val-id2'
798 ] );
799 $wanCache = new WANObjectCache( [ 'cache' => $localBag ] );
800
801 // Warm the process cache
802 $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] );
803 $this->assertEquals(
804 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
805 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
806 );
807 // Use the process cache
808 $this->assertEquals(
809 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
810 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
811 );
812 }
813
814 public static function getMultiWithSetCallback_provider() {
815 return [
816 [ [], false ],
817 [ [ 'version' => 1 ], true ]
818 ];
819 }
820
821 /**
822 * @dataProvider getMultiWithUnionSetCallback_provider
823 * @covers WANObjectCache::getMultiWithUnionSetCallback()
824 * @covers WANObjectCache::makeMultiKeys()
825 * @param array $extOpts
826 * @param bool $versioned
827 */
828 public function testGetMultiWithUnionSetCallback( array $extOpts, $versioned ) {
829 $cache = $this->cache;
830
831 $keyA = wfRandomString();
832 $keyB = wfRandomString();
833 $keyC = wfRandomString();
834 $cKey1 = wfRandomString();
835 $cKey2 = wfRandomString();
836
837 $wasSet = 0;
838 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use (
839 &$wasSet, &$priorValue, &$priorAsOf
840 ) {
841 $newValues = [];
842 foreach ( $ids as $id ) {
843 ++$wasSet;
844 $newValues[$id] = "@$id$";
845 $ttls[$id] = 20; // override with another value
846 }
847
848 return $newValues;
849 };
850
851 $mockWallClock = 1549343530.2053;
852 $priorTime = $mockWallClock; // reference time
853 $cache->setMockTime( $mockWallClock );
854
855 $wasSet = 0;
856 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
857 $value = "@3353$";
858 $v = $cache->getMultiWithUnionSetCallback(
859 $keyedIds, 30, $genFunc, $extOpts );
860 $this->assertEquals( $value, $v[$keyA], "Value returned" );
861 $this->assertEquals( 1, $wasSet, "Value regenerated" );
862
863 $curTTL = null;
864 $cache->get( $keyA, $curTTL );
865 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
866 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
867
868 $wasSet = 0;
869 $value = "@efef$";
870 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
871 $v = $cache->getMultiWithUnionSetCallback(
872 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
873 $this->assertEquals( $value, $v[$keyB], "Value returned" );
874 $this->assertEquals( 1, $wasSet, "Value regenerated" );
875 $this->assertSame( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
876
877 $v = $cache->getMultiWithUnionSetCallback(
878 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
879 $this->assertEquals( $value, $v[$keyB], "Value returned" );
880 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
881 $this->assertSame( 0, $cache->getWarmupKeyMisses(), "Keys warmed in warmup cache" );
882
883 $mockWallClock += 1;
884
885 $wasSet = 0;
886 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
887 $v = $cache->getMultiWithUnionSetCallback(
888 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
889 );
890 $this->assertEquals( $value, $v[$keyB], "Value returned" );
891 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
892 $t1 = $cache->getCheckKeyTime( $cKey1 );
893 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
894 $t2 = $cache->getCheckKeyTime( $cKey2 );
895 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
896
897 $mockWallClock += 0.01;
898 $priorTime = $mockWallClock;
899 $value = "@43636$";
900 $wasSet = 0;
901 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
902 $v = $cache->getMultiWithUnionSetCallback(
903 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
904 );
905 $this->assertEquals( $value, $v[$keyC], "Value returned" );
906 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
907 $t1 = $cache->getCheckKeyTime( $cKey1 );
908 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
909 $t2 = $cache->getCheckKeyTime( $cKey2 );
910 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
911
912 $curTTL = null;
913 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
914 $this->assertEquals( $value, $v, "Value returned" );
915 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
916
917 $wasSet = 0;
918 $key = wfRandomString();
919 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
920 $v = $cache->getMultiWithUnionSetCallback(
921 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
922 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
923 $cache->delete( $key );
924 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
925 $v = $cache->getMultiWithUnionSetCallback(
926 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
927 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
928 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
929
930 $calls = 0;
931 $ids = [ 1, 2, 3, 4, 5, 6 ];
932 $keyFunc = function ( $id, WANObjectCache $wanCache ) {
933 return $wanCache->makeKey( 'test', $id );
934 };
935 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
936 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use ( &$calls ) {
937 $newValues = [];
938 foreach ( $ids as $id ) {
939 ++$calls;
940 $newValues[$id] = "val-{$id}";
941 }
942
943 return $newValues;
944 };
945 $values = $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
946
947 $this->assertEquals(
948 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
949 array_values( $values ),
950 "Correct values in correct order"
951 );
952 $this->assertEquals(
953 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
954 array_keys( $values ),
955 "Correct keys in correct order"
956 );
957 $this->assertEquals( count( $ids ), $calls );
958
959 $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
960 $this->assertEquals( count( $ids ), $calls, "Values cached" );
961 }
962
963 public static function getMultiWithUnionSetCallback_provider() {
964 return [
965 [ [], false ],
966 [ [ 'version' => 1 ], true ]
967 ];
968 }
969
970 /**
971 * @covers WANObjectCache::getWithSetCallback()
972 * @covers WANObjectCache::fetchOrRegenerate()
973 */
974 public function testLockTSE() {
975 $cache = $this->cache;
976 $key = wfRandomString();
977 $value = wfRandomString();
978
979 $mockWallClock = 1549343530.2053;
980 $cache->setMockTime( $mockWallClock );
981
982 $calls = 0;
983 $func = function () use ( &$calls, $value, $cache, $key ) {
984 ++$calls;
985 return $value;
986 };
987
988 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
989 $this->assertEquals( $value, $ret );
990 $this->assertEquals( 1, $calls, 'Value was populated' );
991
992 // Acquire the mutex to verify that getWithSetCallback uses lockTSE properly
993 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
994
995 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
996 $ret = $cache->getWithSetCallback( $key, 30, $func,
997 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
998 $this->assertEquals( $value, $ret, 'Old value used' );
999 $this->assertEquals( 1, $calls, 'Callback was not used' );
1000
1001 $cache->delete( $key );
1002 $mockWallClock += 0.001; // cached values will be newer than tombstone
1003 $ret = $cache->getWithSetCallback( $key, 30, $func,
1004 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
1005 $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
1006 $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
1007
1008 $ret = $cache->getWithSetCallback( $key, 30, $func,
1009 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
1010 $this->assertEquals( $value, $ret, 'Callback was not used; used interim (mutex failed)' );
1011 $this->assertEquals( 2, $calls, 'Callback was not used; used interim (mutex failed)' );
1012 }
1013
1014 /**
1015 * @covers WANObjectCache::getWithSetCallback()
1016 * @covers WANObjectCache::fetchOrRegenerate()
1017 * @covers WANObjectCache::set()
1018 */
1019 public function testLockTSESlow() {
1020 $cache = $this->cache;
1021 $key = wfRandomString();
1022 $key2 = wfRandomString();
1023 $value = wfRandomString();
1024
1025 $mockWallClock = 1549343530.2053;
1026 $cache->setMockTime( $mockWallClock );
1027
1028 $calls = 0;
1029 $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, &$mockWallClock ) {
1030 ++$calls;
1031 $setOpts['since'] = $mockWallClock - 10;
1032 return $value;
1033 };
1034
1035 // Value should be given a low logical TTL due to snapshot lag
1036 $curTTL = null;
1037 $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
1038 $this->assertEquals( $value, $ret );
1039 $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
1040 $this->assertEquals( 1, $curTTL, 'Value has reduced logical TTL', 0.01 );
1041 $this->assertEquals( 1, $calls, 'Value was generated' );
1042
1043 $mockWallClock += 2; // low logical TTL expired
1044
1045 $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
1046 $this->assertEquals( $value, $ret );
1047 $this->assertEquals( 2, $calls, 'Callback used (mutex acquired)' );
1048
1049 $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
1050 $this->assertEquals( $value, $ret );
1051 $this->assertEquals( 2, $calls, 'Callback was not used (interim value used)' );
1052
1053 $mockWallClock += 2; // low logical TTL expired
1054 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
1055 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1056
1057 $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
1058 $this->assertEquals( $value, $ret );
1059 $this->assertEquals( 2, $calls, 'Callback was not used (mutex not acquired)' );
1060
1061 $mockWallClock += 301; // physical TTL expired
1062 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
1063 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1064
1065 $ret = $cache->getWithSetCallback( $key, 300, $func, [ 'lockTSE' => 5 ] );
1066 $this->assertEquals( $value, $ret );
1067 $this->assertEquals( 3, $calls, 'Callback was used (mutex not acquired, not in cache)' );
1068
1069 $calls = 0;
1070 $func2 = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) {
1071 ++$calls;
1072 $setOpts['lag'] = 15;
1073 return $value;
1074 };
1075
1076 // Value should be given a low logical TTL due to replication lag
1077 $curTTL = null;
1078 $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
1079 $this->assertEquals( $value, $ret );
1080 $this->assertEquals( $value, $cache->get( $key2, $curTTL ), 'Value was populated' );
1081 $this->assertEquals( 30, $curTTL, 'Value has reduced logical TTL', 0.01 );
1082 $this->assertEquals( 1, $calls, 'Value was generated' );
1083
1084 $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
1085 $this->assertEquals( $value, $ret );
1086 $this->assertEquals( 1, $calls, 'Callback was used (not expired)' );
1087
1088 $mockWallClock += 31;
1089
1090 $ret = $cache->getWithSetCallback( $key2, 300, $func2, [ 'lockTSE' => 5 ] );
1091 $this->assertEquals( $value, $ret );
1092 $this->assertEquals( 2, $calls, 'Callback was used (mutex acquired)' );
1093 }
1094
1095 /**
1096 * @covers WANObjectCache::getWithSetCallback()
1097 * @covers WANObjectCache::fetchOrRegenerate()
1098 */
1099 public function testBusyValueBasic() {
1100 $cache = $this->cache;
1101 $key = wfRandomString();
1102 $value = wfRandomString();
1103 $busyValue = wfRandomString();
1104
1105 $mockWallClock = 1549343530.2053;
1106 $cache->setMockTime( $mockWallClock );
1107
1108 $calls = 0;
1109 $func = function () use ( &$calls, $value ) {
1110 ++$calls;
1111 return $value;
1112 };
1113
1114 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
1115 $this->assertEquals( $value, $ret );
1116 $this->assertEquals( 1, $calls, 'Value was populated' );
1117
1118 $mockWallClock += 0.2; // interim keys not brand new
1119
1120 // Acquire a lock to verify that getWithSetCallback uses busyValue properly
1121 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1122
1123 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
1124 $ret = $cache->getWithSetCallback( $key, 30, $func,
1125 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
1126 $this->assertEquals( $value, $ret, 'Callback used' );
1127 $this->assertEquals( 2, $calls, 'Callback used' );
1128
1129 $ret = $cache->getWithSetCallback( $key, 30, $func,
1130 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
1131 $this->assertEquals( $value, $ret, 'Old value used' );
1132 $this->assertEquals( 2, $calls, 'Callback was not used' );
1133
1134 $cache->delete( $key ); // no value at all anymore and still locked
1135 $ret = $cache->getWithSetCallback( $key, 30, $func,
1136 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
1137 $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
1138 $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
1139
1140 $this->internalCache->delete( 'WANCache:m:' . $key );
1141 $mockWallClock += 0.001; // cached values will be newer than tombstone
1142 $ret = $cache->getWithSetCallback( $key, 30, $func,
1143 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
1144 $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
1145 $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
1146
1147 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1148 $ret = $cache->getWithSetCallback( $key, 30, $func,
1149 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
1150 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
1151 $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
1152 }
1153
1154 public function getBusyValues_Provider() {
1155 $hash = new HashBagOStuff( [] );
1156
1157 return [
1158 [
1159 function () {
1160 return "Saint Oliver Plunckett";
1161 },
1162 'Saint Oliver Plunckett'
1163 ],
1164 [ 'strlen', 'strlen' ],
1165 [ 'WANObjectCache::newEmpty', 'WANObjectCache::newEmpty' ],
1166 [ [ 'WANObjectCache', 'newEmpty' ], [ 'WANObjectCache', 'newEmpty' ] ],
1167 [ [ $hash, 'getLastError' ], [ $hash, 'getLastError' ] ],
1168 [ [ 1, 2, 3 ], [ 1, 2, 3 ] ]
1169 ];
1170 }
1171
1172 /**
1173 * @covers WANObjectCache::getWithSetCallback()
1174 * @covers WANObjectCache::fetchOrRegenerate()
1175 * @dataProvider getBusyValues_Provider
1176 * @param mixed $busyValue
1177 * @param mixed $expected
1178 */
1179 public function testBusyValueTypes( $busyValue, $expected ) {
1180 $cache = $this->cache;
1181 $key = wfRandomString();
1182
1183 $mockWallClock = 1549343530.2053;
1184 $cache->setMockTime( $mockWallClock );
1185
1186 $calls = 0;
1187 $func = function () use ( &$calls ) {
1188 ++$calls;
1189 return 418;
1190 };
1191
1192 // Acquire a lock to verify that getWithSetCallback uses busyValue properly
1193 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1194
1195 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
1196 $this->assertSame( $expected, $ret, 'busyValue used as expected' );
1197 $this->assertSame( 0, $calls, 'busyValue was used' );
1198 }
1199
1200 /**
1201 * @covers WANObjectCache::getMulti()
1202 */
1203 public function testGetMulti() {
1204 $cache = $this->cache;
1205
1206 $value1 = [ 'this' => 'is', 'a' => 'test' ];
1207 $value2 = [ 'this' => 'is', 'another' => 'test' ];
1208
1209 $key1 = wfRandomString();
1210 $key2 = wfRandomString();
1211 $key3 = wfRandomString();
1212
1213 $mockWallClock = 1549343530.2053;
1214 $priorTime = $mockWallClock; // reference time
1215 $cache->setMockTime( $mockWallClock );
1216
1217 $cache->set( $key1, $value1, 5 );
1218 $cache->set( $key2, $value2, 10 );
1219
1220 $curTTLs = [];
1221 $this->assertSame(
1222 [ $key1 => $value1, $key2 => $value2 ],
1223 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
1224 'Result array populated'
1225 );
1226
1227 $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
1228 $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
1229 $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
1230
1231 $cKey1 = wfRandomString();
1232 $cKey2 = wfRandomString();
1233
1234 $mockWallClock += 1;
1235
1236 $curTTLs = [];
1237 $this->assertSame(
1238 [ $key1 => $value1, $key2 => $value2 ],
1239 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
1240 "Result array populated even with new check keys"
1241 );
1242 $t1 = $cache->getCheckKeyTime( $cKey1 );
1243 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
1244 $t2 = $cache->getCheckKeyTime( $cKey2 );
1245 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
1246 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
1247 $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
1248 $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
1249
1250 $mockWallClock += 1;
1251
1252 $curTTLs = [];
1253 $this->assertEquals(
1254 [ $key1 => $value1, $key2 => $value2 ],
1255 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
1256 "Result array still populated even with new check keys"
1257 );
1258 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
1259 $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
1260 $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
1261 }
1262
1263 /**
1264 * @covers WANObjectCache::getMulti()
1265 * @covers WANObjectCache::processCheckKeys()
1266 */
1267 public function testGetMultiCheckKeys() {
1268 $cache = $this->cache;
1269
1270 $checkAll = wfRandomString();
1271 $check1 = wfRandomString();
1272 $check2 = wfRandomString();
1273 $check3 = wfRandomString();
1274 $value1 = wfRandomString();
1275 $value2 = wfRandomString();
1276
1277 $mockWallClock = 1549343530.2053;
1278 $cache->setMockTime( $mockWallClock );
1279
1280 // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
1281 // several seconds during the test to assert the behaviour.
1282 foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
1283 $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_TTL_NONE );
1284 }
1285
1286 $mockWallClock += 0.100;
1287
1288 $cache->set( 'key1', $value1, 10 );
1289 $cache->set( 'key2', $value2, 10 );
1290
1291 $curTTLs = [];
1292 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1293 'key1' => $check1,
1294 $checkAll,
1295 'key2' => $check2,
1296 'key3' => $check3,
1297 ] );
1298 $this->assertSame(
1299 [ 'key1' => $value1, 'key2' => $value2 ],
1300 $result,
1301 'Initial values'
1302 );
1303 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
1304 $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
1305 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
1306 $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
1307
1308 $mockWallClock += 0.100;
1309 $cache->touchCheckKey( $check1 );
1310
1311 $curTTLs = [];
1312 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1313 'key1' => $check1,
1314 $checkAll,
1315 'key2' => $check2,
1316 'key3' => $check3,
1317 ] );
1318 $this->assertSame(
1319 [ 'key1' => $value1, 'key2' => $value2 ],
1320 $result,
1321 'key1 expired by check1, but value still provided'
1322 );
1323 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
1324 $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
1325
1326 $cache->touchCheckKey( $checkAll );
1327
1328 $curTTLs = [];
1329 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
1330 'key1' => $check1,
1331 $checkAll,
1332 'key2' => $check2,
1333 'key3' => $check3,
1334 ] );
1335 $this->assertEquals(
1336 [ 'key1' => $value1, 'key2' => $value2 ],
1337 $result,
1338 'All keys expired by checkAll, but value still provided'
1339 );
1340 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
1341 $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
1342 }
1343
1344 /**
1345 * @covers WANObjectCache::get()
1346 * @covers WANObjectCache::processCheckKeys()
1347 */
1348 public function testCheckKeyInitHoldoff() {
1349 $cache = $this->cache;
1350
1351 for ( $i = 0; $i < 500; ++$i ) {
1352 $key = wfRandomString();
1353 $checkKey = wfRandomString();
1354 // miss, set, hit
1355 $cache->get( $key, $curTTL, [ $checkKey ] );
1356 $cache->set( $key, 'val', 10 );
1357 $curTTL = null;
1358 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
1359
1360 $this->assertEquals( 'val', $v );
1361 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
1362 }
1363
1364 for ( $i = 0; $i < 500; ++$i ) {
1365 $key = wfRandomString();
1366 $checkKey = wfRandomString();
1367 // set, hit
1368 $cache->set( $key, 'val', 10 );
1369 $curTTL = null;
1370 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
1371
1372 $this->assertEquals( 'val', $v );
1373 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
1374 }
1375 }
1376
1377 /**
1378 * @covers WANObjectCache::get()
1379 * @covers WANObjectCache::processCheckKeys()
1380 */
1381 public function testCheckKeyHoldoff() {
1382 $cache = $this->cache;
1383 $key = wfRandomString();
1384 $checkKey = wfRandomString();
1385
1386 $mockWallClock = 1549343530.2053;
1387 $cache->setMockTime( $mockWallClock );
1388 $cache->touchCheckKey( $checkKey, 8 );
1389
1390 $mockWallClock += 1;
1391 $cache->set( $key, 1, 60 );
1392 $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
1393 $this->assertLessThan( 0, $curTTL, "Key in hold-off due to check key" );
1394
1395 $mockWallClock += 3;
1396 $cache->set( $key, 1, 60 );
1397 $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
1398 $this->assertLessThan( 0, $curTTL, "Key in hold-off due to check key" );
1399
1400 $mockWallClock += 10;
1401 $cache->set( $key, 1, 60 );
1402 $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
1403 $this->assertGreaterThan( 0, $curTTL, "Key not in hold-off due to check key" );
1404 }
1405
1406 /**
1407 * @covers WANObjectCache::delete
1408 * @covers WANObjectCache::relayDelete
1409 * @covers WANObjectCache::relayPurge
1410 */
1411 public function testDelete() {
1412 $key = wfRandomString();
1413 $value = wfRandomString();
1414 $this->cache->set( $key, $value );
1415
1416 $curTTL = null;
1417 $v = $this->cache->get( $key, $curTTL );
1418 $this->assertEquals( $value, $v, "Key was created with value" );
1419 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
1420
1421 $this->cache->delete( $key );
1422
1423 $curTTL = null;
1424 $v = $this->cache->get( $key, $curTTL );
1425 $this->assertFalse( $v, "Deleted key has false value" );
1426 $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
1427
1428 $this->cache->set( $key, $value . 'more' );
1429 $v = $this->cache->get( $key, $curTTL );
1430 $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
1431 $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
1432
1433 $this->cache->set( $key, $value );
1434 $this->cache->delete( $key, WANObjectCache::HOLDOFF_TTL_NONE );
1435
1436 $curTTL = null;
1437 $v = $this->cache->get( $key, $curTTL );
1438 $this->assertFalse( $v, "Deleted key has false value" );
1439 $this->assertNull( $curTTL, "Deleted key has null current TTL" );
1440
1441 $this->cache->set( $key, $value );
1442 $v = $this->cache->get( $key, $curTTL );
1443 $this->assertEquals( $value, $v, "Key was created with value" );
1444 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
1445 }
1446
1447 /**
1448 * @dataProvider getWithSetCallback_versions_provider
1449 * @covers WANObjectCache::getWithSetCallback()
1450 * @covers WANObjectCache::fetchOrRegenerate()
1451 * @param array $extOpts
1452 * @param bool $versioned
1453 */
1454 public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
1455 $cache = $this->cache;
1456
1457 $key = wfRandomString();
1458 $valueV1 = wfRandomString();
1459 $valueV2 = [ wfRandomString() ];
1460
1461 $wasSet = 0;
1462 $funcV1 = function () use ( &$wasSet, $valueV1 ) {
1463 ++$wasSet;
1464
1465 return $valueV1;
1466 };
1467
1468 $priorValue = false;
1469 $priorAsOf = null;
1470 $funcV2 = function ( $oldValue, &$ttl, $setOpts, $oldAsOf )
1471 use ( &$wasSet, $valueV2, &$priorValue, &$priorAsOf ) {
1472 $priorValue = $oldValue;
1473 $priorAsOf = $oldAsOf;
1474 ++$wasSet;
1475
1476 return $valueV2; // new array format
1477 };
1478
1479 // Set the main key (version N if versioned)
1480 $wasSet = 0;
1481 $v = $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
1482 $this->assertEquals( $valueV1, $v, "Value returned" );
1483 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1484 $cache->getWithSetCallback( $key, 30, $funcV1, $extOpts );
1485 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
1486 $this->assertEquals( $valueV1, $v, "Value not regenerated" );
1487
1488 if ( $versioned ) {
1489 // Set the key for version N+1 format
1490 $verOpts = [ 'version' => $extOpts['version'] + 1 ];
1491 } else {
1492 // Start versioning now with the unversioned key still there
1493 $verOpts = [ 'version' => 1 ];
1494 }
1495
1496 // Value goes to secondary key since V1 already used $key
1497 $wasSet = 0;
1498 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
1499 $this->assertEquals( $valueV2, $v, "Value returned" );
1500 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1501 $this->assertEquals( false, $priorValue, "Old value not given due to old format" );
1502 $this->assertNull( $priorAsOf, "Old value not given due to old format" );
1503
1504 $wasSet = 0;
1505 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
1506 $this->assertEquals( $valueV2, $v, "Value not regenerated (secondary key)" );
1507 $this->assertSame( 0, $wasSet, "Value not regenerated (secondary key)" );
1508
1509 // Clear out the older or unversioned key
1510 $cache->delete( $key, 0 );
1511
1512 // Set the key for next/first versioned format
1513 $wasSet = 0;
1514 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
1515 $this->assertEquals( $valueV2, $v, "Value returned" );
1516 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1517
1518 $v = $cache->getWithSetCallback( $key, 30, $funcV2, $verOpts + $extOpts );
1519 $this->assertEquals( $valueV2, $v, "Value not regenerated (main key)" );
1520 $this->assertEquals( 1, $wasSet, "Value not regenerated (main key)" );
1521 }
1522
1523 public static function getWithSetCallback_versions_provider() {
1524 return [
1525 [ [], false ],
1526 [ [ 'version' => 1 ], true ]
1527 ];
1528 }
1529
1530 /**
1531 * @covers WANObjectCache::useInterimHoldOffCaching
1532 * @covers WANObjectCache::getInterimValue
1533 */
1534 public function testInterimHoldOffCaching() {
1535 $cache = $this->cache;
1536
1537 $mockWallClock = 1549343530.2053;
1538 $cache->setMockTime( $mockWallClock );
1539
1540 $value = 'CRL-40-940';
1541 $wasCalled = 0;
1542 $func = function () use ( &$wasCalled, $value ) {
1543 $wasCalled++;
1544
1545 return $value;
1546 };
1547
1548 $cache->useInterimHoldOffCaching( true );
1549
1550 $key = wfRandomString( 32 );
1551 $v = $cache->getWithSetCallback( $key, 60, $func );
1552 $v = $cache->getWithSetCallback( $key, 60, $func );
1553 $this->assertEquals( 1, $wasCalled, 'Value cached' );
1554
1555 $cache->delete( $key );
1556 $mockWallClock += 0.001; // cached values will be newer than tombstone
1557 $v = $cache->getWithSetCallback( $key, 60, $func );
1558 $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
1559 $v = $cache->getWithSetCallback( $key, 60, $func );
1560 $this->assertEquals( 2, $wasCalled, 'Value interim cached' ); // reuses interim
1561
1562 $mockWallClock += 0.2; // interim key not brand new
1563 $v = $cache->getWithSetCallback( $key, 60, $func );
1564 $this->assertEquals( 3, $wasCalled, 'Value regenerated (got mutex)' ); // sets interim
1565 // Lock up the mutex so interim cache is used
1566 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1567 $v = $cache->getWithSetCallback( $key, 60, $func );
1568 $this->assertEquals( 3, $wasCalled, 'Value interim cached (failed mutex)' );
1569 $this->internalCache->delete( 'WANCache:m:' . $key );
1570
1571 $cache->useInterimHoldOffCaching( false );
1572
1573 $wasCalled = 0;
1574 $key = wfRandomString( 32 );
1575 $v = $cache->getWithSetCallback( $key, 60, $func );
1576 $v = $cache->getWithSetCallback( $key, 60, $func );
1577 $this->assertEquals( 1, $wasCalled, 'Value cached' );
1578 $cache->delete( $key );
1579 $v = $cache->getWithSetCallback( $key, 60, $func );
1580 $this->assertEquals( 2, $wasCalled, 'Value regenerated (got mutex)' );
1581 $v = $cache->getWithSetCallback( $key, 60, $func );
1582 $this->assertEquals( 3, $wasCalled, 'Value still regenerated (got mutex)' );
1583 $v = $cache->getWithSetCallback( $key, 60, $func );
1584 $this->assertEquals( 4, $wasCalled, 'Value still regenerated (got mutex)' );
1585 // Lock up the mutex so interim cache is used
1586 $this->internalCache->add( 'WANCache:m:' . $key, 1, 0 );
1587 $v = $cache->getWithSetCallback( $key, 60, $func );
1588 $this->assertEquals( 5, $wasCalled, 'Value still regenerated (failed mutex)' );
1589 }
1590
1591 /**
1592 * @covers WANObjectCache::touchCheckKey
1593 * @covers WANObjectCache::resetCheckKey
1594 * @covers WANObjectCache::getCheckKeyTime
1595 * @covers WANObjectCache::getMultiCheckKeyTime
1596 * @covers WANObjectCache::makePurgeValue
1597 * @covers WANObjectCache::parsePurgeValue
1598 */
1599 public function testTouchKeys() {
1600 $cache = $this->cache;
1601 $key = wfRandomString();
1602
1603 $mockWallClock = 1549343530.2053;
1604 $priorTime = $mockWallClock; // reference time
1605 $cache->setMockTime( $mockWallClock );
1606
1607 $mockWallClock += 0.100;
1608 $t0 = $cache->getCheckKeyTime( $key );
1609 $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
1610
1611 $priorTime = $mockWallClock;
1612 $mockWallClock += 0.100;
1613 $cache->touchCheckKey( $key );
1614 $t1 = $cache->getCheckKeyTime( $key );
1615 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
1616
1617 $t2 = $cache->getCheckKeyTime( $key );
1618 $this->assertEquals( $t1, $t2, 'Check key time did not change' );
1619
1620 $mockWallClock += 0.100;
1621 $cache->touchCheckKey( $key );
1622 $t3 = $cache->getCheckKeyTime( $key );
1623 $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
1624
1625 $t4 = $cache->getCheckKeyTime( $key );
1626 $this->assertEquals( $t3, $t4, 'Check key time did not change' );
1627
1628 $mockWallClock += 0.100;
1629 $cache->resetCheckKey( $key );
1630 $t5 = $cache->getCheckKeyTime( $key );
1631 $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
1632
1633 $t6 = $cache->getCheckKeyTime( $key );
1634 $this->assertEquals( $t5, $t6, 'Check key time did not change' );
1635 }
1636
1637 /**
1638 * @covers WANObjectCache::getMulti()
1639 */
1640 public function testGetWithSeveralCheckKeys() {
1641 $key = wfRandomString();
1642 $tKey1 = wfRandomString();
1643 $tKey2 = wfRandomString();
1644 $value = 'meow';
1645
1646 $mockWallClock = 1549343530.2053;
1647 $priorTime = $mockWallClock; // reference time
1648 $this->cache->setMockTime( $mockWallClock );
1649
1650 // Two check keys are newer (given hold-off) than $key, another is older
1651 $this->internalCache->set(
1652 'WANCache:t:' . $tKey2,
1653 'PURGED:' . ( $priorTime - 3 )
1654 );
1655 $this->internalCache->set(
1656 'WANCache:t:' . $tKey2,
1657 'PURGED:' . ( $priorTime - 5 )
1658 );
1659 $this->internalCache->set(
1660 'WANCache:t:' . $tKey1,
1661 'PURGED:' . ( $priorTime - 30 )
1662 );
1663 $this->cache->set( $key, $value, 30 );
1664
1665 $curTTL = null;
1666 $v = $this->cache->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
1667 $this->assertEquals( $value, $v, "Value matches" );
1668 $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
1669 $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
1670 }
1671
1672 /**
1673 * @covers WANObjectCache::reap()
1674 * @covers WANObjectCache::reapCheckKey()
1675 */
1676 public function testReap() {
1677 $vKey1 = wfRandomString();
1678 $vKey2 = wfRandomString();
1679 $tKey1 = wfRandomString();
1680 $tKey2 = wfRandomString();
1681 $value = 'moo';
1682
1683 $knownPurge = time() - 60;
1684 $goodTime = microtime( true ) - 5;
1685 $badTime = microtime( true ) - 300;
1686
1687 $this->internalCache->set(
1688 'WANCache:v:' . $vKey1,
1689 [
1690 0 => 1,
1691 1 => $value,
1692 2 => 3600,
1693 3 => $goodTime
1694 ]
1695 );
1696 $this->internalCache->set(
1697 'WANCache:v:' . $vKey2,
1698 [
1699 0 => 1,
1700 1 => $value,
1701 2 => 3600,
1702 3 => $badTime
1703 ]
1704 );
1705 $this->internalCache->set(
1706 'WANCache:t:' . $tKey1,
1707 'PURGED:' . $goodTime
1708 );
1709 $this->internalCache->set(
1710 'WANCache:t:' . $tKey2,
1711 'PURGED:' . $badTime
1712 );
1713
1714 $this->assertEquals( $value, $this->cache->get( $vKey1 ) );
1715 $this->assertEquals( $value, $this->cache->get( $vKey2 ) );
1716 $this->cache->reap( $vKey1, $knownPurge, $bad1 );
1717 $this->cache->reap( $vKey2, $knownPurge, $bad2 );
1718
1719 $this->assertFalse( $bad1 );
1720 $this->assertTrue( $bad2 );
1721
1722 $this->cache->reapCheckKey( $tKey1, $knownPurge, $tBad1 );
1723 $this->cache->reapCheckKey( $tKey2, $knownPurge, $tBad2 );
1724 $this->assertFalse( $tBad1 );
1725 $this->assertTrue( $tBad2 );
1726 }
1727
1728 /**
1729 * @covers WANObjectCache::reap()
1730 */
1731 public function testReap_fail() {
1732 $backend = $this->getMockBuilder( EmptyBagOStuff::class )
1733 ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
1734 $backend->expects( $this->once() )->method( 'get' )
1735 ->willReturn( [
1736 0 => 1,
1737 1 => 'value',
1738 2 => 3600,
1739 3 => 300,
1740 ] );
1741 $backend->expects( $this->once() )->method( 'changeTTL' )
1742 ->willReturn( false );
1743
1744 $wanCache = new WANObjectCache( [
1745 'cache' => $backend
1746 ] );
1747
1748 $isStale = null;
1749 $ret = $wanCache->reap( 'key', 360, $isStale );
1750 $this->assertTrue( $isStale, 'value was stale' );
1751 $this->assertFalse( $ret, 'changeTTL failed' );
1752 }
1753
1754 /**
1755 * @covers WANObjectCache::set()
1756 */
1757 public function testSetWithLag() {
1758 $value = 1;
1759
1760 $key = wfRandomString();
1761 $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
1762 $this->cache->set( $key, $value, 30, $opts );
1763 $this->assertEquals( $value, $this->cache->get( $key ), "Rep-lagged value written." );
1764
1765 $key = wfRandomString();
1766 $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
1767 $this->cache->set( $key, $value, 30, $opts );
1768 $this->assertEquals( false, $this->cache->get( $key ), "Trx-lagged value not written." );
1769
1770 $key = wfRandomString();
1771 $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
1772 $this->cache->set( $key, $value, 30, $opts );
1773 $this->assertEquals( false, $this->cache->get( $key ), "Lagged value not written." );
1774 }
1775
1776 /**
1777 * @covers WANObjectCache::set()
1778 */
1779 public function testWritePending() {
1780 $value = 1;
1781
1782 $key = wfRandomString();
1783 $opts = [ 'pending' => true ];
1784 $this->cache->set( $key, $value, 30, $opts );
1785 $this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
1786 }
1787
1788 public function testMcRouterSupport() {
1789 $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
1790 ->setMethods( [ 'set', 'delete' ] )->getMock();
1791 $localBag->expects( $this->never() )->method( 'set' );
1792 $localBag->expects( $this->never() )->method( 'delete' );
1793 $wanCache = new WANObjectCache( [
1794 'cache' => $localBag,
1795 'mcrouterAware' => true,
1796 'region' => 'pmtpa',
1797 'cluster' => 'mw-wan'
1798 ] );
1799 $valFunc = function () {
1800 return 1;
1801 };
1802
1803 // None of these should use broadcasting commands (e.g. SET, DELETE)
1804 $wanCache->get( 'x' );
1805 $wanCache->get( 'x', $ctl, [ 'check1' ] );
1806 $wanCache->getMulti( [ 'x', 'y' ] );
1807 $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
1808 $wanCache->getWithSetCallback( 'p', 30, $valFunc );
1809 $wanCache->getCheckKeyTime( 'zzz' );
1810 $wanCache->reap( 'x', time() - 300 );
1811 $wanCache->reap( 'zzz', time() - 300 );
1812 }
1813
1814 public function testMcRouterSupportBroadcastDelete() {
1815 $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
1816 ->setMethods( [ 'set' ] )->getMock();
1817 $wanCache = new WANObjectCache( [
1818 'cache' => $localBag,
1819 'mcrouterAware' => true,
1820 'region' => 'pmtpa',
1821 'cluster' => 'mw-wan'
1822 ] );
1823
1824 $localBag->expects( $this->once() )->method( 'set' )
1825 ->with( "/*/mw-wan/" . 'WANCache:v:' . "test" );
1826
1827 $wanCache->delete( 'test' );
1828 }
1829
1830 public function testMcRouterSupportBroadcastTouchCK() {
1831 $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
1832 ->setMethods( [ 'set' ] )->getMock();
1833 $wanCache = new WANObjectCache( [
1834 'cache' => $localBag,
1835 'mcrouterAware' => true,
1836 'region' => 'pmtpa',
1837 'cluster' => 'mw-wan'
1838 ] );
1839
1840 $localBag->expects( $this->once() )->method( 'set' )
1841 ->with( "/*/mw-wan/" . 'WANCache:t:' . "test" );
1842
1843 $wanCache->touchCheckKey( 'test' );
1844 }
1845
1846 public function testMcRouterSupportBroadcastResetCK() {
1847 $localBag = $this->getMockBuilder( EmptyBagOStuff::class )
1848 ->setMethods( [ 'delete' ] )->getMock();
1849 $wanCache = new WANObjectCache( [
1850 'cache' => $localBag,
1851 'mcrouterAware' => true,
1852 'region' => 'pmtpa',
1853 'cluster' => 'mw-wan'
1854 ] );
1855
1856 $localBag->expects( $this->once() )->method( 'delete' )
1857 ->with( "/*/mw-wan/" . 'WANCache:t:' . "test" );
1858
1859 $wanCache->resetCheckKey( 'test' );
1860 }
1861
1862 public function testEpoch() {
1863 $bag = new HashBagOStuff();
1864 $cache = new WANObjectCache( [ 'cache' => $bag ] );
1865 $key = $cache->makeGlobalKey( 'The whole of the Law' );
1866
1867 $now = microtime( true );
1868 $cache->setMockTime( $now );
1869
1870 $cache->set( $key, 'Do what thou Wilt' );
1871 $cache->touchCheckKey( $key );
1872
1873 $then = $now;
1874 $now += 30;
1875 $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
1876 $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key init', 0.01 );
1877
1878 $cache = new WANObjectCache( [
1879 'cache' => $bag,
1880 'epoch' => $now - 3600
1881 ] );
1882 $cache->setMockTime( $now );
1883
1884 $this->assertEquals( 'Do what thou Wilt', $cache->get( $key ) );
1885 $this->assertEquals( $then, $cache->getCheckKeyTime( $key ), 'Check key kept', 0.01 );
1886
1887 $now += 30;
1888 $cache = new WANObjectCache( [
1889 'cache' => $bag,
1890 'epoch' => $now + 3600
1891 ] );
1892 $cache->setMockTime( $now );
1893
1894 $this->assertFalse( $cache->get( $key ), 'Key rejected due to epoch' );
1895 $this->assertEquals( $now, $cache->getCheckKeyTime( $key ), 'Check key reset', 0.01 );
1896 }
1897
1898 /**
1899 * @dataProvider provideAdaptiveTTL
1900 * @covers WANObjectCache::adaptiveTTL()
1901 * @param float|int $ago
1902 * @param int $maxTTL
1903 * @param int $minTTL
1904 * @param float $factor
1905 * @param int $adaptiveTTL
1906 */
1907 public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
1908 $mtime = $ago ? time() - $ago : $ago;
1909 $margin = 5;
1910 $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
1911
1912 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1913 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
1914
1915 $ttl = $this->cache->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
1916
1917 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1918 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
1919 }
1920
1921 public static function provideAdaptiveTTL() {
1922 return [
1923 [ 3600, 900, 30, 0.2, 720 ],
1924 [ 3600, 500, 30, 0.2, 500 ],
1925 [ 3600, 86400, 800, 0.2, 800 ],
1926 [ false, 86400, 800, 0.2, 800 ],
1927 [ null, 86400, 800, 0.2, 800 ]
1928 ];
1929 }
1930
1931 /**
1932 * @covers WANObjectCache::__construct
1933 * @covers WANObjectCache::newEmpty
1934 */
1935 public function testNewEmpty() {
1936 $this->assertInstanceOf(
1937 WANObjectCache::class,
1938 WANObjectCache::newEmpty()
1939 );
1940 }
1941
1942 /**
1943 * @covers WANObjectCache::setLogger
1944 */
1945 public function testSetLogger() {
1946 $this->assertSame( null, $this->cache->setLogger( new Psr\Log\NullLogger ) );
1947 }
1948
1949 /**
1950 * @covers WANObjectCache::getQoS
1951 */
1952 public function testGetQoS() {
1953 $backend = $this->getMockBuilder( HashBagOStuff::class )
1954 ->setMethods( [ 'getQoS' ] )->getMock();
1955 $backend->expects( $this->once() )->method( 'getQoS' )
1956 ->willReturn( BagOStuff::QOS_UNKNOWN );
1957 $wanCache = new WANObjectCache( [ 'cache' => $backend ] );
1958
1959 $this->assertSame(
1960 $wanCache::QOS_UNKNOWN,
1961 $wanCache->getQoS( $wanCache::ATTR_EMULATION )
1962 );
1963 }
1964
1965 /**
1966 * @covers WANObjectCache::makeKey
1967 */
1968 public function testMakeKey() {
1969 if ( defined( 'HHVM_VERSION' ) ) {
1970 $this->markTestSkipped( 'HHVM Reflection buggy' );
1971 }
1972
1973 $backend = $this->getMockBuilder( HashBagOStuff::class )
1974 ->setMethods( [ 'makeKey' ] )->getMock();
1975 $backend->expects( $this->once() )->method( 'makeKey' )
1976 ->willReturn( 'special' );
1977
1978 $wanCache = new WANObjectCache( [
1979 'cache' => $backend
1980 ] );
1981
1982 $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) );
1983 }
1984
1985 /**
1986 * @covers WANObjectCache::makeGlobalKey
1987 */
1988 public function testMakeGlobalKey() {
1989 if ( defined( 'HHVM_VERSION' ) ) {
1990 $this->markTestSkipped( 'HHVM Reflection buggy' );
1991 }
1992
1993 $backend = $this->getMockBuilder( HashBagOStuff::class )
1994 ->setMethods( [ 'makeGlobalKey' ] )->getMock();
1995 $backend->expects( $this->once() )->method( 'makeGlobalKey' )
1996 ->willReturn( 'special' );
1997
1998 $wanCache = new WANObjectCache( [
1999 'cache' => $backend
2000 ] );
2001
2002 $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) );
2003 }
2004
2005 public static function statsKeyProvider() {
2006 return [
2007 [ 'domain:page:5', 'page' ],
2008 [ 'domain:main-key', 'main-key' ],
2009 [ 'domain:page:history', 'page' ],
2010 // Regression test for T232907
2011 [ 'domain:foo-bar-1.2:abc:v2', 'foo-bar-1_2' ],
2012 [ 'missingdomainkey', 'missingdomainkey' ]
2013 ];
2014 }
2015
2016 /**
2017 * @dataProvider statsKeyProvider
2018 * @covers WANObjectCache::determineKeyClassForStats
2019 */
2020 public function testStatsKeyClass( $key, $class ) {
2021 $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [
2022 'cache' => new HashBagOStuff
2023 ] ) );
2024
2025 $this->assertEquals( $class, $wanCache->determineKeyClassForStats( $key ) );
2026 }
2027
2028 /**
2029 * @covers WANObjectCache::makeMultiKeys
2030 */
2031 public function testMakeMultiKeys() {
2032 $cache = $this->cache;
2033
2034 $ids = [ 1, 2, 3, 4, 4, 5, 6, 6, 7, 7 ];
2035 $keyCallback = function ( $id, WANObjectCache $cache ) {
2036 return $cache->makeKey( 'key', $id );
2037 };
2038 $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
2039
2040 $expected = [
2041 "local:key:1" => 1,
2042 "local:key:2" => 2,
2043 "local:key:3" => 3,
2044 "local:key:4" => 4,
2045 "local:key:5" => 5,
2046 "local:key:6" => 6,
2047 "local:key:7" => 7
2048 ];
2049 $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
2050
2051 $ids = [ '1', '2', '3', '4', '4', '5', '6', '6', '7', '7' ];
2052 $keyCallback = function ( $id, WANObjectCache $cache ) {
2053 return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
2054 };
2055 $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
2056
2057 $expected = [
2058 "global:key:1:a:1:b" => '1',
2059 "global:key:2:a:2:b" => '2',
2060 "global:key:3:a:3:b" => '3',
2061 "global:key:4:a:4:b" => '4',
2062 "global:key:5:a:5:b" => '5',
2063 "global:key:6:a:6:b" => '6',
2064 "global:key:7:a:7:b" => '7'
2065 ];
2066 $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
2067 }
2068
2069 /**
2070 * @covers WANObjectCache::makeMultiKeys
2071 */
2072 public function testMakeMultiKeysIntString() {
2073 $cache = $this->cache;
2074 $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7, '7' ];
2075 $keyCallback = function ( $id, WANObjectCache $cache ) {
2076 return $cache->makeGlobalKey( 'key', $id, 'a', $id, 'b' );
2077 };
2078
2079 $keyedIds = $cache->makeMultiKeys( $ids, $keyCallback );
2080
2081 $expected = [
2082 "global:key:1:a:1:b" => 1,
2083 "global:key:2:a:2:b" => 2,
2084 "global:key:3:a:3:b" => 3,
2085 "global:key:4:a:4:b" => 4,
2086 "global:key:5:a:5:b" => 5,
2087 "global:key:6:a:6:b" => 6,
2088 "global:key:7:a:7:b" => 7
2089 ];
2090 $this->assertSame( $expected, iterator_to_array( $keyedIds ) );
2091 }
2092
2093 /**
2094 * @covers WANObjectCache::makeMultiKeys
2095 * @expectedException UnexpectedValueException
2096 */
2097 public function testMakeMultiKeysCollision() {
2098 $ids = [ 1, 2, 3, 4, '4', 5, 6, 6, 7 ];
2099
2100 $this->cache->makeMultiKeys(
2101 $ids,
2102 function ( $id ) {
2103 return "keymod:" . $id % 3;
2104 }
2105 );
2106 }
2107
2108 /**
2109 * @covers WANObjectCache::multiRemap
2110 */
2111 public function testMultiRemap() {
2112 $a = [ 'a', 'b', 'c' ];
2113 $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3 ];
2114
2115 $this->assertEquals(
2116 [ 'a' => 1, 'b' => 2, 'c' => 3 ],
2117 $this->cache->multiRemap( $a, $res )
2118 );
2119
2120 $a = [ 'a', 'b', 'c', 'c', 'd' ];
2121 $res = [ 'keyA' => 1, 'keyB' => 2, 'keyC' => 3, 'keyD' => 4 ];
2122
2123 $this->assertEquals(
2124 [ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 ],
2125 $this->cache->multiRemap( $a, $res )
2126 );
2127 }
2128
2129 /**
2130 * @covers WANObjectCache::hash256
2131 */
2132 public function testHash256() {
2133 $bag = new HashBagOStuff();
2134 $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 5 ] );
2135 $this->assertEquals(
2136 'f402bce76bfa1136adc705d8d5719911ce1fe61f0ad82ddf79a15f3c4de6ec4c',
2137 $cache->hash256( 'x' )
2138 );
2139
2140 $cache = new WANObjectCache( [ 'cache' => $bag, 'epoch' => 50 ] );
2141 $this->assertEquals(
2142 'f79a126722f0a682c4c500509f1b61e836e56c4803f92edc89fc281da5caa54e',
2143 $cache->hash256( 'x' )
2144 );
2145
2146 $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden' ] );
2147 $this->assertEquals(
2148 '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
2149 $cache->hash256( 'x' )
2150 );
2151
2152 $cache = new WANObjectCache( [ 'cache' => $bag, 'secret' => 'garden', 'epoch' => 3 ] );
2153 $this->assertEquals(
2154 '48cd57016ffe29981a1114c45e5daef327d30fc6206cb73edc3cb94b4d8fe093',
2155 $cache->hash256( 'x' )
2156 );
2157 }
2158 }
2159
2160 class NearExpiringWANObjectCache extends WANObjectCache {
2161 const CLOCK_SKEW = 1;
2162
2163 protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
2164 return ( $curTTL > 0 && ( $curTTL + self::CLOCK_SKEW ) < $lowTTL );
2165 }
2166 }
2167
2168 class PopularityRefreshingWANObjectCache extends WANObjectCache {
2169 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
2170 return ( ( $now - $asOf ) > $timeTillRefresh );
2171 }
2172 }