Fix WatchedItemStore last-seen stashing logic
[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 $calls = 0;
72 $key = $this->cache->makeKey( self::TEST_KEY );
73 $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls ) {
74 ++$calls;
75
76 return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
77 };
78
79 // merge on non-existing value
80 $merged = $this->cache->merge( $key, $callback, 5 );
81 $this->assertTrue( $merged );
82 $this->assertEquals( 'merged', $this->cache->get( $key ) );
83
84 // merge on existing value
85 $merged = $this->cache->merge( $key, $callback, 5 );
86 $this->assertTrue( $merged );
87 $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) );
88
89 $calls = 0;
90 $this->cache->lock( $key );
91 $this->assertFalse( $this->cache->merge( $key, $callback, 1 ), 'Non-blocking merge' );
92 $this->cache->unlock( $key );
93 $this->assertEquals( 0, $calls );
94 }
95
96 /**
97 * @covers BagOStuff::merge
98 * @covers BagOStuff::mergeViaLock
99 */
100 public function testMerge_fork() {
101 $key = $this->cache->makeKey( self::TEST_KEY );
102 $callback = function ( BagOStuff $cache, $key, $oldVal ) {
103 return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
104 };
105 /*
106 * Test concurrent merges by forking this process, if:
107 * - not manually called with --use-bagostuff
108 * - pcntl_fork is supported by the system
109 * - cache type will correctly support calls over forks
110 */
111 $fork = (bool)$this->getCliArg( 'use-bagostuff' );
112 $fork &= function_exists( 'pcntl_fork' );
113 $fork &= !$this->cache instanceof HashBagOStuff;
114 $fork &= !$this->cache instanceof EmptyBagOStuff;
115 $fork &= !$this->cache instanceof MultiWriteBagOStuff;
116 if ( $fork ) {
117 $pid = null;
118 // Function to start merge(), run another merge() midway through, then finish
119 $outerFunc = function ( BagOStuff $cache, $key, $oldVal ) use ( $callback, &$pid ) {
120 $pid = pcntl_fork();
121 if ( $pid == -1 ) {
122 return false;
123 } elseif ( $pid ) {
124 pcntl_wait( $status );
125
126 return $callback( $cache, $key, $oldVal );
127 } else {
128 $this->cache->merge( $key, $callback, 0, 1 );
129 // Bail out of the outer merge() in the child process since it does not
130 // need to attempt to write anything. Success is checked by the parent.
131 parent::tearDown(); // avoid phpunit notices
132 exit;
133 }
134 };
135
136 // attempt a merge - this should fail
137 $merged = $this->cache->merge( $key, $outerFunc, 0, 1 );
138
139 if ( $pid == -1 ) {
140 return; // can't fork, ignore this test...
141 }
142
143 // merge has failed because child process was merging (and we only attempted once)
144 $this->assertFalse( $merged );
145
146 // make sure the child's merge is completed and verify
147 $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
148 } else {
149 $this->markTestSkipped( 'No pcntl methods available' );
150 }
151 }
152
153 /**
154 * @covers BagOStuff::changeTTL
155 */
156 public function testChangeTTL() {
157 $key = $this->cache->makeKey( self::TEST_KEY );
158 $value = 'meow';
159
160 $this->cache->add( $key, $value, 5 );
161 $this->assertTrue( $this->cache->changeTTL( $key, 5 ) );
162 $this->assertEquals( $this->cache->get( $key ), $value );
163 $this->cache->delete( $key );
164 $this->assertFalse( $this->cache->changeTTL( $key, 5 ) );
165 }
166
167 /**
168 * @covers BagOStuff::add
169 */
170 public function testAdd() {
171 $key = $this->cache->makeKey( self::TEST_KEY );
172 $this->assertTrue( $this->cache->add( $key, 'test', 5 ) );
173 }
174
175 /**
176 * @covers BagOStuff::get
177 */
178 public function testGet() {
179 $value = [ 'this' => 'is', 'a' => 'test' ];
180
181 $key = $this->cache->makeKey( self::TEST_KEY );
182 $this->cache->add( $key, $value, 5 );
183 $this->assertEquals( $this->cache->get( $key ), $value );
184 }
185
186 /**
187 * @covers BagOStuff::get
188 * @covers BagOStuff::set
189 * @covers BagOStuff::getWithSetCallback
190 */
191 public function testGetWithSetCallback() {
192 $key = $this->cache->makeKey( self::TEST_KEY );
193 $value = $this->cache->getWithSetCallback(
194 $key,
195 30,
196 function () {
197 return 'hello kitty';
198 }
199 );
200
201 $this->assertEquals( 'hello kitty', $value );
202 $this->assertEquals( $value, $this->cache->get( $key ) );
203 }
204
205 /**
206 * @covers BagOStuff::incr
207 */
208 public function testIncr() {
209 $key = $this->cache->makeKey( self::TEST_KEY );
210 $this->cache->add( $key, 0, 5 );
211 $this->cache->incr( $key );
212 $expectedValue = 1;
213 $actualValue = $this->cache->get( $key );
214 $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
215 }
216
217 /**
218 * @covers BagOStuff::incrWithInit
219 */
220 public function testIncrWithInit() {
221 $key = $this->cache->makeKey( self::TEST_KEY );
222 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
223 $this->assertEquals( 3, $val, "Correct init value" );
224
225 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
226 $this->assertEquals( 4, $val, "Correct init value" );
227 }
228
229 /**
230 * @covers BagOStuff::getMulti
231 */
232 public function testGetMulti() {
233 $value1 = [ 'this' => 'is', 'a' => 'test' ];
234 $value2 = [ 'this' => 'is', 'another' => 'test' ];
235 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
236 $value4 = [ 'another test where chars in key will be encoded' ];
237
238 $key1 = $this->cache->makeKey( 'test-1' );
239 $key2 = $this->cache->makeKey( 'test-2' );
240 // internally, MemcachedBagOStuffs will encode to will-%25-encode
241 $key3 = $this->cache->makeKey( 'will-%-encode' );
242 $key4 = $this->cache->makeKey(
243 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
244 );
245
246 // cleanup
247 $this->cache->delete( $key1 );
248 $this->cache->delete( $key2 );
249 $this->cache->delete( $key3 );
250 $this->cache->delete( $key4 );
251
252 $this->cache->add( $key1, $value1, 5 );
253 $this->cache->add( $key2, $value2, 5 );
254 $this->cache->add( $key3, $value3, 5 );
255 $this->cache->add( $key4, $value4, 5 );
256
257 $this->assertEquals(
258 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
259 $this->cache->getMulti( [ $key1, $key2, $key3, $key4 ] )
260 );
261
262 // cleanup
263 $this->cache->delete( $key1 );
264 $this->cache->delete( $key2 );
265 $this->cache->delete( $key3 );
266 $this->cache->delete( $key4 );
267 }
268
269 /**
270 * @covers BagOStuff::getScopedLock
271 */
272 public function testGetScopedLock() {
273 $key = $this->cache->makeKey( self::TEST_KEY );
274 $value1 = $this->cache->getScopedLock( $key, 0 );
275 $value2 = $this->cache->getScopedLock( $key, 0 );
276
277 $this->assertType( ScopedCallback::class, $value1, 'First call returned lock' );
278 $this->assertNull( $value2, 'Duplicate call returned no lock' );
279
280 unset( $value1 );
281
282 $value3 = $this->cache->getScopedLock( $key, 0 );
283 $this->assertType( ScopedCallback::class, $value3, 'Lock returned callback after release' );
284 unset( $value3 );
285
286 $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
287 $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
288
289 $this->assertType( ScopedCallback::class, $value1, 'First reentrant call returned lock' );
290 $this->assertType( ScopedCallback::class, $value1, 'Second reentrant call returned lock' );
291 }
292
293 /**
294 * @covers BagOStuff::__construct
295 * @covers BagOStuff::trackDuplicateKeys
296 */
297 public function testReportDupes() {
298 $logger = $this->createMock( Psr\Log\NullLogger::class );
299 $logger->expects( $this->once() )
300 ->method( 'warning' )
301 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
302 'key' => 'foo',
303 'count' => 2,
304 ] );
305
306 $cache = new HashBagOStuff( [
307 'reportDupes' => true,
308 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
309 'logger' => $logger,
310 ] );
311 $cache->get( 'foo' );
312 $cache->get( 'bar' );
313 $cache->get( 'foo' );
314
315 DeferredUpdates::doUpdates();
316 }
317
318 /**
319 * @covers BagOStuff::lock()
320 * @covers BagOStuff::unlock()
321 */
322 public function testLocking() {
323 $key = 'test';
324 $this->assertTrue( $this->cache->lock( $key ) );
325 $this->assertFalse( $this->cache->lock( $key ) );
326 $this->assertTrue( $this->cache->unlock( $key ) );
327
328 $key2 = 'test2';
329 $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
330 $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
331 $this->assertTrue( $this->cache->unlock( $key2 ) );
332 $this->assertTrue( $this->cache->unlock( $key2 ) );
333 }
334 }