Merge "More user related Revision construction test cases"
[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::getNonProcessCachedKeys
15 * @covers WANObjectCache::getRawKeysForWarmup
16 * @covers WANObjectCache::getInterimValue
17 * @covers WANObjectCache::setInterimValue
18 */
19 class WANObjectCacheTest extends PHPUnit_Framework_TestCase {
20 /** @var WANObjectCache */
21 private $cache;
22 /** @var BagOStuff */
23 private $internalCache;
24
25 protected function setUp() {
26 parent::setUp();
27
28 $this->cache = new WANObjectCache( [
29 'cache' => new HashBagOStuff(),
30 'pool' => 'testcache-hash',
31 'relayer' => new EventRelayerNull( [] )
32 ] );
33
34 $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
35 /** @noinspection PhpUndefinedFieldInspection */
36 $this->internalCache = $wanCache->cache;
37 }
38
39 /**
40 * @dataProvider provideSetAndGet
41 * @covers WANObjectCache::set()
42 * @covers WANObjectCache::get()
43 * @covers WANObjectCache::makeKey()
44 * @param mixed $value
45 * @param int $ttl
46 */
47 public function testSetAndGet( $value, $ttl ) {
48 $curTTL = null;
49 $asOf = null;
50 $key = $this->cache->makeKey( 'x', wfRandomString() );
51
52 $this->cache->get( $key, $curTTL, [], $asOf );
53 $this->assertNull( $curTTL, "Current TTL is null" );
54 $this->assertNull( $asOf, "Current as-of-time is infinite" );
55
56 $t = microtime( true );
57 $this->cache->set( $key, $value, $ttl );
58
59 $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
60 if ( is_infinite( $ttl ) || $ttl == 0 ) {
61 $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
62 } else {
63 $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
64 $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
65 }
66 $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
67 $this->assertLessThanOrEqual( $t + 1, $asOf, "As-of-time in range of set() time" );
68 }
69
70 public static function provideSetAndGet() {
71 return [
72 [ 14141, 3 ],
73 [ 3535.666, 3 ],
74 [ [], 3 ],
75 [ null, 3 ],
76 [ '0', 3 ],
77 [ (object)[ 'meow' ], 3 ],
78 [ INF, 3 ],
79 [ '', 3 ],
80 [ 'pizzacat', INF ],
81 ];
82 }
83
84 /**
85 * @covers WANObjectCache::get()
86 * @covers WANObjectCache::makeGlobalKey()
87 */
88 public function testGetNotExists() {
89 $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
90 $curTTL = null;
91 $value = $this->cache->get( $key, $curTTL );
92
93 $this->assertFalse( $value, "Non-existing key has false value" );
94 $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
95 }
96
97 /**
98 * @covers WANObjectCache::set()
99 */
100 public function testSetOver() {
101 $key = wfRandomString();
102 for ( $i = 0; $i < 3; ++$i ) {
103 $value = wfRandomString();
104 $this->cache->set( $key, $value, 3 );
105
106 $this->assertEquals( $this->cache->get( $key ), $value );
107 }
108 }
109
110 /**
111 * @covers WANObjectCache::set()
112 */
113 public function testStaleSet() {
114 $key = wfRandomString();
115 $value = wfRandomString();
116 $this->cache->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
117
118 $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
119 }
120
121 public function testProcessCache() {
122 $hit = 0;
123 $callback = function () use ( &$hit ) {
124 ++$hit;
125 return 42;
126 };
127 $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
128 $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
129
130 foreach ( $keys as $i => $key ) {
131 $this->cache->getWithSetCallback(
132 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
133 }
134 $this->assertEquals( 3, $hit );
135
136 foreach ( $keys as $i => $key ) {
137 $this->cache->getWithSetCallback(
138 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
139 }
140 $this->assertEquals( 3, $hit, "Values cached" );
141
142 foreach ( $keys as $i => $key ) {
143 $this->cache->getWithSetCallback(
144 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
145 }
146 $this->assertEquals( 6, $hit );
147
148 foreach ( $keys as $i => $key ) {
149 $this->cache->getWithSetCallback(
150 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
151 }
152 $this->assertEquals( 6, $hit, "New values cached" );
153
154 foreach ( $keys as $i => $key ) {
155 $this->cache->delete( $key );
156 $this->cache->getWithSetCallback(
157 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
158 }
159 $this->assertEquals( 9, $hit, "Values evicted" );
160
161 $key = reset( $keys );
162 // Get into cache (default process cache group)
163 $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
164 $this->assertEquals( 10, $hit, "Value calculated" );
165 $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
166 $this->assertEquals( 10, $hit, "Value cached" );
167 $outerCallback = function () use ( &$callback, $key ) {
168 $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
169
170 return 43 + $v;
171 };
172 // Outer key misses and refuses inner key process cache value
173 $this->cache->getWithSetCallback( "$key-miss-outer", 100, $outerCallback );
174 $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
175 }
176
177 /**
178 * @dataProvider getWithSetCallback_provider
179 * @covers WANObjectCache::getWithSetCallback()
180 * @covers WANObjectCache::doGetWithSetCallback()
181 * @param array $extOpts
182 * @param bool $versioned
183 */
184 public function testGetWithSetCallback( array $extOpts, $versioned ) {
185 $cache = $this->cache;
186
187 $key = wfRandomString();
188 $value = wfRandomString();
189 $cKey1 = wfRandomString();
190 $cKey2 = wfRandomString();
191
192 $priorValue = null;
193 $priorAsOf = null;
194 $wasSet = 0;
195 $func = function ( $old, &$ttl, &$opts, $asOf )
196 use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
197 {
198 ++$wasSet;
199 $priorValue = $old;
200 $priorAsOf = $asOf;
201 $ttl = 20; // override with another value
202 return $value;
203 };
204
205 $wasSet = 0;
206 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
207 $this->assertEquals( $value, $v, "Value returned" );
208 $this->assertEquals( 1, $wasSet, "Value regenerated" );
209 $this->assertFalse( $priorValue, "No prior value" );
210 $this->assertNull( $priorAsOf, "No prior value" );
211
212 $curTTL = null;
213 $cache->get( $key, $curTTL );
214 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
215 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
216
217 $wasSet = 0;
218 $v = $cache->getWithSetCallback( $key, 30, $func, [
219 'lowTTL' => 0,
220 'lockTSE' => 5,
221 ] + $extOpts );
222 $this->assertEquals( $value, $v, "Value returned" );
223 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
224
225 $priorTime = microtime( true );
226 usleep( 1 );
227 $wasSet = 0;
228 $v = $cache->getWithSetCallback(
229 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
230 );
231 $this->assertEquals( $value, $v, "Value returned" );
232 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
233 $this->assertEquals( $value, $priorValue, "Has prior value" );
234 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
235 $t1 = $cache->getCheckKeyTime( $cKey1 );
236 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
237 $t2 = $cache->getCheckKeyTime( $cKey2 );
238 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
239
240 $priorTime = microtime( true );
241 $wasSet = 0;
242 $v = $cache->getWithSetCallback(
243 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
244 );
245 $this->assertEquals( $value, $v, "Value returned" );
246 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
247 $t1 = $cache->getCheckKeyTime( $cKey1 );
248 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
249 $t2 = $cache->getCheckKeyTime( $cKey2 );
250 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
251
252 $curTTL = null;
253 $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
254 if ( $versioned ) {
255 $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
256 } else {
257 $this->assertEquals( $value, $v, "Value returned" );
258 }
259 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
260
261 $wasSet = 0;
262 $key = wfRandomString();
263 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
264 $this->assertEquals( $value, $v, "Value returned" );
265 $cache->delete( $key );
266 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
267 $this->assertEquals( $value, $v, "Value still returned after deleted" );
268 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
269 }
270
271 public static function getWithSetCallback_provider() {
272 return [
273 [ [], false ],
274 [ [ 'version' => 1 ], true ]
275 ];
276 }
277
278 public function testPreemtiveRefresh() {
279 $value = 'KatCafe';
280 $wasSet = 0;
281 $func = function ( $old, &$ttl, &$opts, $asOf ) use ( &$wasSet, $value )
282 {
283 ++$wasSet;
284 return $value;
285 };
286
287 $cache = new NearExpiringWANObjectCache( [
288 'cache' => new HashBagOStuff(),
289 'pool' => 'empty'
290 ] );
291
292 $wasSet = 0;
293 $key = wfRandomString();
294 $opts = [ 'lowTTL' => 30 ];
295 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
296 $this->assertEquals( $value, $v, "Value returned" );
297 $this->assertEquals( 1, $wasSet, "Value calculated" );
298 $v = $cache->getWithSetCallback( $key, 20, $func, $opts );
299 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
300
301 $wasSet = 0;
302 $key = wfRandomString();
303 $opts = [ 'lowTTL' => 1 ];
304 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
305 $this->assertEquals( $value, $v, "Value returned" );
306 $this->assertEquals( 1, $wasSet, "Value calculated" );
307 $v = $cache->getWithSetCallback( $key, 30, $func, $opts );
308 $this->assertEquals( 1, $wasSet, "Value cached" );
309
310 $cache = new PopularityRefreshingWANObjectCache( [
311 'cache' => new HashBagOStuff(),
312 'pool' => 'empty'
313 ] );
314
315 $now = microtime( true ); // reference time
316 $wasSet = 0;
317 $key = wfRandomString();
318 $opts = [ 'hotTTR' => 900 ];
319 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
320 $this->assertEquals( $value, $v, "Value returned" );
321 $this->assertEquals( 1, $wasSet, "Value calculated" );
322 $cache->setTime( $now + 30 );
323 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
324 $this->assertEquals( 1, $wasSet, "Value cached" );
325
326 $wasSet = 0;
327 $key = wfRandomString();
328 $opts = [ 'hotTTR' => 10 ];
329 $cache->setTime( $now );
330 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
331 $this->assertEquals( $value, $v, "Value returned" );
332 $this->assertEquals( 1, $wasSet, "Value calculated" );
333 $cache->setTime( $now + 30 );
334 $v = $cache->getWithSetCallback( $key, 60, $func, $opts );
335 $this->assertEquals( 2, $wasSet, "Value re-calculated" );
336 }
337
338 /**
339 * @covers WANObjectCache::getWithSetCallback()
340 * @covers WANObjectCache::doGetWithSetCallback()
341 */
342 public function testGetWithSetCallback_invalidCallback() {
343 $this->setExpectedException( InvalidArgumentException::class );
344 $this->cache->getWithSetCallback( 'key', 30, 'invalid callback' );
345 }
346
347 /**
348 * @dataProvider getMultiWithSetCallback_provider
349 * @covers WANObjectCache::getMultiWithSetCallback
350 * @covers WANObjectCache::makeMultiKeys
351 * @covers WANObjectCache::getMulti
352 * @param array $extOpts
353 * @param bool $versioned
354 */
355 public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
356 $cache = $this->cache;
357
358 $keyA = wfRandomString();
359 $keyB = wfRandomString();
360 $keyC = wfRandomString();
361 $cKey1 = wfRandomString();
362 $cKey2 = wfRandomString();
363
364 $priorValue = null;
365 $priorAsOf = null;
366 $wasSet = 0;
367 $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
368 &$wasSet, &$priorValue, &$priorAsOf
369 ) {
370 ++$wasSet;
371 $priorValue = $old;
372 $priorAsOf = $asOf;
373 $ttl = 20; // override with another value
374 return "@$id$";
375 };
376
377 $wasSet = 0;
378 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
379 $value = "@3353$";
380 $v = $cache->getMultiWithSetCallback(
381 $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
382 $this->assertEquals( $value, $v[$keyA], "Value returned" );
383 $this->assertEquals( 1, $wasSet, "Value regenerated" );
384 $this->assertFalse( $priorValue, "No prior value" );
385 $this->assertNull( $priorAsOf, "No prior value" );
386
387 $curTTL = null;
388 $cache->get( $keyA, $curTTL );
389 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
390 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
391
392 $wasSet = 0;
393 $value = "@efef$";
394 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
395 $v = $cache->getMultiWithSetCallback(
396 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
397 $this->assertEquals( $value, $v[$keyB], "Value returned" );
398 $this->assertEquals( 1, $wasSet, "Value regenerated" );
399 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
400 $v = $cache->getMultiWithSetCallback(
401 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
402 $this->assertEquals( $value, $v[$keyB], "Value returned" );
403 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
404 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
405
406 $priorTime = microtime( true );
407 usleep( 1 );
408 $wasSet = 0;
409 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
410 $v = $cache->getMultiWithSetCallback(
411 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
412 );
413 $this->assertEquals( $value, $v[$keyB], "Value returned" );
414 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
415 $this->assertEquals( $value, $priorValue, "Has prior value" );
416 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
417 $t1 = $cache->getCheckKeyTime( $cKey1 );
418 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
419 $t2 = $cache->getCheckKeyTime( $cKey2 );
420 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
421
422 $priorTime = microtime( true );
423 $value = "@43636$";
424 $wasSet = 0;
425 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
426 $v = $cache->getMultiWithSetCallback(
427 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
428 );
429 $this->assertEquals( $value, $v[$keyC], "Value returned" );
430 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
431 $t1 = $cache->getCheckKeyTime( $cKey1 );
432 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
433 $t2 = $cache->getCheckKeyTime( $cKey2 );
434 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
435
436 $curTTL = null;
437 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
438 if ( $versioned ) {
439 $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
440 } else {
441 $this->assertEquals( $value, $v, "Value returned" );
442 }
443 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
444
445 $wasSet = 0;
446 $key = wfRandomString();
447 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
448 $v = $cache->getMultiWithSetCallback(
449 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
450 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
451 $cache->delete( $key );
452 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
453 $v = $cache->getMultiWithSetCallback(
454 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
455 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
456 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
457
458 $calls = 0;
459 $ids = [ 1, 2, 3, 4, 5, 6 ];
460 $keyFunc = function ( $id, WANObjectCache $wanCache ) {
461 return $wanCache->makeKey( 'test', $id );
462 };
463 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
464 $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
465 ++$calls;
466
467 return "val-{$id}";
468 };
469 $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
470
471 $this->assertEquals(
472 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
473 array_values( $values ),
474 "Correct values in correct order"
475 );
476 $this->assertEquals(
477 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
478 array_keys( $values ),
479 "Correct keys in correct order"
480 );
481 $this->assertEquals( count( $ids ), $calls );
482
483 $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
484 $this->assertEquals( count( $ids ), $calls, "Values cached" );
485
486 // Mock the BagOStuff to assure only one getMulti() call given process caching
487 $localBag = $this->getMockBuilder( 'HashBagOStuff' )
488 ->setMethods( [ 'getMulti' ] )->getMock();
489 $localBag->expects( $this->exactly( 1 ) )->method( 'getMulti' )->willReturn( [
490 WANObjectCache::VALUE_KEY_PREFIX . 'k1' => 'val-id1',
491 WANObjectCache::VALUE_KEY_PREFIX . 'k2' => 'val-id2'
492 ] );
493 $wanCache = new WANObjectCache( [ 'cache' => $localBag, 'pool' => 'testcache-hash' ] );
494
495 // Warm the process cache
496 $keyedIds = new ArrayIterator( [ 'k1' => 'id1', 'k2' => 'id2' ] );
497 $this->assertEquals(
498 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
499 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
500 );
501 // Use the process cache
502 $this->assertEquals(
503 [ 'k1' => 'val-id1', 'k2' => 'val-id2' ],
504 $wanCache->getMultiWithSetCallback( $keyedIds, 10, $genFunc, [ 'pcTTL' => 5 ] )
505 );
506 }
507
508 public static function getMultiWithSetCallback_provider() {
509 return [
510 [ [], false ],
511 [ [ 'version' => 1 ], true ]
512 ];
513 }
514
515 /**
516 * @dataProvider getMultiWithUnionSetCallback_provider
517 * @covers WANObjectCache::getMultiWithUnionSetCallback()
518 * @covers WANObjectCache::makeMultiKeys()
519 * @param array $extOpts
520 * @param bool $versioned
521 */
522 public function testGetMultiWithUnionSetCallback( array $extOpts, $versioned ) {
523 $cache = $this->cache;
524
525 $keyA = wfRandomString();
526 $keyB = wfRandomString();
527 $keyC = wfRandomString();
528 $cKey1 = wfRandomString();
529 $cKey2 = wfRandomString();
530
531 $wasSet = 0;
532 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use (
533 &$wasSet, &$priorValue, &$priorAsOf
534 ) {
535 $newValues = [];
536 foreach ( $ids as $id ) {
537 ++$wasSet;
538 $newValues[$id] = "@$id$";
539 $ttls[$id] = 20; // override with another value
540 }
541
542 return $newValues;
543 };
544
545 $wasSet = 0;
546 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
547 $value = "@3353$";
548 $v = $cache->getMultiWithUnionSetCallback(
549 $keyedIds, 30, $genFunc, $extOpts );
550 $this->assertEquals( $value, $v[$keyA], "Value returned" );
551 $this->assertEquals( 1, $wasSet, "Value regenerated" );
552
553 $curTTL = null;
554 $cache->get( $keyA, $curTTL );
555 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
556 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
557
558 $wasSet = 0;
559 $value = "@efef$";
560 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
561 $v = $cache->getMultiWithUnionSetCallback(
562 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
563 $this->assertEquals( $value, $v[$keyB], "Value returned" );
564 $this->assertEquals( 1, $wasSet, "Value regenerated" );
565 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed yet in process cache" );
566 $v = $cache->getMultiWithUnionSetCallback(
567 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0 ] + $extOpts );
568 $this->assertEquals( $value, $v[$keyB], "Value returned" );
569 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
570 $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
571
572 $priorTime = microtime( true );
573 usleep( 1 );
574 $wasSet = 0;
575 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
576 $v = $cache->getMultiWithUnionSetCallback(
577 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
578 );
579 $this->assertEquals( $value, $v[$keyB], "Value returned" );
580 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
581 $t1 = $cache->getCheckKeyTime( $cKey1 );
582 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
583 $t2 = $cache->getCheckKeyTime( $cKey2 );
584 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
585
586 $priorTime = microtime( true );
587 $value = "@43636$";
588 $wasSet = 0;
589 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
590 $v = $cache->getMultiWithUnionSetCallback(
591 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
592 );
593 $this->assertEquals( $value, $v[$keyC], "Value returned" );
594 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
595 $t1 = $cache->getCheckKeyTime( $cKey1 );
596 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
597 $t2 = $cache->getCheckKeyTime( $cKey2 );
598 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
599
600 $curTTL = null;
601 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
602 if ( $versioned ) {
603 $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
604 } else {
605 $this->assertEquals( $value, $v, "Value returned" );
606 }
607 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
608
609 $wasSet = 0;
610 $key = wfRandomString();
611 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
612 $v = $cache->getMultiWithUnionSetCallback(
613 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
614 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
615 $cache->delete( $key );
616 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
617 $v = $cache->getMultiWithUnionSetCallback(
618 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
619 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
620 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
621
622 $calls = 0;
623 $ids = [ 1, 2, 3, 4, 5, 6 ];
624 $keyFunc = function ( $id, WANObjectCache $wanCache ) {
625 return $wanCache->makeKey( 'test', $id );
626 };
627 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
628 $genFunc = function ( array $ids, array &$ttls, array &$setOpts ) use ( &$calls ) {
629 $newValues = [];
630 foreach ( $ids as $id ) {
631 ++$calls;
632 $newValues[$id] = "val-{$id}";
633 }
634
635 return $newValues;
636 };
637 $values = $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
638
639 $this->assertEquals(
640 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
641 array_values( $values ),
642 "Correct values in correct order"
643 );
644 $this->assertEquals(
645 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
646 array_keys( $values ),
647 "Correct keys in correct order"
648 );
649 $this->assertEquals( count( $ids ), $calls );
650
651 $cache->getMultiWithUnionSetCallback( $keyedIds, 10, $genFunc );
652 $this->assertEquals( count( $ids ), $calls, "Values cached" );
653 }
654
655 public static function getMultiWithUnionSetCallback_provider() {
656 return [
657 [ [], false ],
658 [ [ 'version' => 1 ], true ]
659 ];
660 }
661
662 /**
663 * @covers WANObjectCache::getWithSetCallback()
664 * @covers WANObjectCache::doGetWithSetCallback()
665 */
666 public function testLockTSE() {
667 $cache = $this->cache;
668 $key = wfRandomString();
669 $value = wfRandomString();
670
671 $calls = 0;
672 $func = function () use ( &$calls, $value, $cache, $key ) {
673 ++$calls;
674 // Immediately kill any mutex rather than waiting a second
675 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
676 return $value;
677 };
678
679 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
680 $this->assertEquals( $value, $ret );
681 $this->assertEquals( 1, $calls, 'Value was populated' );
682
683 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
684 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
685
686 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
687 $ret = $cache->getWithSetCallback( $key, 30, $func,
688 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
689 $this->assertEquals( $value, $ret, 'Old value used' );
690 $this->assertEquals( 1, $calls, 'Callback was not used' );
691
692 $cache->delete( $key );
693 $ret = $cache->getWithSetCallback( $key, 30, $func,
694 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
695 $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
696 $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
697
698 $ret = $cache->getWithSetCallback( $key, 30, $func,
699 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
700 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
701 $this->assertEquals( 2, $calls, 'Callback was not used; used interim' );
702 }
703
704 /**
705 * @covers WANObjectCache::getWithSetCallback()
706 * @covers WANObjectCache::doGetWithSetCallback()
707 * @covers WANObjectCache::set()
708 */
709 public function testLockTSESlow() {
710 $cache = $this->cache;
711 $key = wfRandomString();
712 $value = wfRandomString();
713
714 $calls = 0;
715 $func = function ( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
716 ++$calls;
717 $setOpts['since'] = microtime( true ) - 10;
718 // Immediately kill any mutex rather than waiting a second
719 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
720 return $value;
721 };
722
723 // Value should be marked as stale due to snapshot lag
724 $curTTL = null;
725 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
726 $this->assertEquals( $value, $ret );
727 $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
728 $this->assertLessThan( 0, $curTTL, 'Value has negative curTTL' );
729 $this->assertEquals( 1, $calls, 'Value was generated' );
730
731 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
732 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
733 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
734 $this->assertEquals( $value, $ret );
735 $this->assertEquals( 1, $calls, 'Callback was not used' );
736 }
737
738 /**
739 * @covers WANObjectCache::getWithSetCallback()
740 * @covers WANObjectCache::doGetWithSetCallback()
741 */
742 public function testBusyValue() {
743 $cache = $this->cache;
744 $key = wfRandomString();
745 $value = wfRandomString();
746 $busyValue = wfRandomString();
747
748 $calls = 0;
749 $func = function () use ( &$calls, $value, $cache, $key ) {
750 ++$calls;
751 // Immediately kill any mutex rather than waiting a second
752 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
753 return $value;
754 };
755
756 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
757 $this->assertEquals( $value, $ret );
758 $this->assertEquals( 1, $calls, 'Value was populated' );
759
760 // Acquire a lock to verify that getWithSetCallback uses busyValue properly
761 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
762
763 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
764 $ret = $cache->getWithSetCallback( $key, 30, $func,
765 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
766 $this->assertEquals( $value, $ret, 'Callback used' );
767 $this->assertEquals( 2, $calls, 'Callback used' );
768
769 $ret = $cache->getWithSetCallback( $key, 30, $func,
770 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
771 $this->assertEquals( $value, $ret, 'Old value used' );
772 $this->assertEquals( 2, $calls, 'Callback was not used' );
773
774 $cache->delete( $key ); // no value at all anymore and still locked
775 $ret = $cache->getWithSetCallback( $key, 30, $func,
776 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
777 $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
778 $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
779
780 $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
781 $ret = $cache->getWithSetCallback( $key, 30, $func,
782 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
783 $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
784 $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
785
786 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
787 $ret = $cache->getWithSetCallback( $key, 30, $func,
788 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
789 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
790 $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
791 }
792
793 /**
794 * @covers WANObjectCache::getMulti()
795 */
796 public function testGetMulti() {
797 $cache = $this->cache;
798
799 $value1 = [ 'this' => 'is', 'a' => 'test' ];
800 $value2 = [ 'this' => 'is', 'another' => 'test' ];
801
802 $key1 = wfRandomString();
803 $key2 = wfRandomString();
804 $key3 = wfRandomString();
805
806 $cache->set( $key1, $value1, 5 );
807 $cache->set( $key2, $value2, 10 );
808
809 $curTTLs = [];
810 $this->assertEquals(
811 [ $key1 => $value1, $key2 => $value2 ],
812 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
813 'Result array populated'
814 );
815
816 $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
817 $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
818 $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
819
820 $cKey1 = wfRandomString();
821 $cKey2 = wfRandomString();
822
823 $priorTime = microtime( true );
824 usleep( 1 );
825 $curTTLs = [];
826 $this->assertEquals(
827 [ $key1 => $value1, $key2 => $value2 ],
828 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
829 "Result array populated even with new check keys"
830 );
831 $t1 = $cache->getCheckKeyTime( $cKey1 );
832 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
833 $t2 = $cache->getCheckKeyTime( $cKey2 );
834 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
835 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
836 $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
837 $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
838
839 usleep( 1 );
840 $curTTLs = [];
841 $this->assertEquals(
842 [ $key1 => $value1, $key2 => $value2 ],
843 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
844 "Result array still populated even with new check keys"
845 );
846 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
847 $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
848 $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
849 }
850
851 /**
852 * @covers WANObjectCache::getMulti()
853 * @covers WANObjectCache::processCheckKeys()
854 */
855 public function testGetMultiCheckKeys() {
856 $cache = $this->cache;
857
858 $checkAll = wfRandomString();
859 $check1 = wfRandomString();
860 $check2 = wfRandomString();
861 $check3 = wfRandomString();
862 $value1 = wfRandomString();
863 $value2 = wfRandomString();
864
865 // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
866 // several seconds during the test to assert the behaviour.
867 foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
868 $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE );
869 }
870 usleep( 100 );
871
872 $cache->set( 'key1', $value1, 10 );
873 $cache->set( 'key2', $value2, 10 );
874
875 $curTTLs = [];
876 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
877 'key1' => $check1,
878 $checkAll,
879 'key2' => $check2,
880 'key3' => $check3,
881 ] );
882 $this->assertEquals(
883 [ 'key1' => $value1, 'key2' => $value2 ],
884 $result,
885 'Initial values'
886 );
887 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
888 $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
889 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
890 $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
891
892 $cache->touchCheckKey( $check1 );
893
894 $curTTLs = [];
895 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
896 'key1' => $check1,
897 $checkAll,
898 'key2' => $check2,
899 'key3' => $check3,
900 ] );
901 $this->assertEquals(
902 [ 'key1' => $value1, 'key2' => $value2 ],
903 $result,
904 'key1 expired by check1, but value still provided'
905 );
906 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
907 $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
908
909 $cache->touchCheckKey( $checkAll );
910
911 $curTTLs = [];
912 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
913 'key1' => $check1,
914 $checkAll,
915 'key2' => $check2,
916 'key3' => $check3,
917 ] );
918 $this->assertEquals(
919 [ 'key1' => $value1, 'key2' => $value2 ],
920 $result,
921 'All keys expired by checkAll, but value still provided'
922 );
923 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
924 $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
925 }
926
927 /**
928 * @covers WANObjectCache::get()
929 * @covers WANObjectCache::processCheckKeys()
930 */
931 public function testCheckKeyInitHoldoff() {
932 $cache = $this->cache;
933
934 for ( $i = 0; $i < 500; ++$i ) {
935 $key = wfRandomString();
936 $checkKey = wfRandomString();
937 // miss, set, hit
938 $cache->get( $key, $curTTL, [ $checkKey ] );
939 $cache->set( $key, 'val', 10 );
940 $curTTL = null;
941 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
942
943 $this->assertEquals( 'val', $v );
944 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
945 }
946
947 for ( $i = 0; $i < 500; ++$i ) {
948 $key = wfRandomString();
949 $checkKey = wfRandomString();
950 // set, hit
951 $cache->set( $key, 'val', 10 );
952 $curTTL = null;
953 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
954
955 $this->assertEquals( 'val', $v );
956 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
957 }
958 }
959
960 /**
961 * @covers WANObjectCache::delete
962 * @covers WANObjectCache::relayDelete
963 * @covers WANObjectCache::relayPurge
964 */
965 public function testDelete() {
966 $key = wfRandomString();
967 $value = wfRandomString();
968 $this->cache->set( $key, $value );
969
970 $curTTL = null;
971 $v = $this->cache->get( $key, $curTTL );
972 $this->assertEquals( $value, $v, "Key was created with value" );
973 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
974
975 $this->cache->delete( $key );
976
977 $curTTL = null;
978 $v = $this->cache->get( $key, $curTTL );
979 $this->assertFalse( $v, "Deleted key has false value" );
980 $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
981
982 $this->cache->set( $key, $value . 'more' );
983 $v = $this->cache->get( $key, $curTTL );
984 $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
985 $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
986
987 $this->cache->set( $key, $value );
988 $this->cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
989
990 $curTTL = null;
991 $v = $this->cache->get( $key, $curTTL );
992 $this->assertFalse( $v, "Deleted key has false value" );
993 $this->assertNull( $curTTL, "Deleted key has null current TTL" );
994
995 $this->cache->set( $key, $value );
996 $v = $this->cache->get( $key, $curTTL );
997 $this->assertEquals( $value, $v, "Key was created with value" );
998 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
999 }
1000
1001 /**
1002 * @dataProvider getWithSetCallback_versions_provider
1003 * @covers WANObjectCache::getWithSetCallback()
1004 * @covers WANObjectCache::doGetWithSetCallback()
1005 * @param array $extOpts
1006 * @param bool $versioned
1007 */
1008 public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
1009 $cache = $this->cache;
1010
1011 $key = wfRandomString();
1012 $value = wfRandomString();
1013
1014 $wasSet = 0;
1015 $func = function ( $old, &$ttl ) use ( &$wasSet, $value ) {
1016 ++$wasSet;
1017 return $value;
1018 };
1019
1020 // Set the main key (version N if versioned)
1021 $wasSet = 0;
1022 $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts );
1023 $this->assertEquals( $value, $v, "Value returned" );
1024 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1025 $cache->getWithSetCallback( $key, 30, $func, $extOpts );
1026 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
1027 // Set the key for version N+1 (if versioned)
1028 if ( $versioned ) {
1029 $verOpts = [ 'version' => $extOpts['version'] + 1 ];
1030
1031 $wasSet = 0;
1032 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
1033 $this->assertEquals( $value, $v, "Value returned" );
1034 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1035
1036 $wasSet = 0;
1037 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
1038 $this->assertEquals( $value, $v, "Value returned" );
1039 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
1040 }
1041
1042 $wasSet = 0;
1043 $cache->getWithSetCallback( $key, 30, $func, $extOpts );
1044 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
1045
1046 $wasSet = 0;
1047 $cache->delete( $key );
1048 $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts );
1049 $this->assertEquals( $value, $v, "Value returned" );
1050 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1051
1052 if ( $versioned ) {
1053 $wasSet = 0;
1054 $verOpts = [ 'version' => $extOpts['version'] + 1 ];
1055 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
1056 $this->assertEquals( $value, $v, "Value returned" );
1057 $this->assertEquals( 1, $wasSet, "Value regenerated" );
1058 }
1059 }
1060
1061 public static function getWithSetCallback_versions_provider() {
1062 return [
1063 [ [], false ],
1064 [ [ 'version' => 1 ], true ]
1065 ];
1066 }
1067
1068 /**
1069 * @covers WANObjectCache::touchCheckKey
1070 * @covers WANObjectCache::resetCheckKey
1071 * @covers WANObjectCache::getCheckKeyTime
1072 * @covers WANObjectCache::makePurgeValue
1073 * @covers WANObjectCache::parsePurgeValue
1074 */
1075 public function testTouchKeys() {
1076 $key = wfRandomString();
1077
1078 $priorTime = microtime( true );
1079 usleep( 100 );
1080 $t0 = $this->cache->getCheckKeyTime( $key );
1081 $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
1082
1083 $priorTime = microtime( true );
1084 usleep( 100 );
1085 $this->cache->touchCheckKey( $key );
1086 $t1 = $this->cache->getCheckKeyTime( $key );
1087 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
1088
1089 $t2 = $this->cache->getCheckKeyTime( $key );
1090 $this->assertEquals( $t1, $t2, 'Check key time did not change' );
1091
1092 usleep( 100 );
1093 $this->cache->touchCheckKey( $key );
1094 $t3 = $this->cache->getCheckKeyTime( $key );
1095 $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
1096
1097 $t4 = $this->cache->getCheckKeyTime( $key );
1098 $this->assertEquals( $t3, $t4, 'Check key time did not change' );
1099
1100 usleep( 100 );
1101 $this->cache->resetCheckKey( $key );
1102 $t5 = $this->cache->getCheckKeyTime( $key );
1103 $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
1104
1105 $t6 = $this->cache->getCheckKeyTime( $key );
1106 $this->assertEquals( $t5, $t6, 'Check key time did not change' );
1107 }
1108
1109 /**
1110 * @covers WANObjectCache::getMulti()
1111 */
1112 public function testGetWithSeveralCheckKeys() {
1113 $key = wfRandomString();
1114 $tKey1 = wfRandomString();
1115 $tKey2 = wfRandomString();
1116 $value = 'meow';
1117
1118 // Two check keys are newer (given hold-off) than $key, another is older
1119 $this->internalCache->set(
1120 WANObjectCache::TIME_KEY_PREFIX . $tKey2,
1121 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 3 )
1122 );
1123 $this->internalCache->set(
1124 WANObjectCache::TIME_KEY_PREFIX . $tKey2,
1125 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 5 )
1126 );
1127 $this->internalCache->set(
1128 WANObjectCache::TIME_KEY_PREFIX . $tKey1,
1129 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 30 )
1130 );
1131 $this->cache->set( $key, $value, 30 );
1132
1133 $curTTL = null;
1134 $v = $this->cache->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
1135 $this->assertEquals( $value, $v, "Value matches" );
1136 $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
1137 $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
1138 }
1139
1140 /**
1141 * @covers WANObjectCache::reap()
1142 * @covers WANObjectCache::reapCheckKey()
1143 */
1144 public function testReap() {
1145 $vKey1 = wfRandomString();
1146 $vKey2 = wfRandomString();
1147 $tKey1 = wfRandomString();
1148 $tKey2 = wfRandomString();
1149 $value = 'moo';
1150
1151 $knownPurge = time() - 60;
1152 $goodTime = microtime( true ) - 5;
1153 $badTime = microtime( true ) - 300;
1154
1155 $this->internalCache->set(
1156 WANObjectCache::VALUE_KEY_PREFIX . $vKey1,
1157 [
1158 WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
1159 WANObjectCache::FLD_VALUE => $value,
1160 WANObjectCache::FLD_TTL => 3600,
1161 WANObjectCache::FLD_TIME => $goodTime
1162 ]
1163 );
1164 $this->internalCache->set(
1165 WANObjectCache::VALUE_KEY_PREFIX . $vKey2,
1166 [
1167 WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
1168 WANObjectCache::FLD_VALUE => $value,
1169 WANObjectCache::FLD_TTL => 3600,
1170 WANObjectCache::FLD_TIME => $badTime
1171 ]
1172 );
1173 $this->internalCache->set(
1174 WANObjectCache::TIME_KEY_PREFIX . $tKey1,
1175 WANObjectCache::PURGE_VAL_PREFIX . $goodTime
1176 );
1177 $this->internalCache->set(
1178 WANObjectCache::TIME_KEY_PREFIX . $tKey2,
1179 WANObjectCache::PURGE_VAL_PREFIX . $badTime
1180 );
1181
1182 $this->assertEquals( $value, $this->cache->get( $vKey1 ) );
1183 $this->assertEquals( $value, $this->cache->get( $vKey2 ) );
1184 $this->cache->reap( $vKey1, $knownPurge, $bad1 );
1185 $this->cache->reap( $vKey2, $knownPurge, $bad2 );
1186
1187 $this->assertFalse( $bad1 );
1188 $this->assertTrue( $bad2 );
1189
1190 $this->cache->reapCheckKey( $tKey1, $knownPurge, $tBad1 );
1191 $this->cache->reapCheckKey( $tKey2, $knownPurge, $tBad2 );
1192 $this->assertFalse( $tBad1 );
1193 $this->assertTrue( $tBad2 );
1194 }
1195
1196 /**
1197 * @covers WANObjectCache::reap()
1198 */
1199 public function testReap_fail() {
1200 $backend = $this->getMockBuilder( EmptyBagOStuff::class )
1201 ->setMethods( [ 'get', 'changeTTL' ] )->getMock();
1202 $backend->expects( $this->once() )->method( 'get' )
1203 ->willReturn( [
1204 WANObjectCache::FLD_VERSION => WANObjectCache::VERSION,
1205 WANObjectCache::FLD_VALUE => 'value',
1206 WANObjectCache::FLD_TTL => 3600,
1207 WANObjectCache::FLD_TIME => 300,
1208 ] );
1209 $backend->expects( $this->once() )->method( 'changeTTL' )
1210 ->willReturn( false );
1211
1212 $wanCache = new WANObjectCache( [
1213 'cache' => $backend,
1214 'pool' => 'testcache-hash',
1215 'relayer' => new EventRelayerNull( [] )
1216 ] );
1217
1218 $isStale = null;
1219 $ret = $wanCache->reap( 'key', 360, $isStale );
1220 $this->assertTrue( $isStale, 'value was stale' );
1221 $this->assertFalse( $ret, 'changeTTL failed' );
1222 }
1223
1224 /**
1225 * @covers WANObjectCache::set()
1226 */
1227 public function testSetWithLag() {
1228 $value = 1;
1229
1230 $key = wfRandomString();
1231 $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
1232 $this->cache->set( $key, $value, 30, $opts );
1233 $this->assertEquals( $value, $this->cache->get( $key ), "Rep-lagged value written." );
1234
1235 $key = wfRandomString();
1236 $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
1237 $this->cache->set( $key, $value, 30, $opts );
1238 $this->assertEquals( false, $this->cache->get( $key ), "Trx-lagged value not written." );
1239
1240 $key = wfRandomString();
1241 $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
1242 $this->cache->set( $key, $value, 30, $opts );
1243 $this->assertEquals( false, $this->cache->get( $key ), "Lagged value not written." );
1244 }
1245
1246 /**
1247 * @covers WANObjectCache::set()
1248 */
1249 public function testWritePending() {
1250 $value = 1;
1251
1252 $key = wfRandomString();
1253 $opts = [ 'pending' => true ];
1254 $this->cache->set( $key, $value, 30, $opts );
1255 $this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
1256 }
1257
1258 public function testMcRouterSupport() {
1259 $localBag = $this->getMockBuilder( 'EmptyBagOStuff' )
1260 ->setMethods( [ 'set', 'delete' ] )->getMock();
1261 $localBag->expects( $this->never() )->method( 'set' );
1262 $localBag->expects( $this->never() )->method( 'delete' );
1263 $wanCache = new WANObjectCache( [
1264 'cache' => $localBag,
1265 'pool' => 'testcache-hash',
1266 'relayer' => new EventRelayerNull( [] )
1267 ] );
1268 $valFunc = function () {
1269 return 1;
1270 };
1271
1272 // None of these should use broadcasting commands (e.g. SET, DELETE)
1273 $wanCache->get( 'x' );
1274 $wanCache->get( 'x', $ctl, [ 'check1' ] );
1275 $wanCache->getMulti( [ 'x', 'y' ] );
1276 $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
1277 $wanCache->getWithSetCallback( 'p', 30, $valFunc );
1278 $wanCache->getCheckKeyTime( 'zzz' );
1279 $wanCache->reap( 'x', time() - 300 );
1280 $wanCache->reap( 'zzz', time() - 300 );
1281 }
1282
1283 /**
1284 * @dataProvider provideAdaptiveTTL
1285 * @covers WANObjectCache::adaptiveTTL()
1286 * @param float|int $ago
1287 * @param int $maxTTL
1288 * @param int $minTTL
1289 * @param float $factor
1290 * @param int $adaptiveTTL
1291 */
1292 public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
1293 $mtime = $ago ? time() - $ago : $ago;
1294 $margin = 5;
1295 $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
1296
1297 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1298 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
1299
1300 $ttl = $this->cache->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
1301
1302 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
1303 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
1304 }
1305
1306 public static function provideAdaptiveTTL() {
1307 return [
1308 [ 3600, 900, 30, 0.2, 720 ],
1309 [ 3600, 500, 30, 0.2, 500 ],
1310 [ 3600, 86400, 800, 0.2, 800 ],
1311 [ false, 86400, 800, 0.2, 800 ],
1312 [ null, 86400, 800, 0.2, 800 ]
1313 ];
1314 }
1315
1316 /**
1317 * @covers WANObjectCache::__construct
1318 * @covers WANObjectCache::newEmpty
1319 */
1320 public function testNewEmpty() {
1321 $this->assertInstanceOf(
1322 WANObjectCache::class,
1323 WANObjectCache::newEmpty()
1324 );
1325 }
1326
1327 /**
1328 * @covers WANObjectCache::setLogger
1329 */
1330 public function testSetLogger() {
1331 $this->assertSame( null, $this->cache->setLogger( new Psr\Log\NullLogger ) );
1332 }
1333
1334 /**
1335 * @covers WANObjectCache::getQoS
1336 */
1337 public function testGetQoS() {
1338 $backend = $this->getMockBuilder( HashBagOStuff::class )
1339 ->setMethods( [ 'getQoS' ] )->getMock();
1340 $backend->expects( $this->once() )->method( 'getQoS' )
1341 ->willReturn( BagOStuff::QOS_UNKNOWN );
1342 $wanCache = new WANObjectCache( [ 'cache' => $backend ] );
1343
1344 $this->assertSame(
1345 $wanCache::QOS_UNKNOWN,
1346 $wanCache->getQoS( $wanCache::ATTR_EMULATION )
1347 );
1348 }
1349
1350 /**
1351 * @covers WANObjectCache::makeKey
1352 */
1353 public function testMakeKey() {
1354 $backend = $this->getMockBuilder( HashBagOStuff::class )
1355 ->setMethods( [ 'makeKey' ] )->getMock();
1356 $backend->expects( $this->once() )->method( 'makeKey' )
1357 ->willReturn( 'special' );
1358
1359 $wanCache = new WANObjectCache( [
1360 'cache' => $backend,
1361 'pool' => 'testcache-hash',
1362 'relayer' => new EventRelayerNull( [] )
1363 ] );
1364
1365 $this->assertSame( 'special', $wanCache->makeKey( 'a', 'b' ) );
1366 }
1367
1368 /**
1369 * @covers WANObjectCache::makeGlobalKey
1370 */
1371 public function testMakeGlobalKey() {
1372 $backend = $this->getMockBuilder( HashBagOStuff::class )
1373 ->setMethods( [ 'makeGlobalKey' ] )->getMock();
1374 $backend->expects( $this->once() )->method( 'makeGlobalKey' )
1375 ->willReturn( 'special' );
1376
1377 $wanCache = new WANObjectCache( [
1378 'cache' => $backend,
1379 'pool' => 'testcache-hash',
1380 'relayer' => new EventRelayerNull( [] )
1381 ] );
1382
1383 $this->assertSame( 'special', $wanCache->makeGlobalKey( 'a', 'b' ) );
1384 }
1385
1386 public static function statsKeyProvider() {
1387 return [
1388 [ 'domain:page:5', 'page' ],
1389 [ 'domain:main-key', 'main-key' ],
1390 [ 'domain:page:history', 'page' ],
1391 [ 'missingdomainkey', 'missingdomainkey' ]
1392 ];
1393 }
1394
1395 /**
1396 * @dataProvider statsKeyProvider
1397 * @covers WANObjectCache::determineKeyClass
1398 */
1399 public function testStatsKeyClass( $key, $class ) {
1400 $wanCache = TestingAccessWrapper::newFromObject( new WANObjectCache( [
1401 'cache' => new HashBagOStuff,
1402 'pool' => 'testcache-hash',
1403 'relayer' => new EventRelayerNull( [] )
1404 ] ) );
1405
1406 $this->assertEquals( $class, $wanCache->determineKeyClass( $key ) );
1407 }
1408 }
1409
1410 class TimeAdjustableHashBagOStuff extends HashBagOStuff {
1411 private $timeOverride = 0;
1412
1413 public function setTime( $time ) {
1414 $this->timeOverride = $time;
1415 }
1416
1417 protected function getCurrentTime() {
1418 return $this->timeOverride ?: parent::getCurrentTime();
1419 }
1420 }
1421
1422 class TimeAdjustableWANObjectCache extends WANObjectCache {
1423 private $timeOverride = 0;
1424
1425 public function setTime( $time ) {
1426 $this->timeOverride = $time;
1427 if ( $this->cache instanceof TimeAdjustableHashBagOStuff ) {
1428 $this->cache->setTime( $time );
1429 }
1430 }
1431
1432 protected function getCurrentTime() {
1433 return $this->timeOverride ?: parent::getCurrentTime();
1434 }
1435 }
1436
1437 class NearExpiringWANObjectCache extends TimeAdjustableWANObjectCache {
1438 const CLOCK_SKEW = 1;
1439
1440 protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
1441 return ( ( $curTTL + self::CLOCK_SKEW ) < $lowTTL );
1442 }
1443 }
1444
1445 class PopularityRefreshingWANObjectCache extends TimeAdjustableWANObjectCache {
1446 protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
1447 return ( ( $now - $asOf ) > $timeTillRefresh );
1448 }
1449 }