Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / tests / phpunit / includes / libs / objectcache / WANObjectCacheTest.php
blobaa46c966ad4622f31d7429d651bd0f904002eca0
1 <?php
3 class WANObjectCacheTest extends PHPUnit_Framework_TestCase {
4 /** @var WANObjectCache */
5 private $cache;
6 /**@var BagOStuff */
7 private $internalCache;
9 protected function setUp() {
10 parent::setUp();
12 $this->cache = new WANObjectCache( [
13 'cache' => new HashBagOStuff(),
14 'pool' => 'testcache-hash',
15 'relayer' => new EventRelayerNull( [] )
16 ] );
18 $wanCache = TestingAccessWrapper::newFromObject( $this->cache );
19 /** @noinspection PhpUndefinedFieldInspection */
20 $this->internalCache = $wanCache->cache;
23 /**
24 * @dataProvider provideSetAndGet
25 * @covers WANObjectCache::set()
26 * @covers WANObjectCache::get()
27 * @covers WANObjectCache::makeKey()
28 * @param mixed $value
29 * @param integer $ttl
31 public function testSetAndGet( $value, $ttl ) {
32 $curTTL = null;
33 $asOf = null;
34 $key = $this->cache->makeKey( 'x', wfRandomString() );
36 $this->cache->get( $key, $curTTL, [], $asOf );
37 $this->assertNull( $curTTL, "Current TTL is null" );
38 $this->assertNull( $asOf, "Current as-of-time is infinite" );
40 $t = microtime( true );
41 $this->cache->set( $key, $value, $ttl );
43 $this->assertEquals( $value, $this->cache->get( $key, $curTTL, [], $asOf ) );
44 if ( is_infinite( $ttl ) || $ttl == 0 ) {
45 $this->assertTrue( is_infinite( $curTTL ), "Current TTL is infinite" );
46 } else {
47 $this->assertGreaterThan( 0, $curTTL, "Current TTL > 0" );
48 $this->assertLessThanOrEqual( $ttl, $curTTL, "Current TTL < nominal TTL" );
50 $this->assertGreaterThanOrEqual( $t - 1, $asOf, "As-of-time in range of set() time" );
51 $this->assertLessThanOrEqual( $t + 1, $asOf, "As-of-time in range of set() time" );
54 public static function provideSetAndGet() {
55 return [
56 [ 14141, 3 ],
57 [ 3535.666, 3 ],
58 [ [], 3 ],
59 [ null, 3 ],
60 [ '0', 3 ],
61 [ (object)[ 'meow' ], 3 ],
62 [ INF, 3 ],
63 [ '', 3 ],
64 [ 'pizzacat', INF ],
68 /**
69 * @covers WANObjectCache::get()
70 * @covers WANObjectCache::makeGlobalKey()
72 public function testGetNotExists() {
73 $key = $this->cache->makeGlobalKey( 'y', wfRandomString(), 'p' );
74 $curTTL = null;
75 $value = $this->cache->get( $key, $curTTL );
77 $this->assertFalse( $value, "Non-existing key has false value" );
78 $this->assertNull( $curTTL, "Non-existing key has null current TTL" );
81 /**
82 * @covers WANObjectCache::set()
84 public function testSetOver() {
85 $key = wfRandomString();
86 for ( $i = 0; $i < 3; ++$i ) {
87 $value = wfRandomString();
88 $this->cache->set( $key, $value, 3 );
90 $this->assertEquals( $this->cache->get( $key ), $value );
94 /**
95 * @covers WANObjectCache::set()
97 public function testStaleSet() {
98 $key = wfRandomString();
99 $value = wfRandomString();
100 $this->cache->set( $key, $value, 3, [ 'since' => microtime( true ) - 30 ] );
102 $this->assertFalse( $this->cache->get( $key ), "Stale set() value ignored" );
105 public function testProcessCache() {
106 $hit = 0;
107 $callback = function () use ( &$hit ) {
108 ++$hit;
109 return 42;
111 $keys = [ wfRandomString(), wfRandomString(), wfRandomString() ];
112 $groups = [ 'thiscache:1', 'thatcache:1', 'somecache:1' ];
114 foreach ( $keys as $i => $key ) {
115 $this->cache->getWithSetCallback(
116 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
118 $this->assertEquals( 3, $hit );
120 foreach ( $keys as $i => $key ) {
121 $this->cache->getWithSetCallback(
122 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
124 $this->assertEquals( 3, $hit, "Values cached" );
126 foreach ( $keys as $i => $key ) {
127 $this->cache->getWithSetCallback(
128 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
130 $this->assertEquals( 6, $hit );
132 foreach ( $keys as $i => $key ) {
133 $this->cache->getWithSetCallback(
134 "$key-2", 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
136 $this->assertEquals( 6, $hit, "New values cached" );
138 foreach ( $keys as $i => $key ) {
139 $this->cache->delete( $key );
140 $this->cache->getWithSetCallback(
141 $key, 100, $callback, [ 'pcTTL' => 5, 'pcGroup' => $groups[$i] ] );
143 $this->assertEquals( 9, $hit, "Values evicted" );
145 $key = reset( $keys );
146 // Get into cache
147 $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
148 $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
149 $this->assertEquals( 10, $hit, "Value cached" );
150 $outerCallback = function () use ( &$callback, $key ) {
151 $v = $this->cache->getWithSetCallback( $key, 100, $callback, [ 'pcTTL' => 5 ] );
153 return 43 + $v;
155 $this->cache->getWithSetCallback( $key, 100, $outerCallback );
156 $this->assertEquals( 11, $hit, "Nested callback value process cache skipped" );
160 * @dataProvider getWithSetCallback_provider
161 * @covers WANObjectCache::getWithSetCallback()
162 * @covers WANObjectCache::doGetWithSetCallback()
163 * @param array $extOpts
164 * @param bool $versioned
166 public function testGetWithSetCallback( array $extOpts, $versioned ) {
167 $cache = $this->cache;
169 $key = wfRandomString();
170 $value = wfRandomString();
171 $cKey1 = wfRandomString();
172 $cKey2 = wfRandomString();
174 $priorValue = null;
175 $priorAsOf = null;
176 $wasSet = 0;
177 $func = function( $old, &$ttl, &$opts, $asOf )
178 use ( &$wasSet, &$priorValue, &$priorAsOf, $value )
180 ++$wasSet;
181 $priorValue = $old;
182 $priorAsOf = $asOf;
183 $ttl = 20; // override with another value
184 return $value;
187 $wasSet = 0;
188 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
189 $this->assertEquals( $value, $v, "Value returned" );
190 $this->assertEquals( 1, $wasSet, "Value regenerated" );
191 $this->assertFalse( $priorValue, "No prior value" );
192 $this->assertNull( $priorAsOf, "No prior value" );
194 $curTTL = null;
195 $cache->get( $key, $curTTL );
196 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
197 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
199 $wasSet = 0;
200 $v = $cache->getWithSetCallback( $key, 30, $func, [
201 'lowTTL' => 0,
202 'lockTSE' => 5,
203 ] + $extOpts );
204 $this->assertEquals( $value, $v, "Value returned" );
205 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
207 $priorTime = microtime( true );
208 usleep( 1 );
209 $wasSet = 0;
210 $v = $cache->getWithSetCallback(
211 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
213 $this->assertEquals( $value, $v, "Value returned" );
214 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
215 $this->assertEquals( $value, $priorValue, "Has prior value" );
216 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
217 $t1 = $cache->getCheckKeyTime( $cKey1 );
218 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
219 $t2 = $cache->getCheckKeyTime( $cKey2 );
220 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
222 $priorTime = microtime( true );
223 $wasSet = 0;
224 $v = $cache->getWithSetCallback(
225 $key, 30, $func, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
227 $this->assertEquals( $value, $v, "Value returned" );
228 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
229 $t1 = $cache->getCheckKeyTime( $cKey1 );
230 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
231 $t2 = $cache->getCheckKeyTime( $cKey2 );
232 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
234 $curTTL = null;
235 $v = $cache->get( $key, $curTTL, [ $cKey1, $cKey2 ] );
236 if ( $versioned ) {
237 $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
238 } else {
239 $this->assertEquals( $value, $v, "Value returned" );
241 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
243 $wasSet = 0;
244 $key = wfRandomString();
245 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
246 $this->assertEquals( $value, $v, "Value returned" );
247 $cache->delete( $key );
248 $v = $cache->getWithSetCallback( $key, 30, $func, [ 'pcTTL' => 5 ] + $extOpts );
249 $this->assertEquals( $value, $v, "Value still returned after deleted" );
250 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
253 public static function getWithSetCallback_provider() {
254 return [
255 [ [], false ],
256 [ [ 'version' => 1 ], true ]
261 * @dataProvider getMultiWithSetCallback_provider
262 * @covers WANObjectCache::getMultiWithSetCallback()
263 * @covers WANObjectCache::makeMultiKeys()
264 * @param array $extOpts
265 * @param bool $versioned
267 public function testGetMultiWithSetCallback( array $extOpts, $versioned ) {
268 $cache = $this->cache;
270 $keyA = wfRandomString();
271 $keyB = wfRandomString();
272 $keyC = wfRandomString();
273 $cKey1 = wfRandomString();
274 $cKey2 = wfRandomString();
276 $priorValue = null;
277 $priorAsOf = null;
278 $wasSet = 0;
279 $genFunc = function ( $id, $old, &$ttl, &$opts, $asOf ) use (
280 &$wasSet, &$priorValue, &$priorAsOf
282 ++$wasSet;
283 $priorValue = $old;
284 $priorAsOf = $asOf;
285 $ttl = 20; // override with another value
286 return "@$id$";
289 $wasSet = 0;
290 $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
291 $value = "@3353$";
292 $v = $cache->getMultiWithSetCallback(
293 $keyedIds, 30, $genFunc, [ 'lockTSE' => 5 ] + $extOpts );
294 $this->assertEquals( $value, $v[$keyA], "Value returned" );
295 $this->assertEquals( 1, $wasSet, "Value regenerated" );
296 $this->assertFalse( $priorValue, "No prior value" );
297 $this->assertNull( $priorAsOf, "No prior value" );
299 $curTTL = null;
300 $cache->get( $keyA, $curTTL );
301 $this->assertLessThanOrEqual( 20, $curTTL, 'Current TTL between 19-20 (overriden)' );
302 $this->assertGreaterThanOrEqual( 19, $curTTL, 'Current TTL between 19-20 (overriden)' );
304 $wasSet = 0;
305 $value = "@efef$";
306 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
307 $v = $cache->getMultiWithSetCallback(
308 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
309 $this->assertEquals( $value, $v[$keyB], "Value returned" );
310 $this->assertEquals( 1, $wasSet, "Value regenerated" );
311 $v = $cache->getMultiWithSetCallback(
312 $keyedIds, 30, $genFunc, [ 'lowTTL' => 0, 'lockTSE' => 5, ] + $extOpts );
313 $this->assertEquals( $value, $v[$keyB], "Value returned" );
314 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
316 $priorTime = microtime( true );
317 usleep( 1 );
318 $wasSet = 0;
319 $keyedIds = new ArrayIterator( [ $keyB => 'efef' ] );
320 $v = $cache->getMultiWithSetCallback(
321 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
323 $this->assertEquals( $value, $v[$keyB], "Value returned" );
324 $this->assertEquals( 1, $wasSet, "Value regenerated due to check keys" );
325 $this->assertEquals( $value, $priorValue, "Has prior value" );
326 $this->assertInternalType( 'float', $priorAsOf, "Has prior value" );
327 $t1 = $cache->getCheckKeyTime( $cKey1 );
328 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check keys generated on miss' );
329 $t2 = $cache->getCheckKeyTime( $cKey2 );
330 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check keys generated on miss' );
332 $priorTime = microtime( true );
333 $value = "@43636$";
334 $wasSet = 0;
335 $keyedIds = new ArrayIterator( [ $keyC => 43636 ] );
336 $v = $cache->getMultiWithSetCallback(
337 $keyedIds, 30, $genFunc, [ 'checkKeys' => [ $cKey1, $cKey2 ] ] + $extOpts
339 $this->assertEquals( $value, $v[$keyC], "Value returned" );
340 $this->assertEquals( 1, $wasSet, "Value regenerated due to still-recent check keys" );
341 $t1 = $cache->getCheckKeyTime( $cKey1 );
342 $this->assertLessThanOrEqual( $priorTime, $t1, 'Check keys did not change again' );
343 $t2 = $cache->getCheckKeyTime( $cKey2 );
344 $this->assertLessThanOrEqual( $priorTime, $t2, 'Check keys did not change again' );
346 $curTTL = null;
347 $v = $cache->get( $keyC, $curTTL, [ $cKey1, $cKey2 ] );
348 if ( $versioned ) {
349 $this->assertEquals( $value, $v[$cache::VFLD_DATA], "Value returned" );
350 } else {
351 $this->assertEquals( $value, $v, "Value returned" );
353 $this->assertLessThanOrEqual( 0, $curTTL, "Value has current TTL < 0 due to check keys" );
355 $wasSet = 0;
356 $key = wfRandomString();
357 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
358 $v = $cache->getMultiWithSetCallback(
359 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
360 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value returned" );
361 $cache->delete( $key );
362 $keyedIds = new ArrayIterator( [ $key => 242424 ] );
363 $v = $cache->getMultiWithSetCallback(
364 $keyedIds, 30, $genFunc, [ 'pcTTL' => 5 ] + $extOpts );
365 $this->assertEquals( "@{$keyedIds[$key]}$", $v[$key], "Value still returned after deleted" );
366 $this->assertEquals( 1, $wasSet, "Value process cached while deleted" );
368 $calls = 0;
369 $ids = [ 1, 2, 3, 4, 5, 6 ];
370 $keyFunc = function ( $id, WANObjectCache $wanCache ) {
371 return $wanCache->makeKey( 'test', $id );
373 $keyedIds = $cache->makeMultiKeys( $ids, $keyFunc );
374 $genFunc = function ( $id, $oldValue, &$ttl, array &$setops ) use ( &$calls ) {
375 ++$calls;
377 return "val-{$id}";
379 $values = $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
381 $this->assertEquals(
382 [ "val-1", "val-2", "val-3", "val-4", "val-5", "val-6" ],
383 array_values( $values ),
384 "Correct values in correct order"
386 $this->assertEquals(
387 array_map( $keyFunc, $ids, array_fill( 0, count( $ids ), $this->cache ) ),
388 array_keys( $values ),
389 "Correct keys in correct order"
391 $this->assertEquals( count( $ids ), $calls );
393 $cache->getMultiWithSetCallback( $keyedIds, 10, $genFunc );
394 $this->assertEquals( count( $ids ), $calls, "Values cached" );
397 public static function getMultiWithSetCallback_provider() {
398 return [
399 [ [], false ],
400 [ [ 'version' => 1 ], true ]
405 * @covers WANObjectCache::getWithSetCallback()
406 * @covers WANObjectCache::doGetWithSetCallback()
408 public function testLockTSE() {
409 $cache = $this->cache;
410 $key = wfRandomString();
411 $value = wfRandomString();
413 $calls = 0;
414 $func = function() use ( &$calls, $value, $cache, $key ) {
415 ++$calls;
416 // Immediately kill any mutex rather than waiting a second
417 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
418 return $value;
421 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
422 $this->assertEquals( $value, $ret );
423 $this->assertEquals( 1, $calls, 'Value was populated' );
425 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
426 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
428 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
429 $ret = $cache->getWithSetCallback( $key, 30, $func,
430 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
431 $this->assertEquals( $value, $ret, 'Old value used' );
432 $this->assertEquals( 1, $calls, 'Callback was not used' );
434 $cache->delete( $key );
435 $ret = $cache->getWithSetCallback( $key, 30, $func,
436 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
437 $this->assertEquals( $value, $ret, 'Callback was used; interim saved' );
438 $this->assertEquals( 2, $calls, 'Callback was used; interim saved' );
440 $ret = $cache->getWithSetCallback( $key, 30, $func,
441 [ 'lockTSE' => 5, 'checkKeys' => $checkKeys ] );
442 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
443 $this->assertEquals( 2, $calls, 'Callback was not used; used interim' );
447 * @covers WANObjectCache::getWithSetCallback()
448 * @covers WANObjectCache::doGetWithSetCallback()
450 public function testLockTSESlow() {
451 $cache = $this->cache;
452 $key = wfRandomString();
453 $value = wfRandomString();
455 $calls = 0;
456 $func = function( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
457 ++$calls;
458 $setOpts['since'] = microtime( true ) - 10;
459 // Immediately kill any mutex rather than waiting a second
460 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
461 return $value;
464 // Value should be marked as stale due to snapshot lag
465 $curTTL = null;
466 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
467 $this->assertEquals( $value, $ret );
468 $this->assertEquals( $value, $cache->get( $key, $curTTL ), 'Value was populated' );
469 $this->assertLessThan( 0, $curTTL, 'Value has negative curTTL' );
470 $this->assertEquals( 1, $calls, 'Value was generated' );
472 // Acquire a lock to verify that getWithSetCallback uses lockTSE properly
473 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
474 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
475 $this->assertEquals( $value, $ret );
476 $this->assertEquals( 1, $calls, 'Callback was not used' );
480 * @covers WANObjectCache::getWithSetCallback()
481 * @covers WANObjectCache::doGetWithSetCallback()
483 public function testBusyValue() {
484 $cache = $this->cache;
485 $key = wfRandomString();
486 $value = wfRandomString();
487 $busyValue = wfRandomString();
489 $calls = 0;
490 $func = function() use ( &$calls, $value, $cache, $key ) {
491 ++$calls;
492 // Immediately kill any mutex rather than waiting a second
493 $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
494 return $value;
497 $ret = $cache->getWithSetCallback( $key, 30, $func, [ 'busyValue' => $busyValue ] );
498 $this->assertEquals( $value, $ret );
499 $this->assertEquals( 1, $calls, 'Value was populated' );
501 // Acquire a lock to verify that getWithSetCallback uses busyValue properly
502 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
504 $checkKeys = [ wfRandomString() ]; // new check keys => force misses
505 $ret = $cache->getWithSetCallback( $key, 30, $func,
506 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
507 $this->assertEquals( $value, $ret, 'Callback used' );
508 $this->assertEquals( 2, $calls, 'Callback used' );
510 $ret = $cache->getWithSetCallback( $key, 30, $func,
511 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
512 $this->assertEquals( $value, $ret, 'Old value used' );
513 $this->assertEquals( 2, $calls, 'Callback was not used' );
515 $cache->delete( $key ); // no value at all anymore and still locked
516 $ret = $cache->getWithSetCallback( $key, 30, $func,
517 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
518 $this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
519 $this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
521 $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
522 $ret = $cache->getWithSetCallback( $key, 30, $func,
523 [ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
524 $this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
525 $this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
527 $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
528 $ret = $cache->getWithSetCallback( $key, 30, $func,
529 [ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
530 $this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
531 $this->assertEquals( 3, $calls, 'Callback was not used; used interim' );
535 * @covers WANObjectCache::getMulti()
537 public function testGetMulti() {
538 $cache = $this->cache;
540 $value1 = [ 'this' => 'is', 'a' => 'test' ];
541 $value2 = [ 'this' => 'is', 'another' => 'test' ];
543 $key1 = wfRandomString();
544 $key2 = wfRandomString();
545 $key3 = wfRandomString();
547 $cache->set( $key1, $value1, 5 );
548 $cache->set( $key2, $value2, 10 );
550 $curTTLs = [];
551 $this->assertEquals(
552 [ $key1 => $value1, $key2 => $value2 ],
553 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs ),
554 'Result array populated'
557 $this->assertEquals( 2, count( $curTTLs ), "Two current TTLs in array" );
558 $this->assertGreaterThan( 0, $curTTLs[$key1], "Key 1 has current TTL > 0" );
559 $this->assertGreaterThan( 0, $curTTLs[$key2], "Key 2 has current TTL > 0" );
561 $cKey1 = wfRandomString();
562 $cKey2 = wfRandomString();
564 $priorTime = microtime( true );
565 usleep( 1 );
566 $curTTLs = [];
567 $this->assertEquals(
568 [ $key1 => $value1, $key2 => $value2 ],
569 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
570 "Result array populated even with new check keys"
572 $t1 = $cache->getCheckKeyTime( $cKey1 );
573 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key 1 generated on miss' );
574 $t2 = $cache->getCheckKeyTime( $cKey2 );
575 $this->assertGreaterThanOrEqual( $priorTime, $t2, 'Check key 2 generated on miss' );
576 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs array set" );
577 $this->assertLessThanOrEqual( 0, $curTTLs[$key1], 'Key 1 has current TTL <= 0' );
578 $this->assertLessThanOrEqual( 0, $curTTLs[$key2], 'Key 2 has current TTL <= 0' );
580 usleep( 1 );
581 $curTTLs = [];
582 $this->assertEquals(
583 [ $key1 => $value1, $key2 => $value2 ],
584 $cache->getMulti( [ $key1, $key2, $key3 ], $curTTLs, [ $cKey1, $cKey2 ] ),
585 "Result array still populated even with new check keys"
587 $this->assertEquals( 2, count( $curTTLs ), "Current TTLs still array set" );
588 $this->assertLessThan( 0, $curTTLs[$key1], 'Key 1 has negative current TTL' );
589 $this->assertLessThan( 0, $curTTLs[$key2], 'Key 2 has negative current TTL' );
593 * @covers WANObjectCache::getMulti()
594 * @covers WANObjectCache::processCheckKeys()
596 public function testGetMultiCheckKeys() {
597 $cache = $this->cache;
599 $checkAll = wfRandomString();
600 $check1 = wfRandomString();
601 $check2 = wfRandomString();
602 $check3 = wfRandomString();
603 $value1 = wfRandomString();
604 $value2 = wfRandomString();
606 // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
607 // several seconds during the test to assert the behaviour.
608 foreach ( [ $checkAll, $check1, $check2 ] as $checkKey ) {
609 $cache->touchCheckKey( $checkKey, WANObjectCache::HOLDOFF_NONE );
611 usleep( 100 );
613 $cache->set( 'key1', $value1, 10 );
614 $cache->set( 'key2', $value2, 10 );
616 $curTTLs = [];
617 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
618 'key1' => $check1,
619 $checkAll,
620 'key2' => $check2,
621 'key3' => $check3,
622 ] );
623 $this->assertEquals(
624 [ 'key1' => $value1, 'key2' => $value2 ],
625 $result,
626 'Initial values'
628 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key1'], 'Initial ttls' );
629 $this->assertLessThanOrEqual( 10.5, $curTTLs['key1'], 'Initial ttls' );
630 $this->assertGreaterThanOrEqual( 9.5, $curTTLs['key2'], 'Initial ttls' );
631 $this->assertLessThanOrEqual( 10.5, $curTTLs['key2'], 'Initial ttls' );
633 $cache->touchCheckKey( $check1 );
635 $curTTLs = [];
636 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
637 'key1' => $check1,
638 $checkAll,
639 'key2' => $check2,
640 'key3' => $check3,
641 ] );
642 $this->assertEquals(
643 [ 'key1' => $value1, 'key2' => $value2 ],
644 $result,
645 'key1 expired by check1, but value still provided'
647 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 TTL expired' );
648 $this->assertGreaterThan( 0, $curTTLs['key2'], 'key2 still valid' );
650 $cache->touchCheckKey( $checkAll );
652 $curTTLs = [];
653 $result = $cache->getMulti( [ 'key1', 'key2', 'key3' ], $curTTLs, [
654 'key1' => $check1,
655 $checkAll,
656 'key2' => $check2,
657 'key3' => $check3,
658 ] );
659 $this->assertEquals(
660 [ 'key1' => $value1, 'key2' => $value2 ],
661 $result,
662 'All keys expired by checkAll, but value still provided'
664 $this->assertLessThan( 0, $curTTLs['key1'], 'key1 expired by checkAll' );
665 $this->assertLessThan( 0, $curTTLs['key2'], 'key2 expired by checkAll' );
669 * @covers WANObjectCache::get()
670 * @covers WANObjectCache::processCheckKeys()
672 public function testCheckKeyInitHoldoff() {
673 $cache = $this->cache;
675 for ( $i = 0; $i < 500; ++$i ) {
676 $key = wfRandomString();
677 $checkKey = wfRandomString();
678 // miss, set, hit
679 $cache->get( $key, $curTTL, [ $checkKey ] );
680 $cache->set( $key, 'val', 10 );
681 $curTTL = null;
682 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
684 $this->assertEquals( 'val', $v );
685 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (miss/set/hit)" );
688 for ( $i = 0; $i < 500; ++$i ) {
689 $key = wfRandomString();
690 $checkKey = wfRandomString();
691 // set, hit
692 $cache->set( $key, 'val', 10 );
693 $curTTL = null;
694 $v = $cache->get( $key, $curTTL, [ $checkKey ] );
696 $this->assertEquals( 'val', $v );
697 $this->assertLessThan( 0, $curTTL, "Step $i: CTL < 0 (set/hit)" );
702 * @covers WANObjectCache::delete()
704 public function testDelete() {
705 $key = wfRandomString();
706 $value = wfRandomString();
707 $this->cache->set( $key, $value );
709 $curTTL = null;
710 $v = $this->cache->get( $key, $curTTL );
711 $this->assertEquals( $value, $v, "Key was created with value" );
712 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
714 $this->cache->delete( $key );
716 $curTTL = null;
717 $v = $this->cache->get( $key, $curTTL );
718 $this->assertFalse( $v, "Deleted key has false value" );
719 $this->assertLessThan( 0, $curTTL, "Deleted key has current TTL < 0" );
721 $this->cache->set( $key, $value . 'more' );
722 $v = $this->cache->get( $key, $curTTL );
723 $this->assertFalse( $v, "Deleted key is tombstoned and has false value" );
724 $this->assertLessThan( 0, $curTTL, "Deleted key is tombstoned and has current TTL < 0" );
726 $this->cache->set( $key, $value );
727 $this->cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
729 $curTTL = null;
730 $v = $this->cache->get( $key, $curTTL );
731 $this->assertFalse( $v, "Deleted key has false value" );
732 $this->assertNull( $curTTL, "Deleted key has null current TTL" );
734 $this->cache->set( $key, $value );
735 $v = $this->cache->get( $key, $curTTL );
736 $this->assertEquals( $value, $v, "Key was created with value" );
737 $this->assertGreaterThan( 0, $curTTL, "Existing key has current TTL > 0" );
741 * @dataProvider getWithSetCallback_versions_provider
742 * @param array $extOpts
743 * @param $versioned
745 public function testGetWithSetCallback_versions( array $extOpts, $versioned ) {
746 $cache = $this->cache;
748 $key = wfRandomString();
749 $value = wfRandomString();
751 $wasSet = 0;
752 $func = function( $old, &$ttl ) use ( &$wasSet, $value ) {
753 ++$wasSet;
754 return $value;
757 // Set the main key (version N if versioned)
758 $wasSet = 0;
759 $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts );
760 $this->assertEquals( $value, $v, "Value returned" );
761 $this->assertEquals( 1, $wasSet, "Value regenerated" );
762 $cache->getWithSetCallback( $key, 30, $func, $extOpts );
763 $this->assertEquals( 1, $wasSet, "Value not regenerated" );
764 // Set the key for version N+1 (if versioned)
765 if ( $versioned ) {
766 $verOpts = [ 'version' => $extOpts['version'] + 1 ];
768 $wasSet = 0;
769 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
770 $this->assertEquals( $value, $v, "Value returned" );
771 $this->assertEquals( 1, $wasSet, "Value regenerated" );
773 $wasSet = 0;
774 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
775 $this->assertEquals( $value, $v, "Value returned" );
776 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
779 $wasSet = 0;
780 $cache->getWithSetCallback( $key, 30, $func, $extOpts );
781 $this->assertEquals( 0, $wasSet, "Value not regenerated" );
783 $wasSet = 0;
784 $cache->delete( $key );
785 $v = $cache->getWithSetCallback( $key, 30, $func, $extOpts );
786 $this->assertEquals( $value, $v, "Value returned" );
787 $this->assertEquals( 1, $wasSet, "Value regenerated" );
789 if ( $versioned ) {
790 $wasSet = 0;
791 $verOpts = [ 'version' => $extOpts['version'] + 1 ];
792 $v = $cache->getWithSetCallback( $key, 30, $func, $verOpts + $extOpts );
793 $this->assertEquals( $value, $v, "Value returned" );
794 $this->assertEquals( 1, $wasSet, "Value regenerated" );
798 public static function getWithSetCallback_versions_provider() {
799 return [
800 [ [], false ],
801 [ [ 'version' => 1 ], true ]
806 * @covers WANObjectCache::touchCheckKey()
807 * @covers WANObjectCache::resetCheckKey()
808 * @covers WANObjectCache::getCheckKeyTime()
810 public function testTouchKeys() {
811 $key = wfRandomString();
813 $priorTime = microtime( true );
814 usleep( 100 );
815 $t0 = $this->cache->getCheckKeyTime( $key );
816 $this->assertGreaterThanOrEqual( $priorTime, $t0, 'Check key auto-created' );
818 $priorTime = microtime( true );
819 usleep( 100 );
820 $this->cache->touchCheckKey( $key );
821 $t1 = $this->cache->getCheckKeyTime( $key );
822 $this->assertGreaterThanOrEqual( $priorTime, $t1, 'Check key created' );
824 $t2 = $this->cache->getCheckKeyTime( $key );
825 $this->assertEquals( $t1, $t2, 'Check key time did not change' );
827 usleep( 100 );
828 $this->cache->touchCheckKey( $key );
829 $t3 = $this->cache->getCheckKeyTime( $key );
830 $this->assertGreaterThan( $t2, $t3, 'Check key time increased' );
832 $t4 = $this->cache->getCheckKeyTime( $key );
833 $this->assertEquals( $t3, $t4, 'Check key time did not change' );
835 usleep( 100 );
836 $this->cache->resetCheckKey( $key );
837 $t5 = $this->cache->getCheckKeyTime( $key );
838 $this->assertGreaterThan( $t4, $t5, 'Check key time increased' );
840 $t6 = $this->cache->getCheckKeyTime( $key );
841 $this->assertEquals( $t5, $t6, 'Check key time did not change' );
845 * @covers WANObjectCache::getMulti()
847 public function testGetWithSeveralCheckKeys() {
848 $key = wfRandomString();
849 $tKey1 = wfRandomString();
850 $tKey2 = wfRandomString();
851 $value = 'meow';
853 // Two check keys are newer (given hold-off) than $key, another is older
854 $this->internalCache->set(
855 WANObjectCache::TIME_KEY_PREFIX . $tKey2,
856 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 3 )
858 $this->internalCache->set(
859 WANObjectCache::TIME_KEY_PREFIX . $tKey2,
860 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 5 )
862 $this->internalCache->set(
863 WANObjectCache::TIME_KEY_PREFIX . $tKey1,
864 WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 30 )
866 $this->cache->set( $key, $value, 30 );
868 $curTTL = null;
869 $v = $this->cache->get( $key, $curTTL, [ $tKey1, $tKey2 ] );
870 $this->assertEquals( $value, $v, "Value matches" );
871 $this->assertLessThan( -4.9, $curTTL, "Correct CTL" );
872 $this->assertGreaterThan( -5.1, $curTTL, "Correct CTL" );
876 * @covers WANObjectCache::set()
878 public function testSetWithLag() {
879 $value = 1;
881 $key = wfRandomString();
882 $opts = [ 'lag' => 300, 'since' => microtime( true ) ];
883 $this->cache->set( $key, $value, 30, $opts );
884 $this->assertEquals( $value, $this->cache->get( $key ), "Rep-lagged value written." );
886 $key = wfRandomString();
887 $opts = [ 'lag' => 0, 'since' => microtime( true ) - 300 ];
888 $this->cache->set( $key, $value, 30, $opts );
889 $this->assertEquals( false, $this->cache->get( $key ), "Trx-lagged value not written." );
891 $key = wfRandomString();
892 $opts = [ 'lag' => 5, 'since' => microtime( true ) - 5 ];
893 $this->cache->set( $key, $value, 30, $opts );
894 $this->assertEquals( false, $this->cache->get( $key ), "Lagged value not written." );
898 * @covers WANObjectCache::set()
900 public function testWritePending() {
901 $value = 1;
903 $key = wfRandomString();
904 $opts = [ 'pending' => true ];
905 $this->cache->set( $key, $value, 30, $opts );
906 $this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
909 public function testMcRouterSupport() {
910 $localBag = $this->getMock( 'EmptyBagOStuff', [ 'set', 'delete' ] );
911 $localBag->expects( $this->never() )->method( 'set' );
912 $localBag->expects( $this->never() )->method( 'delete' );
913 $wanCache = new WANObjectCache( [
914 'cache' => $localBag,
915 'pool' => 'testcache-hash',
916 'relayer' => new EventRelayerNull( [] )
917 ] );
918 $valFunc = function () {
919 return 1;
922 // None of these should use broadcasting commands (e.g. SET, DELETE)
923 $wanCache->get( 'x' );
924 $wanCache->get( 'x', $ctl, [ 'check1' ] );
925 $wanCache->getMulti( [ 'x', 'y' ] );
926 $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
927 $wanCache->getWithSetCallback( 'p', 30, $valFunc );
928 $wanCache->getCheckKeyTime( 'zzz' );
932 * @dataProvider provideAdaptiveTTL
933 * @covers WANObjectCache::adaptiveTTL()
934 * @param float|int $ago
935 * @param int $maxTTL
936 * @param int $minTTL
937 * @param float $factor
938 * @param int $adaptiveTTL
940 public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
941 $mtime = $ago ? time() - $ago : $ago;
942 $margin = 5;
943 $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
945 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
946 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
948 $ttl = $this->cache->adaptiveTTL( (string)$mtime, $maxTTL, $minTTL, $factor );
950 $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
951 $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
954 public static function provideAdaptiveTTL() {
955 return [
956 [ 3600, 900, 30, .2, 720 ],
957 [ 3600, 500, 30, .2, 500 ],
958 [ 3600, 86400, 800, .2, 800 ],
959 [ false, 86400, 800, .2, 800 ],
960 [ null, 86400, 800, .2, 800 ]