44
55use MediaWiki \Extension \NamespaceManager \NamespaceRepository ;
66use MediaWikiIntegrationTestCase ;
7+ use Psr \Log \NullLogger ;
8+ use Wikimedia \ObjectCache \HashBagOStuff ;
9+ use Wikimedia \ObjectCache \WANObjectCache ;
10+ use Wikimedia \Rdbms \IConnectionProvider ;
711use Wikimedia \Rdbms \IDatabase ;
812
913/**
@@ -66,6 +70,105 @@ public function testReplaceWithEmptyListDeletesAllRows(): void {
6670 $ this ->assertTrue ( $ this ->repository ->isEmpty () );
6771 }
6872
73+ public function testInvalidationRejectsConcurrentStaleCacheFill (): void {
74+ $ cache = new WANObjectCache ( [ 'cache ' => new HashBagOStuff () ] );
75+ $ mockTime = 1_700_000_000.0 ;
76+ $ cache ->setMockTime ( $ mockTime );
77+ $ repository = $ this ->newCacheOnlyRepository ( $ cache );
78+ $ key = $ cache ->makeKey ( 'namespacemanager ' , 'definitions ' );
79+ $ checkKey = $ cache ->makeKey ( 'namespacemanager ' , 'definitions ' , 'check ' );
80+ $ options = $ this ->getCacheOptions ( $ checkKey );
81+
82+ $ stale = $ cache ->getWithSetCallback (
83+ $ key ,
84+ WANObjectCache::TTL_WEEK ,
85+ static function () use ( $ repository ): string {
86+ $ repository ->invalidate ();
87+ return 'stale ' ;
88+ },
89+ $ options
90+ );
91+ $ cache ->setMockTime ( $ mockTime + 20 );
92+ $ regenerations = 0 ;
93+ $ fresh = $ cache ->getWithSetCallback (
94+ $ key ,
95+ WANObjectCache::TTL_WEEK ,
96+ static function () use ( &$ regenerations ): string {
97+ $ regenerations ++;
98+ return 'fresh ' ;
99+ },
100+ $ options
101+ );
102+
103+ $ this ->assertSame ( 'stale ' , $ stale );
104+ $ this ->assertSame ( 'fresh ' , $ fresh );
105+ $ this ->assertSame ( 1 , $ regenerations );
106+ }
107+
108+ public function testInvalidationDoesNotPurgeConcurrentFreshCacheFill (): void {
109+ $ cacheBag = new CallbackHashBagOStuff ();
110+ $ writerCache = new WANObjectCache ( [ 'cache ' => $ cacheBag ] );
111+ $ readerCache = new WANObjectCache ( [ 'cache ' => $ cacheBag ] );
112+ $ repository = $ this ->newCacheOnlyRepository ( $ writerCache );
113+ $ key = $ readerCache ->makeKey ( 'namespacemanager ' , 'definitions ' );
114+ $ checkKey = $ readerCache ->makeKey ( 'namespacemanager ' , 'definitions ' , 'check ' );
115+ $ options = $ this ->getCacheOptions ( $ checkKey );
116+ $ regenerations = 0 ;
117+
118+ $ cacheBag ->afterNextSet (
119+ static function () use (
120+ $ readerCache ,
121+ $ key ,
122+ $ options ,
123+ &$ regenerations
124+ ): void {
125+ $ readerCache ->getWithSetCallback (
126+ $ key ,
127+ WANObjectCache::TTL_WEEK ,
128+ static function () use ( &$ regenerations ): string {
129+ $ regenerations ++;
130+ return 'fresh ' ;
131+ },
132+ $ options
133+ );
134+ }
135+ );
136+
137+ $ repository ->invalidate ();
138+ $ value = $ readerCache ->getWithSetCallback (
139+ $ key ,
140+ WANObjectCache::TTL_WEEK ,
141+ static function () use ( &$ regenerations ): string {
142+ $ regenerations ++;
143+ return 'unexpected ' ;
144+ },
145+ $ options
146+ );
147+
148+ $ this ->assertSame ( 'fresh ' , $ value );
149+ $ this ->assertSame ( 1 , $ regenerations );
150+ }
151+
152+ /**
153+ * @return array<string,mixed>
154+ */
155+ private function getCacheOptions ( string $ checkKey ): array {
156+ return [
157+ 'checkKeys ' => [ $ checkKey ],
158+ 'hotTTR ' => WANObjectCache::TTL_HOUR ,
159+ 'lockTSE ' => 30 ,
160+ 'version ' => 1 ,
161+ ];
162+ }
163+
164+ private function newCacheOnlyRepository ( WANObjectCache $ cache ): NamespaceRepository {
165+ return new NamespaceRepository (
166+ $ this ->createMock ( IConnectionProvider::class ),
167+ $ cache ,
168+ new NullLogger ()
169+ );
170+ }
171+
69172 /**
70173 * @return array<int,array<string,mixed>>
71174 */
@@ -90,3 +193,30 @@ private function getDefinitions(): array {
90193 ];
91194 }
92195}
196+
197+ class CallbackHashBagOStuff extends HashBagOStuff {
198+
199+ /** @var callable|null */
200+ private $ afterNextSet ;
201+
202+ public function afterNextSet ( callable $ callback ): void {
203+ $ this ->afterNextSet = $ callback ;
204+ }
205+
206+ /**
207+ * @param string $key
208+ * @param mixed $value
209+ * @param int $exptime
210+ * @param int $flags
211+ * @return bool
212+ */
213+ protected function doSet ( $ key , $ value , $ exptime = 0 , $ flags = 0 ) {
214+ $ result = parent ::doSet ( $ key , $ value , $ exptime , $ flags );
215+ if ( $ this ->afterNextSet !== null ) {
216+ $ callback = $ this ->afterNextSet ;
217+ $ this ->afterNextSet = null ;
218+ $ callback ();
219+ }
220+ return $ result ;
221+ }
222+ }
0 commit comments