Merge "MWExceptionRenderer: Fix db error outage page"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / objectcache / BagOStuffTest.php
1 <?php
2
3 use Wikimedia\ScopedCallback;
4
5 /**
6 * @author Matthias Mullie <mmullie@wikimedia.org>
7 * @group BagOStuff
8 */
9 class BagOStuffTest extends MediaWikiTestCase {
10 /** @var BagOStuff */
11 private $cache;
12
13 const TEST_KEY = 'test';
14
15 protected function setUp() {
16 parent::setUp();
17
18 // type defined through parameter
19 if ( $this->getCliArg( 'use-bagostuff' ) ) {
20 $name = $this->getCliArg( 'use-bagostuff' );
21
22 $this->cache = ObjectCache::newFromId( $name );
23 } else {
24 // no type defined - use simple hash
25 $this->cache = new HashBagOStuff;
26 }
27
28 $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) );
29 }
30
31 /**
32 * @covers BagOStuff::makeGlobalKey
33 * @covers BagOStuff::makeKeyInternal
34 */
35 public function testMakeKey() {
36 $cache = ObjectCache::newFromId( 'hash' );
37
38 $localKey = $cache->makeKey( 'first', 'second', 'third' );
39 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
40
41 $this->assertStringMatchesFormat(
42 '%Sfirst%Ssecond%Sthird%S',
43 $localKey,
44 'Local key interpolates parameters'
45 );
46
47 $this->assertStringMatchesFormat(
48 'global%Sfirst%Ssecond%Sthird%S',
49 $globalKey,
50 'Global key interpolates parameters and contains global prefix'
51 );
52
53 $this->assertNotEquals(
54 $localKey,
55 $globalKey,
56 'Local key and global key with same parameters should not be equal'
57 );
58
59 $this->assertNotEquals(
60 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc:', 'de' ] ),
61 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc', ':de' ] )
62 );
63 }
64
65 /**
66 * @covers BagOStuff::merge
67 * @covers BagOStuff::mergeViaLock
68 * @covers BagOStuff::mergeViaCas
69 */
70 public function testMerge() {
71 $key = $this->cache->makeKey( self::TEST_KEY );
72 $callback = function ( BagOStuff $cache, $key, $oldVal ) {
73 return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
74 };
75
76 // merge on non-existing value
77 $merged = $this->cache->merge( $key, $callback, 5 );
78 $this->assertTrue( $merged );
79 $this->assertEquals( 'merged', $this->cache->get( $key ) );
80
81 // merge on existing value
82 $merged = $this->cache->merge( $key, $callback, 5 );
83 $this->assertTrue( $merged );
84 $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) );
85 }
86
87 /**
88 * @covers BagOStuff::merge
89 * @covers BagOStuff::mergeViaLock
90 */
91 public function testMerge_fork() {
92 $key = $this->cache->makeKey( self::TEST_KEY );
93 $callback = function ( BagOStuff $cache, $key, $oldVal ) {
94 return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
95 };
96 /*
97 * Test concurrent merges by forking this process, if:
98 * - not manually called with --use-bagostuff
99 * - pcntl_fork is supported by the system
100 * - cache type will correctly support calls over forks
101 */
102 $fork = (bool)$this->getCliArg( 'use-bagostuff' );
103 $fork &= function_exists( 'pcntl_fork' );
104 $fork &= !$this->cache instanceof HashBagOStuff;
105 $fork &= !$this->cache instanceof EmptyBagOStuff;
106 $fork &= !$this->cache instanceof MultiWriteBagOStuff;
107 if ( $fork ) {
108 $pid = null;
109 // Function to start merge(), run another merge() midway through, then finish
110 $outerFunc = function ( BagOStuff $cache, $key, $oldVal ) use ( $callback, &$pid ) {
111 $pid = pcntl_fork();
112 if ( $pid == -1 ) {
113 return false;
114 } elseif ( $pid ) {
115 pcntl_wait( $status );
116
117 return $callback( $cache, $key, $oldVal );
118 } else {
119 $this->cache->merge( $key, $callback, 0, 1 );
120 // Bail out of the outer merge() in the child process since it does not
121 // need to attempt to write anything. Success is checked by the parent.
122 parent::tearDown(); // avoid phpunit notices
123 exit;
124 }
125 };
126
127 // attempt a merge - this should fail
128 $merged = $this->cache->merge( $key, $outerFunc, 0, 1 );
129
130 if ( $pid == -1 ) {
131 return; // can't fork, ignore this test...
132 }
133
134 // merge has failed because child process was merging (and we only attempted once)
135 $this->assertFalse( $merged );
136
137 // make sure the child's merge is completed and verify
138 $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
139 } else {
140 $this->markTestSkipped( 'No pcntl methods available' );
141 }
142 }
143
144 /**
145 * @covers BagOStuff::changeTTL
146 */
147 public function testChangeTTL() {
148 $key = $this->cache->makeKey( self::TEST_KEY );
149 $value = 'meow';
150
151 $this->cache->add( $key, $value, 5 );
152 $this->assertTrue( $this->cache->changeTTL( $key, 5 ) );
153 $this->assertEquals( $this->cache->get( $key ), $value );
154 $this->cache->delete( $key );
155 $this->assertFalse( $this->cache->changeTTL( $key, 5 ) );
156 }
157
158 /**
159 * @covers BagOStuff::add
160 */
161 public function testAdd() {
162 $key = $this->cache->makeKey( self::TEST_KEY );
163 $this->assertTrue( $this->cache->add( $key, 'test', 5 ) );
164 }
165
166 /**
167 * @covers BagOStuff::get
168 */
169 public function testGet() {
170 $value = [ 'this' => 'is', 'a' => 'test' ];
171
172 $key = $this->cache->makeKey( self::TEST_KEY );
173 $this->cache->add( $key, $value, 5 );
174 $this->assertEquals( $this->cache->get( $key ), $value );
175 }
176
177 /**
178 * @covers BagOStuff::get
179 * @covers BagOStuff::set
180 * @covers BagOStuff::getWithSetCallback
181 */
182 public function testGetWithSetCallback() {
183 $key = $this->cache->makeKey( self::TEST_KEY );
184 $value = $this->cache->getWithSetCallback(
185 $key,
186 30,
187 function () {
188 return 'hello kitty';
189 }
190 );
191
192 $this->assertEquals( 'hello kitty', $value );
193 $this->assertEquals( $value, $this->cache->get( $key ) );
194 }
195
196 /**
197 * @covers BagOStuff::incr
198 */
199 public function testIncr() {
200 $key = $this->cache->makeKey( self::TEST_KEY );
201 $this->cache->add( $key, 0, 5 );
202 $this->cache->incr( $key );
203 $expectedValue = 1;
204 $actualValue = $this->cache->get( $key );
205 $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
206 }
207
208 /**
209 * @covers BagOStuff::incrWithInit
210 */
211 public function testIncrWithInit() {
212 $key = $this->cache->makeKey( self::TEST_KEY );
213 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
214 $this->assertEquals( 3, $val, "Correct init value" );
215
216 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
217 $this->assertEquals( 4, $val, "Correct init value" );
218 }
219
220 /**
221 * @covers BagOStuff::getMulti
222 */
223 public function testGetMulti() {
224 $value1 = [ 'this' => 'is', 'a' => 'test' ];
225 $value2 = [ 'this' => 'is', 'another' => 'test' ];
226 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
227 $value4 = [ 'another test where chars in key will be encoded' ];
228
229 $key1 = $this->cache->makeKey( 'test-1' );
230 $key2 = $this->cache->makeKey( 'test-2' );
231 // internally, MemcachedBagOStuffs will encode to will-%25-encode
232 $key3 = $this->cache->makeKey( 'will-%-encode' );
233 $key4 = $this->cache->makeKey(
234 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
235 );
236
237 // cleanup
238 $this->cache->delete( $key1 );
239 $this->cache->delete( $key2 );
240 $this->cache->delete( $key3 );
241 $this->cache->delete( $key4 );
242
243 $this->cache->add( $key1, $value1, 5 );
244 $this->cache->add( $key2, $value2, 5 );
245 $this->cache->add( $key3, $value3, 5 );
246 $this->cache->add( $key4, $value4, 5 );
247
248 $this->assertEquals(
249 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
250 $this->cache->getMulti( [ $key1, $key2, $key3, $key4 ] )
251 );
252
253 // cleanup
254 $this->cache->delete( $key1 );
255 $this->cache->delete( $key2 );
256 $this->cache->delete( $key3 );
257 $this->cache->delete( $key4 );
258 }
259
260 /**
261 * @covers BagOStuff::getScopedLock
262 */
263 public function testGetScopedLock() {
264 $key = $this->cache->makeKey( self::TEST_KEY );
265 $value1 = $this->cache->getScopedLock( $key, 0 );
266 $value2 = $this->cache->getScopedLock( $key, 0 );
267
268 $this->assertType( ScopedCallback::class, $value1, 'First call returned lock' );
269 $this->assertNull( $value2, 'Duplicate call returned no lock' );
270
271 unset( $value1 );
272
273 $value3 = $this->cache->getScopedLock( $key, 0 );
274 $this->assertType( ScopedCallback::class, $value3, 'Lock returned callback after release' );
275 unset( $value3 );
276
277 $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
278 $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
279
280 $this->assertType( ScopedCallback::class, $value1, 'First reentrant call returned lock' );
281 $this->assertType( ScopedCallback::class, $value1, 'Second reentrant call returned lock' );
282 }
283
284 /**
285 * @covers BagOStuff::__construct
286 * @covers BagOStuff::trackDuplicateKeys
287 */
288 public function testReportDupes() {
289 $logger = $this->createMock( Psr\Log\NullLogger::class );
290 $logger->expects( $this->once() )
291 ->method( 'warning' )
292 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
293 'key' => 'foo',
294 'count' => 2,
295 ] );
296
297 $cache = new HashBagOStuff( [
298 'reportDupes' => true,
299 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
300 'logger' => $logger,
301 ] );
302 $cache->get( 'foo' );
303 $cache->get( 'bar' );
304 $cache->get( 'foo' );
305
306 DeferredUpdates::doUpdates();
307 }
308 }