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